Repository navigation
fix(responses): carry WS dispatch override, pooled key rotation, usage accounting and Kiro refusal hop (B3) #7389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7160e0
35ba672
fce3e5b
9d27e77
3397674
35f68a5
10eb255
f530474
1b29dc6
9918fd7
a710d9b
e3b4564
1e7d626
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -248,6 +248,20 @@ export function sendWithConnectionPolicy( | |
| }); | ||
| } | ||
|
|
||
| interface CodexWsExecutor { provider: OcxProviderConfig; http: typeof globalThis.fetch } | ||
| const codexWsExecutors = new WeakMap<object, CodexWsExecutor>(); | ||
| const codexWsExecutor = (send: (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => Promise<Response>, binding: CodexWsExecutor) => { | ||
| codexWsExecutors.set(send, binding); | ||
| return send as typeof globalThis.fetch; | ||
| }; | ||
| /** | ||
| * Set for the executor a `dispatchOverride` receives on the WS branch: it must not get HTTP-only | ||
| * send policy. `provider` is the one WS eligibility was checked against; a dispatch-time | ||
| * replacement was not, so it sends through `http` (the same fallback) instead. | ||
| */ | ||
| export const codexWsExecutorBinding = (execute: unknown): CodexWsExecutor | undefined => | ||
| typeof execute === "function" ? codexWsExecutors.get(execute) : undefined; | ||
|
|
||
| export interface ProviderFetchOptions { | ||
| accountAutoPause?: (name: string, provider: OcxProviderConfig, headers: Headers) => AccountAutoPauseObserver | undefined; | ||
| /** | ||
|
|
@@ -330,7 +344,9 @@ export function providerFetch( | |
| ) as typeof globalThis.fetch); | ||
| const httpFetch = Object.assign( | ||
| async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit, | ||
| reacquirePacing?: (provider: OcxProviderConfig, modelId?: string) => Promise<void>) => { | ||
| reacquirePacing?: (provider: OcxProviderConfig, modelId?: string) => Promise<void>, | ||
| // False only for the HTTP fallback of a WS send the override already wrapped: one logical send. | ||
| override: ProviderFetchOptions["dispatchOverride"] | null = options.dispatchOverride) => { | ||
| // Refuse before any dispatch side effect where that is sound. `beforeDispatch` commits | ||
| // attempt accounting and consumes admission state, so a refusal firing after it would | ||
| // charge an attempt for a send that never happens, and a throwing hook would mask the | ||
|
|
@@ -340,7 +356,7 @@ export function providerFetch( | |
| // full decision can be made now. With an override, only the configured value is checked: | ||
| // the override may rebuild against a different host and select a different transport, and | ||
| // refusing on this destination would reject a request whose real route is fine. | ||
| if (options.dispatchOverride) egressFor(input); | ||
| if (override) egressFor(input); | ||
| else providerEgressSendInit(egressBinding, transport, input); | ||
| // The hook inspects the outgoing headers and refuses the send by throwing; it is not a | ||
| // mutator, and the copy it receives is deliberately not threaded onward. `Connection` | ||
|
|
@@ -357,8 +373,8 @@ export function providerFetch( | |
| else if (!notified) { options.onPhysicalDispatch!(); notified = true; } | ||
| } } : {}), | ||
| }; | ||
| return options.dispatchOverride | ||
| ? options.dispatchOverride(input, dispatchInit, dispatch, reacquirePacing) | ||
| return override | ||
| ? override(input, dispatchInit, dispatch, reacquirePacing) | ||
| : dispatch(input, dispatchInit); | ||
| }, | ||
| { preconnect }, | ||
|
|
@@ -397,9 +413,15 @@ export function providerFetch( | |
| options.onPhysicalDispatch?.(); | ||
| }; | ||
| const fallback = ((url: Parameters<typeof globalThis.fetch>[0], fallbackInit?: RequestInit) => | ||
| httpFetch(url, { ...fallbackInit, [WS_FALLBACK_RECEIPT]: notifyOnce } as RequestInit, reacquirePacing)) as typeof globalThis.fetch; | ||
| return codexWsUpstreamFetch(input, init, fallback, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl, | ||
| async () => { (await waitForPacing(init.signal ?? undefined))?.release(); }, notifyOnce, options.preOutputRefusalHold); | ||
| httpFetch(url, { ...fallbackInit, [WS_FALLBACK_RECEIPT]: notifyOnce } as RequestInit, reacquirePacing, null)) as typeof globalThis.fetch; | ||
| const wsSend = codexWsExecutor((url: Parameters<typeof globalThis.fetch>[0], wsInit?: RequestInit) => codexWsUpstreamFetch(String(url), wsInit ?? init, | ||
| fallback, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl, | ||
| async () => { (await waitForPacing(init.signal ?? undefined))?.release(); }, notifyOnce, options.preOutputRefusalHold), | ||
| { provider, http: markEgressTransparentExecutor(fallback) }); | ||
| // A WS frame is a physical send like any other, so it runs the same override: that is where a | ||
| // key-auth send is committed to its attempt row and the send budget is rebound to the | ||
| // credential that left. Without it a key-auth WS turn logged `sendCount: 0`. | ||
| return options.dispatchOverride ? options.dispatchOverride(input, init, wsSend, reacquirePacing) : wsSend(input, init); | ||
|
Comment on lines
+421
to
+424
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When WebSocket reuse is enabled or the socket handshake is delayed, this invokes Useful? React with 👍 / 👎. |
||
| } | ||
| return httpFetch(input, init, reacquirePacing); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { isNonReplayableResponse } from "../../lib/upstream-retry"; | ||
| import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover"; | ||
| import type { RouteResult } from "../../router"; | ||
| import type { OcxConfig, OcxParsedRequest } from "../../types"; | ||
| import { bindRouteReasoningReplayScope } from "./core-replay"; | ||
|
|
||
| // Passthrough's credential moves share one import site so passthrough-dispatch.ts stays under its line cap. | ||
| export { recoverAutoPausedAccount } from "./account-auto-pause-recovery"; | ||
|
|
||
| /** | ||
| * Key-pool failover for a native passthrough 429 before any output. This wire returns before the | ||
| * adapter loop's key-429 arm, so without it a pooled key's 429 reached the client while the same | ||
| * refusal rotates on the adapter and chat-native wires. Distributed (round-robin) selection already | ||
| * cools the key on the send and deliberately answers without a replay. A synthesized replay refusal | ||
| * is not a provider rate limit and must not cool a key. A spent send budget is checked before any | ||
| * key is cooled, so it keeps the real 429 body. Returns undefined when nothing rotated. | ||
| */ | ||
| export async function rotatePassthroughKeyOn429<T>(options: { | ||
| config: OcxConfig; | ||
| route: Pick<RouteResult, "provider" | "providerName">; | ||
| parsed: OcxParsedRequest; | ||
| response: Response; | ||
| adapterName: string; | ||
| budgetSpent: () => boolean; | ||
| refetch: () => Promise<T>; | ||
| }): Promise<T | undefined> { | ||
| const { config, route, parsed, response, adapterName, refetch } = options; | ||
| if (response.status !== 429 || isNonReplayableResponse(response) || !hasKeyPoolFailover(route.provider) | ||
| || route.provider._apiKeyAttempt?.distributed || options.budgetSpent()) return; | ||
| const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, { | ||
| retryAfter: response.headers.get("retry-after"), | ||
| attemptedKey: route.provider.apiKey, | ||
| promptCacheKey: parsed.options.promptCacheKey, | ||
| }); | ||
| if (!rotated) return; | ||
| try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ } | ||
| route.provider = rotated; | ||
| bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: rotated, adapterName }); | ||
| return refetch(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The newly classified Kiro profile refusal is an HTTP 400, but both the main and continuation Kiro rotation paths still label its replay as
oauth-account-429. Because the failed attempt normally has no status set yet, this fallback maps that recovery kind to 429 and persists the first account's real 400 as a rate-limit response, corrupting per-attempt status and error accounting whenever the new profile rotation succeeds. Carry the actual refusal status or add a distinct 400 recovery kind before sealing the attempt.Useful? React with 👍 / 👎.