@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
2
|
+
import {
|
|
3
|
+
markResponseNonReplayable,
|
|
4
|
+
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
5
|
+
UPSTREAM_NO_RESPONSE_CODE,
|
|
6
|
+
} from "../../lib/upstream-retry";
|
|
2
7
|
// If the 101 never arrives (network black hole), give SSE a chance well before
|
|
3
8
|
// the caller's connect timeout (default 200s) would fire.
|
|
4
9
|
export const UPGRADE_DEADLINE_MS = 10_000;
|
|
10
|
+
// Liveness while the exchange waits for its first response event. The proxy cannot know
|
|
11
|
+
// how long the origin legitimately needs before `response.created` (a multi-image replay
|
|
12
|
+
// spends that time in prefill; #4083 measured 30 s), and it is not the party that owns
|
|
13
|
+
// the slowness policy — the client holds its own deadline and the operator holds
|
|
14
|
+
// `connectTimeoutMs`. What the proxy can decide is whether the peer is still there, and
|
|
15
|
+
// WebSocket has a native answer: ping. While waiting, the exchange pings every
|
|
16
|
+
// CODEX_WS_LIVENESS_PING_INTERVAL_MS on sockets that expose `ping()`; any inbound frame
|
|
17
|
+
// or pong resets the silence clock, and only CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS of
|
|
18
|
+
// nothing at all settles the exchange as an origin-silence 504. A peer that never pongs
|
|
19
|
+
// keeps exactly the previous 90 s bound; a peer that does can never trip it while alive.
|
|
20
|
+
export const CODEX_WS_LIVENESS_PING_INTERVAL_MS = 15_000;
|
|
5
21
|
export const CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS = 90_000;
|
|
6
22
|
// Keep the push-based WS transport inside the same memory envelope as the
|
|
7
23
|
// bounded SSE relays that consume this response. Unlike fetch response bodies,
|
|
@@ -48,6 +64,35 @@ export function markCodexWsResponse(response: Response, observed: boolean): void
|
|
|
48
64
|
if (observed) quotaObservedResponses.add(response);
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The honest settlement for an exchange that sent its create frame and never saw a
|
|
69
|
+
* response event.
|
|
70
|
+
*
|
|
71
|
+
* Before this existed the relay committed a 200 SSE Response and errored its body, on the
|
|
72
|
+
* reasoning that a 5xx could make the pre-stream retry wrapper resend the frame. That
|
|
73
|
+
* reasoning was right about the resend and wrong about the status: it turned "no response"
|
|
74
|
+
* into "a response that failed", removed the code a user agent uses for its own retry
|
|
75
|
+
* policy, and neutered the client's first-byte timeout with chunked headers. The client
|
|
76
|
+
* on the direct path receives a gateway status in this situation and retries under its own
|
|
77
|
+
* policy; this response restores that equivalence. The resend is forbidden by the
|
|
78
|
+
* non-replayable marker instead (see upstream-retry.ts), and the structured code lets the
|
|
79
|
+
* combo failover reach the same verdict after it re-parses the body.
|
|
80
|
+
*
|
|
81
|
+
* 504 is the origin's silence (nothing at all, or nothing but liveness, for the tolerated
|
|
82
|
+
* window); 502 is a transport that closed or misbehaved after the send. Both carry the
|
|
83
|
+
* content-free stage detail in the message and the metadata snapshot in the headers, the
|
|
84
|
+
* same way a refused create does, so quota captured during the prelude is not lost.
|
|
85
|
+
*/
|
|
86
|
+
export function codexWsPreResponseFailure(status: 502 | 504, message: string, prelude: Headers): Response {
|
|
87
|
+
const headers = new Headers(prelude);
|
|
88
|
+
headers.set("content-type", "application/json");
|
|
89
|
+
headers.set("cache-control", "no-store");
|
|
90
|
+
const code = status === 504 ? UPSTREAM_NO_RESPONSE_CODE : UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE;
|
|
91
|
+
const response = new Response(JSON.stringify({ error: { type: "upstream_error", code, message } }), { status, headers });
|
|
92
|
+
markResponseNonReplayable(response);
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
const CLOSED_BEFORE_TERMINAL = "codex websocket closed before a Responses terminal event";
|
|
52
97
|
|
|
53
98
|
/**
|
|
@@ -78,6 +123,10 @@ export type CodexWsFailureStage = {
|
|
|
78
123
|
firstFrameMs: number | null;
|
|
79
124
|
/** Milliseconds from send to this failure; null when the failure predates the send. */
|
|
80
125
|
elapsedMs: number | null;
|
|
126
|
+
/** Liveness pings the exchange sent while waiting for the first response event. */
|
|
127
|
+
pings: number;
|
|
128
|
+
/** Pongs the peer answered with; zero on a peer that never answers pings. */
|
|
129
|
+
pongs: number;
|
|
81
130
|
};
|
|
82
131
|
|
|
83
132
|
/**
|
|
@@ -113,7 +162,8 @@ export function codexWsFailureDetail(stage: CodexWsFailureStage): string {
|
|
|
113
162
|
return ` [cause=${classifyCodexWsFailure(stage)} request=${stage.requestBytes}B`
|
|
114
163
|
+ ` sent=${stage.sent ? "yes" : "no"} frames=${stage.upstreamFrames}`
|
|
115
164
|
+ ` control=${stage.controlFrames} relayed=${stage.relayedEvents}`
|
|
116
|
-
+ ` first-frame=${duration(stage.firstFrameMs)} elapsed=${duration(stage.elapsedMs)}
|
|
165
|
+
+ ` first-frame=${duration(stage.firstFrameMs)} elapsed=${duration(stage.elapsedMs)}`
|
|
166
|
+
+ ` pings=${stage.pings} pongs=${stage.pongs}]`;
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
export type ResponsesWsRelayEvent = {
|
|
@@ -111,7 +111,7 @@ import {
|
|
|
111
111
|
UnsupportedContentEncodingError,
|
|
112
112
|
} from "../request-decompress";
|
|
113
113
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
114
|
-
import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
|
|
114
|
+
import { hasKeyPoolFailover, rotateProviderTransportOn429, selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
115
115
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
116
116
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
117
117
|
import type { WsData } from "../ws-bridge";
|
|
@@ -743,6 +743,13 @@ export async function handleResponsesCompact(
|
|
|
743
743
|
? CODEX_FORWARD_BASE_URL
|
|
744
744
|
: (compactProvider.baseUrl ?? "").replace(/\/+$/, "");
|
|
745
745
|
if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
|
|
746
|
+
// Native compact never enters handleResponses, so it needs its own pre-dispatch key
|
|
747
|
+
// pick. Kept inside this branch on purpose: the overlay above owns the forward and
|
|
748
|
+
// codexAccountMode cases, and the picker returns null for them anyway.
|
|
749
|
+
// Transport variant, for the same reason as the Responses core: the persisted row has no
|
|
750
|
+
// registry backfills, and compactProvider is read for its transport fields below.
|
|
751
|
+
const warmKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, compactProvider);
|
|
752
|
+
if (warmKeyProvider?.apiKey) compactProvider = warmKeyProvider;
|
|
746
753
|
headers.set("authorization", `Bearer ${resolveProviderApiKey(compactProvider.apiKey)}`);
|
|
747
754
|
}
|
|
748
755
|
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
+
import { recordContextSessionOwner } from "../../codex/context-owner";
|
|
3
|
+
import { contextRelayActivated } from "../../codex/context-compat";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
4
6
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
@@ -145,6 +147,7 @@ import {
|
|
|
145
147
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
146
148
|
isGenericFailoverProvider,
|
|
147
149
|
isGenericOAuthFailoverEnabled,
|
|
150
|
+
noteGenericPoolSelection,
|
|
148
151
|
preferredInitialAccount,
|
|
149
152
|
rotateGenericOAuthAccountOn429,
|
|
150
153
|
} from "../../oauth/generic-account-failover";
|
|
@@ -210,6 +213,8 @@ import {
|
|
|
210
213
|
applyUpstreamRecoveryInit,
|
|
211
214
|
fetchWithResetRetry,
|
|
212
215
|
fetchWithTransientRetry,
|
|
216
|
+
isNonReplayableResponse,
|
|
217
|
+
isTransientUpstreamStatus,
|
|
213
218
|
prepareSameTarget429Wait,
|
|
214
219
|
} from "../../lib/upstream-retry";
|
|
215
220
|
import {
|
|
@@ -218,6 +223,7 @@ import {
|
|
|
218
223
|
isProxyAdmissionSecret,
|
|
219
224
|
validateForwardAdmissionCredential,
|
|
220
225
|
} from "../auth-cors";
|
|
226
|
+
import { resolveContextPrincipal } from "../auth-cors";
|
|
221
227
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
222
228
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
223
229
|
import { captureExplicitOpenAiCallerAuth, listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ExplicitOpenAiCallerAuth, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
@@ -265,6 +271,7 @@ import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
|
|
|
265
271
|
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../../providers/api-key-selection";
|
|
266
272
|
import {
|
|
267
273
|
hasKeyPoolFailover,
|
|
274
|
+
selectProactiveApiKeyTransport,
|
|
268
275
|
rateLimitRetryDelayMs,
|
|
269
276
|
rateLimitRetryPolicyFor,
|
|
270
277
|
rotateProviderTransportOn429,
|
|
@@ -290,6 +297,7 @@ import {
|
|
|
290
297
|
upstreamErrorMessageFromPayload,
|
|
291
298
|
} from "../../lib/errors";
|
|
292
299
|
import type { AdmissionLease } from "../../lib/admission";
|
|
300
|
+
import { tryClaimNativeMainProfileForTurn as tryClaimStoredSidecarMainProfile } from "../../codex/native-main-admission";
|
|
293
301
|
import { prepareEffortNormalization, supportedLadderFor } from "../effort-policy";
|
|
294
302
|
import { isThreadSpawnRequest } from "../effort-policy";
|
|
295
303
|
import {
|
|
@@ -410,7 +418,7 @@ import {
|
|
|
410
418
|
payloadRewriteAsBlockRewrite,
|
|
411
419
|
relaySseWithBlockRewrite,
|
|
412
420
|
} from "../sse-payload-rewrite";
|
|
413
|
-
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
421
|
+
import { hasUnmappedRoutedCustomToolOutput, restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
414
422
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
415
423
|
import { collectFunctionCallRepairSchemas, repairFunctionCallsInJson } from "../../responses/function-call-compat";
|
|
416
424
|
import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
|
|
@@ -424,10 +432,13 @@ import {
|
|
|
424
432
|
type RoutedNamespaceToolAliases,
|
|
425
433
|
} from "../../responses/namespace-tool-compat";
|
|
426
434
|
import {
|
|
435
|
+
collectDeclaredBareWireToolNames,
|
|
427
436
|
collectDeclaredNamelessClientCallTypes,
|
|
428
437
|
collectDeclaredWireToolNames,
|
|
429
438
|
collectProviderExecutedCallTypes,
|
|
430
439
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
440
|
+
normalizeDefaultNamespaceInJson,
|
|
441
|
+
normalizeDefaultNamespaceInResponse,
|
|
431
442
|
currentTurnWireToolCatalogBody,
|
|
432
443
|
hasExplicitWireToolCatalog,
|
|
433
444
|
undeclaredToolCallMessage,
|
|
@@ -827,7 +838,8 @@ async function opaqueBlobRejectionBodyForRecovery(
|
|
|
827
838
|
signal: AbortSignal,
|
|
828
839
|
): Promise<string | undefined> {
|
|
829
840
|
if (
|
|
830
|
-
response
|
|
841
|
+
isNonReplayableResponse(response)
|
|
842
|
+
|| response.status < 400
|
|
831
843
|
|| (response.status >= 500 && response.status !== 502)
|
|
832
844
|
|| adapterName !== "openai-responses"
|
|
833
845
|
|| alreadyAttempted
|
|
@@ -1121,6 +1133,9 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
1121
1133
|
response: Response,
|
|
1122
1134
|
signal?: AbortSignal,
|
|
1123
1135
|
): Promise<boolean> {
|
|
1136
|
+
// A post-send WebSocket gateway status must not become a second account's send; the
|
|
1137
|
+
// body carries no quota evidence either, but the marker is the contract, not the prose.
|
|
1138
|
+
if (isNonReplayableResponse(response)) return false;
|
|
1124
1139
|
if (response.status === 402 || response.status === 429) return true;
|
|
1125
1140
|
if (response.status < 500 || response.status >= 600) return false;
|
|
1126
1141
|
try {
|
|
@@ -1136,6 +1151,28 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
1136
1151
|
}
|
|
1137
1152
|
}
|
|
1138
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* A pre-stream upstream 5xx another Codex account may still be able to serve.
|
|
1156
|
+
*
|
|
1157
|
+
* `server_is_overloaded` is the shape this exists for. The ChatGPT backend refuses in a few
|
|
1158
|
+
* hundred milliseconds, the body carries no quota evidence, and nothing in that exchange is
|
|
1159
|
+
* account health — so the pool keeps choosing the same account and every request fails on it
|
|
1160
|
+
* while the other accounts sit idle. That is what an operator sees as the pool refusing to move.
|
|
1161
|
+
*
|
|
1162
|
+
* The status stays exactly as upstream sent it. `classifyCodexUpstreamOutcome` maps 5xx to the
|
|
1163
|
+
* transient class, so the account earns an ordinary failure streak and `upstreamFailoverThreshold`
|
|
1164
|
+
* decides when it is soft-avoided, rather than a quota cooldown it never earned.
|
|
1165
|
+
*
|
|
1166
|
+
* Deliberately narrow. {@link isNonReplayableResponse} still refuses: a post-send WebSocket
|
|
1167
|
+
* gateway status means the body already reached the origin, so sending it from a second account
|
|
1168
|
+
* could duplicate a turn the origin may still be running. A 5xx whose body confirms quota is not
|
|
1169
|
+
* routed here either — {@link shouldRetryCodexPoolAccountQuota} classifies that one first and
|
|
1170
|
+
* carries the cooldown with it.
|
|
1171
|
+
*/
|
|
1172
|
+
export function shouldRetryCodexPoolAccountTransient(response: Response): boolean {
|
|
1173
|
+
return !isNonReplayableResponse(response) && isTransientUpstreamStatus(response.status);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1139
1176
|
interface CodexPoolAccountRetryArgs {
|
|
1140
1177
|
/** Sanitized caller input, before any selected Pool credential was materialized. */
|
|
1141
1178
|
callerAuthHeaders: Headers;
|
|
@@ -1310,6 +1347,21 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1310
1347
|
const inboundWire = options.inboundWire ?? "responses";
|
|
1311
1348
|
const entitlementResolver = options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements;
|
|
1312
1349
|
let retryAuthCtx: CodexAuthContext | undefined;
|
|
1350
|
+
// A transient 5xx must record even when this request cannot move: the ordinary terminal
|
|
1351
|
+
// recorder only fires for an OK event-stream body, so a pre-stream refusal would otherwise
|
|
1352
|
+
// leave the account looking healthy no matter how many times it refused, and the pool would
|
|
1353
|
+
// keep handing it the next request.
|
|
1354
|
+
const recordUnmovedTransientOutcome = (): void => {
|
|
1355
|
+
if (!isTransientUpstreamStatus(outcomeStatus)) return;
|
|
1356
|
+
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
1357
|
+
threadId: firstAuthCtx.affinityKey,
|
|
1358
|
+
fixedAccount: firstAuthCtx.fixedAccount,
|
|
1359
|
+
modelId: route.modelId,
|
|
1360
|
+
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
1361
|
+
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
1362
|
+
writerGeneration: firstAuthCtx.writerGeneration,
|
|
1363
|
+
});
|
|
1364
|
+
};
|
|
1313
1365
|
if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
|
|
1314
1366
|
invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
|
|
1315
1367
|
let refreshed;
|
|
@@ -1334,6 +1386,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1334
1386
|
// Exact account selectors may retry the same confirmed account above, but must never resolve
|
|
1335
1387
|
// an alternate. Quota failures and a refreshed entitlement miss remain terminal.
|
|
1336
1388
|
if (!retryAuthCtx && (firstAuthCtx.fixedAccount || args.sameAccountOnly === true)) {
|
|
1389
|
+
recordUnmovedTransientOutcome();
|
|
1337
1390
|
return { kind: "no-alternate" };
|
|
1338
1391
|
}
|
|
1339
1392
|
try {
|
|
@@ -1384,6 +1437,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1384
1437
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
1385
1438
|
});
|
|
1386
1439
|
}
|
|
1440
|
+
recordUnmovedTransientOutcome();
|
|
1387
1441
|
return { kind: "no-alternate" };
|
|
1388
1442
|
}
|
|
1389
1443
|
|
|
@@ -1426,6 +1480,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1426
1480
|
const retryAdapter = resolveAdapter(
|
|
1427
1481
|
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
|
|
1428
1482
|
config.cacheRetention,
|
|
1483
|
+
route.providerName,
|
|
1429
1484
|
);
|
|
1430
1485
|
bindRouteReasoningReplayScope({
|
|
1431
1486
|
parsed,
|
|
@@ -1670,6 +1725,10 @@ export interface ConsumedComboFailure {
|
|
|
1670
1725
|
|
|
1671
1726
|
|
|
1672
1727
|
export interface HandleResponsesOptions {
|
|
1728
|
+
/** Internal Claude replay identity; consumed only by the final canonical Go transport. */
|
|
1729
|
+
claudeGoAffinity?: { sessionLane?: string };
|
|
1730
|
+
/** Validated Claude metadata identity; projected only into final canonical attempt headers. */
|
|
1731
|
+
claudeNativeSessionId?: string;
|
|
1673
1732
|
/** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
|
|
1674
1733
|
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
1675
1734
|
turnAdmissionLease?: AdmissionLease;
|
|
@@ -1728,6 +1787,8 @@ export interface HandleResponsesOptions {
|
|
|
1728
1787
|
trustedClaudeMainAuth?: { authorization: string; chatgptAccountId?: string };
|
|
1729
1788
|
/** Sidecar-only auth captured before route changes; null means no usable original pair. */
|
|
1730
1789
|
openAiSidecarAuth?: ExplicitOpenAiCallerAuth | null;
|
|
1790
|
+
/** Internal Chat bridge permission to obtain claimed stored auth only for a final Direct sidecar. */
|
|
1791
|
+
allowStoredOpenAiSidecarAuth?: boolean;
|
|
1731
1792
|
/** Original caller-owned native pair; separate from any claimed sidecar enrichment. */
|
|
1732
1793
|
nativeCallerAuth?: ExplicitOpenAiCallerAuth | null;
|
|
1733
1794
|
/** Caller Direct credential under Direct\'s own predicate; restored only for the canonical OpenAI final route. */
|
|
@@ -2037,6 +2098,15 @@ function canPassThroughEncryptedV2AgentTask(
|
|
|
2037
2098
|
).adapter === "openai-responses";
|
|
2038
2099
|
}
|
|
2039
2100
|
|
|
2101
|
+
/** Keep synthesized Claude identity out of request headers reused by policy/combo fallback. */
|
|
2102
|
+
function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
2103
|
+
if (!sessionId || !isCanonicalOpenAiForwardProvider(provider)
|
|
2104
|
+
|| headers.has("session_id") || headers.has("session-id") || headers.has("thread-id")) return headers;
|
|
2105
|
+
const forwarded = new Headers(headers);
|
|
2106
|
+
forwarded.set("session_id", sessionId);
|
|
2107
|
+
return forwarded;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2040
2110
|
type ResponsesAuthResolution =
|
|
2041
2111
|
| { ok: true; authCtx: CodexAuthContext; headers: Headers; callerAuthHeaders: Headers; substituteMainCredential: boolean }
|
|
2042
2112
|
| { ok: false; response: Response };
|
|
@@ -2471,6 +2541,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2471
2541
|
logCtx: RequestLogContext;
|
|
2472
2542
|
inboundWire: InboundWire;
|
|
2473
2543
|
inboundTransport?: "websocket";
|
|
2544
|
+
claudeGoAffinity?: HandleResponsesOptions["claudeGoAffinity"];
|
|
2474
2545
|
}): Promise<void> {
|
|
2475
2546
|
const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
|
|
2476
2547
|
const effortSelector = prepareEffortNormalization(parsed, route);
|
|
@@ -2498,7 +2569,8 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2498
2569
|
|
|
2499
2570
|
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
2500
2571
|
// this request will actually use (#404).
|
|
2501
|
-
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
2572
|
+
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
2573
|
+
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req));
|
|
2502
2574
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2503
2575
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
2504
2576
|
logCtx.model = route.modelId;
|
|
@@ -3891,6 +3963,22 @@ async function handleResponsesInner(
|
|
|
3891
3963
|
);
|
|
3892
3964
|
}
|
|
3893
3965
|
|
|
3966
|
+
if (hasUnexpandedPreviousResponse) {
|
|
3967
|
+
const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
3968
|
+
// Stateless destinations cannot resolve the omitted prefix. Stateful destinations may,
|
|
3969
|
+
// but a lowered custom result still needs its call to recover the original wire type.
|
|
3970
|
+
// Native function/custom continuations without lowering keep their upstream-owned state.
|
|
3971
|
+
if (continuationProvider.adapter === "openai-responses"
|
|
3972
|
+
&& (continuationProvider.statelessResponses === true
|
|
3973
|
+
|| hasUnmappedRoutedCustomToolOutput(parsed._rawBody, continuationProvider.supportsResponsesCustomTools))) {
|
|
3974
|
+
return formatErrorResponse(
|
|
3975
|
+
400,
|
|
3976
|
+
"previous_response_not_found",
|
|
3977
|
+
"Routed continuation requires unavailable local history; resend the full conversation without previous_response_id.",
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3894
3982
|
// Captured before normalization: whether the CLIENT asked for SSE. The
|
|
3895
3983
|
// transport-neutral upstream-streaming policy below may force a bounded JSON
|
|
3896
3984
|
// upstream for reliability (#875); the answer must then be reframed to SSE
|
|
@@ -3904,6 +3992,7 @@ async function handleResponsesInner(
|
|
|
3904
3992
|
logCtx,
|
|
3905
3993
|
inboundWire,
|
|
3906
3994
|
inboundTransport: options.inboundTransport,
|
|
3995
|
+
claudeGoAffinity: options.claudeGoAffinity,
|
|
3907
3996
|
});
|
|
3908
3997
|
// Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
|
|
3909
3998
|
// the normal post-resolution provider label is assigned.
|
|
@@ -3965,8 +4054,8 @@ async function handleResponsesInner(
|
|
|
3965
4054
|
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options, credentialDomainWasRewritten);
|
|
3966
4055
|
if (!finalAuth.ok) return finalAuth.response;
|
|
3967
4056
|
authCtx = finalAuth.authCtx;
|
|
3968
|
-
selectedForwardHeaders = finalAuth.headers;
|
|
3969
|
-
callerAuthHeaders = finalAuth.callerAuthHeaders;
|
|
4057
|
+
selectedForwardHeaders = withClaudeNativeSession(finalAuth.headers, route.provider, options.claudeNativeSessionId);
|
|
4058
|
+
callerAuthHeaders = withClaudeNativeSession(finalAuth.callerAuthHeaders, route.provider, options.claudeNativeSessionId);
|
|
3970
4059
|
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
3971
4060
|
}
|
|
3972
4061
|
|
|
@@ -4158,7 +4247,7 @@ async function handleResponsesInner(
|
|
|
4158
4247
|
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
4159
4248
|
};
|
|
4160
4249
|
const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
|
|
4161
|
-
const resolved = resolveAdapter(provider, retention);
|
|
4250
|
+
const resolved = resolveAdapter(provider, retention, route.providerName);
|
|
4162
4251
|
if (route.provider.authMode === "forward") return resolved;
|
|
4163
4252
|
const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
|
|
4164
4253
|
? oauthSelection && servingOAuthSnapshot
|
|
@@ -4397,6 +4486,10 @@ async function handleResponsesInner(
|
|
|
4397
4486
|
// whichever account is active by the time the response comes back (#2568).
|
|
4398
4487
|
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
4399
4488
|
genericFailoverAccountId = resolved.accountId;
|
|
4489
|
+
// Advance the pool cursor only now that this account is actually admitted. The
|
|
4490
|
+
// helper returns immediately unless the kernel is on AND the strategy is
|
|
4491
|
+
// round-robin, so quota and fill-first pools reach it without being touched.
|
|
4492
|
+
noteGenericPoolSelection(config, route.providerName, resolved.accountId);
|
|
4400
4493
|
}
|
|
4401
4494
|
// Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
|
|
4402
4495
|
// a fail-closed local-cli credential rule -- so without this stamp its identity is
|
|
@@ -4434,6 +4527,26 @@ async function handleResponsesInner(
|
|
|
4434
4527
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
4435
4528
|
}
|
|
4436
4529
|
}
|
|
4530
|
+
// Key-auth twin of the OAuth preference above: pick a warm key BEFORE the first attempt when
|
|
4531
|
+
// the committed one is already cooling, instead of spending the request earning a 429 the
|
|
4532
|
+
// runtime could already predict. The picker refuses to override a healthy committed key and
|
|
4533
|
+
// returns null without a configured strategy, so an ordinary install evaluates one predicate.
|
|
4534
|
+
//
|
|
4535
|
+
// It RETURNS a rebuilt route rather than mutating one, and the assignment has to land here --
|
|
4536
|
+
// ahead of the transport pin below, the adapterProvider copy that follows it, and the request
|
|
4537
|
+
// the HTTP path bakes later. The image bridge and web search read route.provider directly and
|
|
4538
|
+
// have no stale-selection re-read to save them, so ordering is the whole correctness argument.
|
|
4539
|
+
//
|
|
4540
|
+
// The Transport variant, not the bare picker: the picker answers with the PERSISTED row, and
|
|
4541
|
+
// a built-in provider stored in its valid minimal form would lose the adapter id, base URL
|
|
4542
|
+
// and static headers registry backfill supplies, throwing `Unknown adapter: undefined`.
|
|
4543
|
+
const proactiveKeyProvider = selectProactiveApiKeyTransport(
|
|
4544
|
+
config,
|
|
4545
|
+
route.providerName,
|
|
4546
|
+
route.provider,
|
|
4547
|
+
parsed.options.promptCacheKey,
|
|
4548
|
+
);
|
|
4549
|
+
if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
|
|
4437
4550
|
route.provider = resolveProviderTransport(
|
|
4438
4551
|
route.providerName,
|
|
4439
4552
|
route.provider,
|
|
@@ -4530,21 +4643,42 @@ async function handleResponsesInner(
|
|
|
4530
4643
|
}
|
|
4531
4644
|
|
|
4532
4645
|
let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
|
|
4533
|
-
const
|
|
4534
|
-
const
|
|
4646
|
+
const visionDescribeTerminal = options.visionDescribeTerminal === true;
|
|
4647
|
+
const routedCompaction = parsed._compactionRequest === true
|
|
4648
|
+
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
4649
|
+
const needsOpenAiVision = !visionDescribeTerminal
|
|
4650
|
+
&& shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
|
|
4651
|
+
const needsOpenAiSearch = !routedCompaction && !adapter.runTurn
|
|
4652
|
+
&& shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
|
|
4535
4653
|
if (needsOpenAiVision || needsOpenAiSearch) {
|
|
4536
4654
|
try {
|
|
4655
|
+
const candidates = listOpenAiForwardSidecarCandidates(config);
|
|
4656
|
+
let sidecarAuth = options.openAiSidecarAuth;
|
|
4657
|
+
if (!sidecarAuth && options.allowStoredOpenAiSidecarAuth === true
|
|
4658
|
+
&& route.codexAccountId === undefined
|
|
4659
|
+
&& candidates.some(candidate => candidate.accountMode === "direct")
|
|
4660
|
+
&& tryClaimStoredSidecarMainProfile(options.turnAdmissionLease)) {
|
|
4661
|
+
// Request-local helper authority only: never promote this pair to caller, primary,
|
|
4662
|
+
// or retry credentials. Claim before reading so profile switches remain fenced.
|
|
4663
|
+
try {
|
|
4664
|
+
const { getMainAccountToken } = await import("../../codex/main-account");
|
|
4665
|
+
const token = getMainAccountToken();
|
|
4666
|
+
if (token) sidecarAuth = captureExplicitOpenAiCallerAuth(new Headers({
|
|
4667
|
+
authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId,
|
|
4668
|
+
}), config);
|
|
4669
|
+
} catch { /* stored enrichment is optional */ }
|
|
4670
|
+
}
|
|
4537
4671
|
// Preserve explicit OpenAI helper auth across route changes without returning it to
|
|
4538
4672
|
// primary-provider headers or alternate-main retry. The resolver revalidates scope.
|
|
4539
4673
|
const sidecarHeaders = new Headers(req.headers);
|
|
4540
4674
|
sidecarHeaders.delete("authorization");
|
|
4541
4675
|
sidecarHeaders.delete("chatgpt-account-id");
|
|
4542
|
-
if (
|
|
4543
|
-
sidecarHeaders.set("authorization",
|
|
4544
|
-
sidecarHeaders.set("chatgpt-account-id",
|
|
4676
|
+
if (sidecarAuth) {
|
|
4677
|
+
sidecarHeaders.set("authorization", sidecarAuth.authorization);
|
|
4678
|
+
sidecarHeaders.set("chatgpt-account-id", sidecarAuth.chatgptAccountId);
|
|
4545
4679
|
}
|
|
4546
4680
|
openAiSidecar = await resolveFirstUsableOpenAiSidecar(
|
|
4547
|
-
|
|
4681
|
+
candidates,
|
|
4548
4682
|
sidecarHeaders,
|
|
4549
4683
|
config,
|
|
4550
4684
|
{
|
|
@@ -4579,7 +4713,6 @@ async function handleResponsesInner(
|
|
|
4579
4713
|
// call must never plan another describe. The flag arrives from the Chat
|
|
4580
4714
|
// surface (whose bridge rebuilds headers) or as the raw header for native
|
|
4581
4715
|
// Responses callers. Marked + text-only routed model → strip, depth cap 1.
|
|
4582
|
-
const visionDescribeTerminal = options.visionDescribeTerminal === true;
|
|
4583
4716
|
const visionPlan = visionDescribeTerminal
|
|
4584
4717
|
? undefined
|
|
4585
4718
|
: planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, {
|
|
@@ -4641,10 +4774,16 @@ async function handleResponsesInner(
|
|
|
4641
4774
|
// `compaction_trigger` item — only the canonical ChatGPT backend speaks that
|
|
4642
4775
|
// contract. An API-key gateway would receive the trigger, answer with an ordinary
|
|
4643
4776
|
// message, and leave Codex fataling on a missing compaction item (#422).
|
|
4644
|
-
const
|
|
4645
|
-
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
4646
|
-
const commitReasoningReplayServingRoute = (): void => {
|
|
4777
|
+
const commitReasoningReplayServingRoute = (outboundHeaders?: HeadersInit): void => {
|
|
4647
4778
|
commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
|
|
4779
|
+
// History has no model namespace. Record the account that actually accepted this
|
|
4780
|
+
// final attempt, after refresh/failover, rather than guessing from mutable affinity.
|
|
4781
|
+
// Recording is relay state. With the feature off there is no relay, so building an owner
|
|
4782
|
+
// registry for it is out of scope for this request.
|
|
4783
|
+
if (outboundHeaders && isCanonicalOpenAiForwardProvider(route.provider) && contextRelayActivated()) {
|
|
4784
|
+
recordContextSessionOwner(resolveContextPrincipal(req, config, options.admission), req.headers,
|
|
4785
|
+
route.provider.baseUrl, authCtx, new Headers(outboundHeaders), substituteMainCredential);
|
|
4786
|
+
}
|
|
4648
4787
|
};
|
|
4649
4788
|
if (routedCompaction) {
|
|
4650
4789
|
delete parsed.context.tools;
|
|
@@ -4714,6 +4853,7 @@ async function handleResponsesInner(
|
|
|
4714
4853
|
);
|
|
4715
4854
|
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
4716
4855
|
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
4856
|
+
const clientDeclaredBareWireToolNames = collectDeclaredBareWireToolNames(clientToolAuthorizationBody);
|
|
4717
4857
|
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
4718
4858
|
clientToolAuthorizationBody,
|
|
4719
4859
|
);
|
|
@@ -4789,6 +4929,7 @@ async function handleResponsesInner(
|
|
|
4789
4929
|
};
|
|
4790
4930
|
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
4791
4931
|
const declaredWireToolNames = new Set<string>();
|
|
4932
|
+
const declaredBareWireToolNames = new Set<string>();
|
|
4792
4933
|
const declaredNamelessClientCallTypes = new Set<string>();
|
|
4793
4934
|
// `buildToolBridgeMaps` creates a bare alias only when the caller selected exactly one
|
|
4794
4935
|
// namespaced tool through a bare tool_choice. Restore that request-bounded identity before
|
|
@@ -4838,12 +4979,17 @@ async function handleResponsesInner(
|
|
|
4838
4979
|
// aliases are authoritative. A continuation's outbound body still contains historical
|
|
4839
4980
|
// catalogs (and may promote historical tool-search definitions), so it can never widen the
|
|
4840
4981
|
// current caller snapshot captured above.
|
|
4982
|
+
declaredBareWireToolNames.clear();
|
|
4841
4983
|
if (replayedInputPrefixLength === 0) {
|
|
4842
4984
|
for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
|
|
4843
4985
|
declaredWireToolNames.add(name);
|
|
4844
4986
|
}
|
|
4987
|
+
for (const name of collectDeclaredBareWireToolNames(outboundRequestBody)) {
|
|
4988
|
+
declaredBareWireToolNames.add(name);
|
|
4989
|
+
}
|
|
4845
4990
|
}
|
|
4846
4991
|
for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
|
|
4992
|
+
for (const name of clientDeclaredBareWireToolNames) declaredBareWireToolNames.add(name);
|
|
4847
4993
|
declaredNamelessClientCallTypes.clear();
|
|
4848
4994
|
if (replayedInputPrefixLength === 0) {
|
|
4849
4995
|
for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
|
|
@@ -4936,6 +5082,7 @@ async function handleResponsesInner(
|
|
|
4936
5082
|
declaredWireToolNames,
|
|
4937
5083
|
declaredNamelessClientCallTypes,
|
|
4938
5084
|
providerExecutedCallTypes,
|
|
5085
|
+
declaredBareWireToolNames,
|
|
4939
5086
|
) !== undefined) {
|
|
4940
5087
|
inspectionSawUndeclaredTool = true;
|
|
4941
5088
|
}
|
|
@@ -4973,11 +5120,19 @@ async function handleResponsesInner(
|
|
|
4973
5120
|
declaredWireToolNames,
|
|
4974
5121
|
declaredNamelessClientCallTypes,
|
|
4975
5122
|
providerExecutedCallTypes,
|
|
5123
|
+
declaredBareWireToolNames,
|
|
4976
5124
|
) !== undefined
|
|
4977
5125
|
) {
|
|
4978
5126
|
return;
|
|
4979
5127
|
}
|
|
4980
|
-
|
|
5128
|
+
const normalizedReplayResponse = (undeclaredToolGuardActive
|
|
5129
|
+
? normalizeDefaultNamespaceInResponse(
|
|
5130
|
+
replayResponse,
|
|
5131
|
+
declaredWireToolNames,
|
|
5132
|
+
declaredBareWireToolNames,
|
|
5133
|
+
).value
|
|
5134
|
+
: replayResponse) as typeof replayResponse;
|
|
5135
|
+
rememberPassthroughResponse?.(normalizedReplayResponse);
|
|
4981
5136
|
const firstCompletion = !inspectedCompletionSeen;
|
|
4982
5137
|
inspectedCompletionSeen = true;
|
|
4983
5138
|
if (firstCompletion && (inspectedTerminal === null || firstTerminalAllowsRecall)) {
|
|
@@ -5291,7 +5446,7 @@ async function handleResponsesInner(
|
|
|
5291
5446
|
}
|
|
5292
5447
|
authCtx = replay.authCtx;
|
|
5293
5448
|
route.provider = replay.provider;
|
|
5294
|
-
selectedForwardHeaders = replay.headers;
|
|
5449
|
+
selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
|
|
5295
5450
|
const replayAdapter = resolveSelectionAdapter(
|
|
5296
5451
|
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
|
|
5297
5452
|
config.cacheRetention,
|
|
@@ -5618,6 +5773,10 @@ async function handleResponsesInner(
|
|
|
5618
5773
|
// ChatGPT sometimes wraps quota exhaustion in a generic 5xx. Normalize only
|
|
5619
5774
|
// body-confirmed cases to quota evidence so cooldown and rotation both apply.
|
|
5620
5775
|
poolRetryOutcome = upstreamResponse.status >= 500 ? 429 : upstreamResponse.status;
|
|
5776
|
+
} else if (!authCtx.fixedAccount && shouldRetryCodexPoolAccountTransient(upstreamResponse)) {
|
|
5777
|
+
// A plain transient 5xx the same-account retry layer could not absorb. Keep the real
|
|
5778
|
+
// status so it records as transient rather than quota.
|
|
5779
|
+
poolRetryOutcome = upstreamResponse.status;
|
|
5621
5780
|
}
|
|
5622
5781
|
|
|
5623
5782
|
if (poolRetryOutcome !== undefined) {
|
|
@@ -5863,7 +6022,7 @@ async function handleResponsesInner(
|
|
|
5863
6022
|
// For streamed passthrough, a successful terminal response means non-error upstream status
|
|
5864
6023
|
// before relay starts. Waiting for SSE completion would retain request state across the whole
|
|
5865
6024
|
// stream; a later body failure does not undo that this destination accepted and served the turn.
|
|
5866
|
-
commitReasoningReplayServingRoute();
|
|
6025
|
+
commitReasoningReplayServingRoute(request.headers);
|
|
5867
6026
|
const terminalRepairPolicy = providerModelResponsesTerminalRepair(
|
|
5868
6027
|
route.providerName,
|
|
5869
6028
|
route.provider,
|
|
@@ -5998,6 +6157,7 @@ async function handleResponsesInner(
|
|
|
5998
6157
|
declaredWireToolNames,
|
|
5999
6158
|
declaredNamelessClientCallTypes,
|
|
6000
6159
|
providerExecutedCallTypes,
|
|
6160
|
+
declaredBareWireToolNames,
|
|
6001
6161
|
)
|
|
6002
6162
|
: undefined,
|
|
6003
6163
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
@@ -6198,7 +6358,7 @@ async function handleResponsesInner(
|
|
|
6198
6358
|
}
|
|
6199
6359
|
const text = bounded.text;
|
|
6200
6360
|
inspectResponseLogJson(logCtx, text);
|
|
6201
|
-
|
|
6361
|
+
let clientJson = (() => {
|
|
6202
6362
|
const restoredNamespace = restoreRoutedNamespaceCallsInJson(
|
|
6203
6363
|
scrubSelfNamedToolCallNamespaceInJson(
|
|
6204
6364
|
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
@@ -6244,6 +6404,7 @@ async function handleResponsesInner(
|
|
|
6244
6404
|
declaredWireToolNames,
|
|
6245
6405
|
declaredNamelessClientCallTypes,
|
|
6246
6406
|
providerExecutedCallTypes,
|
|
6407
|
+
declaredBareWireToolNames,
|
|
6247
6408
|
);
|
|
6248
6409
|
} catch {
|
|
6249
6410
|
return undefined;
|
|
@@ -6252,8 +6413,13 @@ async function handleResponsesInner(
|
|
|
6252
6413
|
if (undeclared !== undefined) {
|
|
6253
6414
|
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
|
|
6254
6415
|
}
|
|
6416
|
+
clientJson = normalizeDefaultNamespaceInJson(
|
|
6417
|
+
clientJson,
|
|
6418
|
+
declaredWireToolNames,
|
|
6419
|
+
declaredBareWireToolNames,
|
|
6420
|
+
);
|
|
6255
6421
|
}
|
|
6256
|
-
commitReasoningReplayServingRoute();
|
|
6422
|
+
commitReasoningReplayServingRoute(request.headers);
|
|
6257
6423
|
try {
|
|
6258
6424
|
rememberPassthroughResponseChecked(
|
|
6259
6425
|
JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
|
|
@@ -6335,7 +6501,7 @@ async function handleResponsesInner(
|
|
|
6335
6501
|
}
|
|
6336
6502
|
// An unclassified passthrough body is relayed directly and has no bounded completion observer;
|
|
6337
6503
|
// use the same non-error-status success boundary as SSE instead of retaining per-stream state.
|
|
6338
|
-
commitReasoningReplayServingRoute();
|
|
6504
|
+
commitReasoningReplayServingRoute(request.headers);
|
|
6339
6505
|
const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
6340
6506
|
const turnAc = new AbortController();
|
|
6341
6507
|
const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
|
|
@@ -20,7 +20,7 @@ import { codexWsExchange } from "./codex-ws-exchange";
|
|
|
20
20
|
import { CodexWsSession } from "./codex-ws-session";
|
|
21
21
|
import { codexWsPool, codexWsReuseIdentity } from "./codex-ws-pool";
|
|
22
22
|
import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
|
|
23
|
-
export { CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
|
|
23
|
+
export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
|
|
24
24
|
MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
|
|
25
25
|
isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./codex-ws-wire";
|
|
26
26
|
export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
|