@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +54 -24
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +47 -13
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/observe.ts +3 -1
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +474 -178
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +625 -98
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +122 -28
- package/src/server/live.ts +18 -4
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +45 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
} from "./outbound-body-guard";
|
|
14
14
|
import { nativeContextLimits } from "../../codex/catalog";
|
|
15
15
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
16
|
+
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
17
|
+
import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota";
|
|
18
|
+
import { isCodexWsQuotaObservedResponse } from "./ws-upstream";
|
|
16
19
|
import {
|
|
17
20
|
multiAgentGuidanceEnabled,
|
|
18
21
|
resolveEnvValue,
|
|
@@ -67,6 +70,7 @@ import { evidenceFromBody } from "../../routing/request-evidence";
|
|
|
67
70
|
import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
|
|
68
71
|
import {
|
|
69
72
|
advanceComboAfterFailure,
|
|
73
|
+
comboCooldownRetryAfterSeconds,
|
|
70
74
|
comboDefaultEffort,
|
|
71
75
|
comboFailureCooldownScope,
|
|
72
76
|
comboFailureDecision,
|
|
@@ -75,11 +79,11 @@ import {
|
|
|
75
79
|
concreteComboRequestBody,
|
|
76
80
|
getCombo,
|
|
77
81
|
isComboTargetInCooldown,
|
|
78
|
-
comboCooldownRetryAfterSeconds,
|
|
79
82
|
NoAvailableComboTargetsError,
|
|
80
83
|
noteComboSuccess,
|
|
81
84
|
parseRetryAfterMs,
|
|
82
85
|
pickComboTarget,
|
|
86
|
+
pickComboTargetWithWait,
|
|
83
87
|
targetKey,
|
|
84
88
|
} from "../../combos";
|
|
85
89
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
@@ -122,6 +126,7 @@ import {
|
|
|
122
126
|
getAnthropicPoolAccessToken,
|
|
123
127
|
getAnthropicPoolRetryAfterSeconds,
|
|
124
128
|
isAnthropicAccountPoolEnabled,
|
|
129
|
+
hasAnthropicFailoverQuorum,
|
|
125
130
|
promoteAnthropicActiveAccount,
|
|
126
131
|
resolveAnthropicAccountForSession,
|
|
127
132
|
rotateAnthropicAccountOn429,
|
|
@@ -143,6 +148,8 @@ import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenA
|
|
|
143
148
|
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
144
149
|
import {
|
|
145
150
|
applyCodexAuthContextToProvider,
|
|
151
|
+
createCodexReserveDispatchGuard,
|
|
152
|
+
unwrapUpstreamRetryEvidenceError,
|
|
146
153
|
codexPoolAffinityKey,
|
|
147
154
|
CodexAccountCooldownError,
|
|
148
155
|
CodexAuthContextError,
|
|
@@ -158,6 +165,7 @@ import {
|
|
|
158
165
|
releaseCodexAuthContextProbeLease,
|
|
159
166
|
stripCodexRuntimeProviderFields,
|
|
160
167
|
type CodexAuthContext,
|
|
168
|
+
type CodexAuthPolicyConfig,
|
|
161
169
|
} from "../../codex/auth-context";
|
|
162
170
|
import {
|
|
163
171
|
entitledCodexAccountIdsForModel,
|
|
@@ -201,6 +209,7 @@ import type { DataPlaneAdmission } from "../auth-cors";
|
|
|
201
209
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
202
210
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
203
211
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
212
|
+
import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../../codex/loopback-target";
|
|
204
213
|
import { providerContextCap } from "../../providers/context-cap";
|
|
205
214
|
import {
|
|
206
215
|
fastPolicyForModel,
|
|
@@ -219,6 +228,9 @@ import {
|
|
|
219
228
|
waitForProviderRequestSlot,
|
|
220
229
|
} from "../../providers/request-pacing";
|
|
221
230
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
231
|
+
import { isMuseSubscriptionUsagePayload, parseMuseSubscriptionUsage } from "../../providers/muse-subscription-usage";
|
|
232
|
+
import { hasPassiveAccountQuota, recordPassiveAccountQuota } from "../../providers/quota";
|
|
233
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
222
234
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
223
235
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
224
236
|
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
|
|
@@ -234,10 +246,12 @@ import {
|
|
|
234
246
|
rateLimitRetryDelayMs,
|
|
235
247
|
rateLimitRetryPolicyFor,
|
|
236
248
|
rotateProviderTransportOn429,
|
|
249
|
+
rotateProviderTransportOn401,
|
|
237
250
|
transientRetryPolicyFor,
|
|
238
251
|
} from "../../providers/key-failover";
|
|
239
252
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
240
253
|
import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
|
|
254
|
+
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
241
255
|
import type { WsData } from "../ws-bridge";
|
|
242
256
|
import {
|
|
243
257
|
codexAccountSelectionForTurn,
|
|
@@ -284,6 +298,7 @@ import {
|
|
|
284
298
|
conversationIdFromResponsesRequest,
|
|
285
299
|
normalizeLogConversationId,
|
|
286
300
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
301
|
+
sessionLaneIdFromRequest,
|
|
287
302
|
sessionIdHeaderFromRequest,
|
|
288
303
|
} from "../request-log-conversation";
|
|
289
304
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -329,6 +344,7 @@ import {
|
|
|
329
344
|
} from "../responses-image-gen-repair";
|
|
330
345
|
import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
|
|
331
346
|
import { parseRequestEffortRowId } from "../effort-row";
|
|
347
|
+
import { parseSyntheticRowId } from "../fast-row";
|
|
332
348
|
import {
|
|
333
349
|
collectSelfNamedNamespaceScrubAuthorization,
|
|
334
350
|
createSelfNamedToolCallNamespaceScrubRewrite,
|
|
@@ -858,6 +874,13 @@ export function usesCodexForwardPoolAuth(
|
|
|
858
874
|
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
859
875
|
}
|
|
860
876
|
|
|
877
|
+
function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined {
|
|
878
|
+
if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
879
|
+
const { accountId, writerGeneration } = authCtx;
|
|
880
|
+
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
|
|
881
|
+
return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
|
|
882
|
+
}
|
|
883
|
+
|
|
861
884
|
export function preAuthUpstreamHostCircuitKey(
|
|
862
885
|
route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
|
|
863
886
|
config: OcxConfig,
|
|
@@ -965,6 +988,9 @@ interface CodexPoolAccountRetryArgs {
|
|
|
965
988
|
parsed: OcxParsedRequest;
|
|
966
989
|
logCtx: RequestLogContext;
|
|
967
990
|
options: {
|
|
991
|
+
admission?: DataPlaneAdmission;
|
|
992
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
993
|
+
visionDescribeTerminal?: boolean;
|
|
968
994
|
abortSignal?: AbortSignal;
|
|
969
995
|
onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
|
|
970
996
|
deferCodexResetDerivedCooldown?: boolean;
|
|
@@ -1161,6 +1187,8 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1161
1187
|
"pool",
|
|
1162
1188
|
{
|
|
1163
1189
|
excludeAccountId: firstAuthCtx.accountId,
|
|
1190
|
+
admission: options.admission,
|
|
1191
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
1164
1192
|
modelId: route.modelId,
|
|
1165
1193
|
requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
|
|
1166
1194
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
@@ -1210,6 +1238,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1210
1238
|
firstAuthCtx.accountId,
|
|
1211
1239
|
firstResponse.headers,
|
|
1212
1240
|
firstAuthCtx.writerGeneration,
|
|
1241
|
+
firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
|
|
1213
1242
|
);
|
|
1214
1243
|
}
|
|
1215
1244
|
const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
|
|
@@ -1231,7 +1260,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1231
1260
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
1232
1261
|
// ordinary requests must block the first account before the alternate send.
|
|
1233
1262
|
if (!deferFirstOutcome) recordFirstOutcome();
|
|
1234
|
-
const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
|
|
1263
|
+
const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
|
|
1235
1264
|
const retryProvider = applyCodexAuthContextToProvider(
|
|
1236
1265
|
stripCodexRuntimeProviderFields(route.provider),
|
|
1237
1266
|
retryAuthCtx,
|
|
@@ -1300,6 +1329,9 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1300
1329
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1301
1330
|
providerName: route.providerName,
|
|
1302
1331
|
modelId: route.modelId,
|
|
1332
|
+
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider),
|
|
1333
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1334
|
+
? createCodexReserveDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1303
1335
|
}),
|
|
1304
1336
|
// Credential-bearing forward send: never follow a redirect into a
|
|
1305
1337
|
// dead-host rejection after the credential was seen (#914).
|
|
@@ -1469,6 +1501,8 @@ export interface ConsumedComboFailure {
|
|
|
1469
1501
|
upstreamCode?: string;
|
|
1470
1502
|
/** Valid numeric/date value used only for cooldown calculation. */
|
|
1471
1503
|
retryAfter?: string;
|
|
1504
|
+
/** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
|
|
1505
|
+
resetAt?: string[];
|
|
1472
1506
|
/** Reserved for 040 usage attribution without adding another body read. */
|
|
1473
1507
|
usage?: OcxUsage;
|
|
1474
1508
|
}
|
|
@@ -1476,6 +1510,8 @@ export interface ConsumedComboFailure {
|
|
|
1476
1510
|
|
|
1477
1511
|
|
|
1478
1512
|
export interface HandleResponsesOptions {
|
|
1513
|
+
/** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
|
|
1514
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
1479
1515
|
turnAdmissionLease?: AdmissionLease;
|
|
1480
1516
|
/**
|
|
1481
1517
|
* How the caller proved data-plane admission (#1686).
|
|
@@ -1585,9 +1621,26 @@ export async function consumeComboFailure(
|
|
|
1585
1621
|
let upstreamCode: string | undefined;
|
|
1586
1622
|
let upstreamMessage: string | undefined;
|
|
1587
1623
|
let upstreamType: string | undefined;
|
|
1624
|
+
// Whether the body itself confirms a quota/rate-limit refusal, computed on the SAME read as
|
|
1625
|
+
// the classification below. `shouldRetryCodexPoolAccountQuota` cannot be called here without
|
|
1626
|
+
// a second body read, so this mirrors its normalization: raw 402/429, or a 5xx whose intact,
|
|
1627
|
+
// display-safe body carries a recognized quota message.
|
|
1628
|
+
let quotaConfirmedByBody = false;
|
|
1588
1629
|
try {
|
|
1589
|
-
const body = await readBoundedResponseBody(response, {
|
|
1630
|
+
const body = await readBoundedResponseBody(response, {
|
|
1631
|
+
signal,
|
|
1632
|
+
// Match shouldRetryCodexPoolAccountQuota before treating a 5xx body as quota evidence.
|
|
1633
|
+
fatalUtf8: response.status >= 500 && response.status < 600,
|
|
1634
|
+
});
|
|
1590
1635
|
usage = usageFromComboFailureText(body.text);
|
|
1636
|
+
if (
|
|
1637
|
+
response.status >= 500 && response.status < 600
|
|
1638
|
+
&& body.displaySafe && !body.truncated
|
|
1639
|
+
) {
|
|
1640
|
+
const quotaMessage = codexQuotaFailureMessage(body.text);
|
|
1641
|
+
quotaConfirmedByBody = quotaMessage !== undefined
|
|
1642
|
+
&& isRateLimitOrQuotaFailureMessage(quotaMessage);
|
|
1643
|
+
}
|
|
1591
1644
|
if (body.displaySafe) {
|
|
1592
1645
|
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1593
1646
|
classificationText = normalized.safeText;
|
|
@@ -1608,18 +1661,24 @@ export async function consumeComboFailure(
|
|
|
1608
1661
|
? fallback
|
|
1609
1662
|
: `${fallback}: ${classificationText}`;
|
|
1610
1663
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1664
|
+
// Past HTTP dates are an immediate retry directive, just like the numeric value zero.
|
|
1665
|
+
// Normalize before the client helper discards them and substitutes a default delay.
|
|
1666
|
+
const effectiveRetryAfter = parseRetryAfterMs(upstreamRetryAfter, now) === undefined
|
|
1667
|
+
&& parseRetryAfterMs(upstreamRetryAfter, now, { preserveImmediate: true }) !== undefined
|
|
1668
|
+
? "0"
|
|
1669
|
+
: upstreamRetryAfter;
|
|
1611
1670
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1612
1671
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
1613
1672
|
const clientRetryAfter = resolveClientRetryAfter({
|
|
1614
1673
|
status: response.status,
|
|
1615
1674
|
message,
|
|
1616
|
-
upstreamRetryAfter,
|
|
1675
|
+
upstreamRetryAfter: effectiveRetryAfter,
|
|
1617
1676
|
now,
|
|
1618
1677
|
});
|
|
1619
1678
|
const cooldownRetryAfter = resolveClientRetryAfter({
|
|
1620
1679
|
status: response.status,
|
|
1621
1680
|
message,
|
|
1622
|
-
upstreamRetryAfter,
|
|
1681
|
+
upstreamRetryAfter: effectiveRetryAfter,
|
|
1623
1682
|
now,
|
|
1624
1683
|
includeDefault: false,
|
|
1625
1684
|
});
|
|
@@ -1636,6 +1695,14 @@ export async function consumeComboFailure(
|
|
|
1636
1695
|
classificationText,
|
|
1637
1696
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1638
1697
|
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1698
|
+
// The EFFECTIVE classification decides, not the raw status. An upstream that wraps a quota
|
|
1699
|
+
// refusal in a 5xx still carries `x-codex-*-reset-at`, and gating on 402/429 alone threw
|
|
1700
|
+
// those away, so the combo target came back up immediately instead of waiting for the
|
|
1701
|
+
// window it was told about. `cyberFailure` stays excluded: a policy block is not a quota.
|
|
1702
|
+
...(!cyberFailure
|
|
1703
|
+
&& (response.status === 429 || response.status === 402 || quotaConfirmedByBody)
|
|
1704
|
+
? { resetAt: codexQuotaOutcomeMeta(response).resetAt }
|
|
1705
|
+
: {}),
|
|
1639
1706
|
...(usage ? { usage } : {}),
|
|
1640
1707
|
};
|
|
1641
1708
|
}
|
|
@@ -1751,6 +1818,31 @@ function unreadableEncryptedAgentTaskResponse(): Response {
|
|
|
1751
1818
|
);
|
|
1752
1819
|
}
|
|
1753
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* Keep this trust boundary deliberately narrow: only a key-auth Responses route may consume
|
|
1823
|
+
* opaque child-task ciphertext, and the model's final wire override must still be Responses.
|
|
1824
|
+
* Callers keep combo attempts on their existing native-only recovery/fail-closed behavior.
|
|
1825
|
+
*/
|
|
1826
|
+
function canPassThroughEncryptedV2AgentTask(
|
|
1827
|
+
route: RouteResult,
|
|
1828
|
+
inboundWire: InboundWire,
|
|
1829
|
+
): boolean {
|
|
1830
|
+
if (route.combo !== undefined) return false;
|
|
1831
|
+
const provider = route.provider;
|
|
1832
|
+
if (
|
|
1833
|
+
inboundWire !== "responses"
|
|
1834
|
+
|| provider.allowEncryptedV2AgentTasks !== true
|
|
1835
|
+
|| (provider.authMode ?? "key") !== "key"
|
|
1836
|
+
) return false;
|
|
1837
|
+
|
|
1838
|
+
return resolveWireProtocolOverride(
|
|
1839
|
+
route.providerName,
|
|
1840
|
+
route.modelId,
|
|
1841
|
+
provider,
|
|
1842
|
+
inboundWire,
|
|
1843
|
+
).adapter === "openai-responses";
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1754
1846
|
type ResponsesAuthResolution =
|
|
1755
1847
|
| { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
|
|
1756
1848
|
| { ok: false; response: Response };
|
|
@@ -1798,6 +1890,8 @@ async function resolveResponsesCodexAuth(
|
|
|
1798
1890
|
let authCtx: CodexAuthContext;
|
|
1799
1891
|
if (route.codexAccountMode) {
|
|
1800
1892
|
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
1893
|
+
admission: options.admission,
|
|
1894
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
1801
1895
|
accountId: route.codexAccountId,
|
|
1802
1896
|
modelId: route.modelId,
|
|
1803
1897
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
@@ -1826,6 +1920,23 @@ async function resolveResponsesCodexAuth(
|
|
|
1826
1920
|
authCtx = { kind: "main", accountId: null };
|
|
1827
1921
|
options.onCodexAuthContextResolved?.(undefined);
|
|
1828
1922
|
}
|
|
1923
|
+
// This resolver also builds a synthetic main context for unrelated keyed routes. Only
|
|
1924
|
+
// the actual Codex-forward transport consumes main quota; provider names are not proof
|
|
1925
|
+
// (custom-named canonical-forward providers must retain the same protection).
|
|
1926
|
+
const mainPolicyConfig = isCanonicalOpenAiForwardProvider(route.provider)
|
|
1927
|
+
? options.codexAuthPolicy ?? config : undefined;
|
|
1928
|
+
const headers = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
|
|
1929
|
+
admission: options.admission,
|
|
1930
|
+
config: mainPolicyConfig,
|
|
1931
|
+
modelId: route.modelId,
|
|
1932
|
+
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1933
|
+
substituteMainCredential,
|
|
1934
|
+
signal: options.abortSignal,
|
|
1935
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1936
|
+
});
|
|
1937
|
+
// Awaiting even a cached materialization yields. Preserve the policy error if the live
|
|
1938
|
+
// quota/config changed during that yield, before usability could mislabel it as reauth.
|
|
1939
|
+
headersForCodexAuthContext(headers, authCtx, mainPolicyConfig, route.modelId, options.admission);
|
|
1829
1940
|
if (!isCodexAuthContextUsable(authCtx, config)) {
|
|
1830
1941
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
1831
1942
|
return {
|
|
@@ -1836,11 +1947,7 @@ async function resolveResponsesCodexAuth(
|
|
|
1836
1947
|
return {
|
|
1837
1948
|
ok: true,
|
|
1838
1949
|
authCtx,
|
|
1839
|
-
headers
|
|
1840
|
-
substituteMainCredential,
|
|
1841
|
-
signal: options.abortSignal,
|
|
1842
|
-
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1843
|
-
}),
|
|
1950
|
+
headers,
|
|
1844
1951
|
substituteMainCredential,
|
|
1845
1952
|
};
|
|
1846
1953
|
} catch (err) {
|
|
@@ -1883,6 +1990,7 @@ function isTerminalPoolRefreshFailure(error: unknown): boolean {
|
|
|
1883
1990
|
*/
|
|
1884
1991
|
async function refreshPoolForwardAuth(args: {
|
|
1885
1992
|
req: Request;
|
|
1993
|
+
config: OcxConfig;
|
|
1886
1994
|
route: RouteResult;
|
|
1887
1995
|
authCtx: CodexAuthContext & { kind: "pool" };
|
|
1888
1996
|
substituteMainCredential: boolean;
|
|
@@ -1891,7 +1999,7 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1891
1999
|
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1892
2000
|
| { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
|
|
1893
2001
|
> {
|
|
1894
|
-
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
2002
|
+
const { req, config, route, authCtx, substituteMainCredential, options } = args;
|
|
1895
2003
|
try {
|
|
1896
2004
|
const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
|
|
1897
2005
|
rejectedGeneration: authCtx.generation,
|
|
@@ -1929,6 +2037,9 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1929
2037
|
route.codexAccountMode,
|
|
1930
2038
|
);
|
|
1931
2039
|
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
2040
|
+
admission: options.admission,
|
|
2041
|
+
config: options.codexAuthPolicy ?? config,
|
|
2042
|
+
modelId: route.modelId,
|
|
1932
2043
|
substituteMainCredential,
|
|
1933
2044
|
signal: options.abortSignal,
|
|
1934
2045
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
@@ -1955,6 +2066,7 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1955
2066
|
|
|
1956
2067
|
async function refreshNativeMainForwardAuth(args: {
|
|
1957
2068
|
req: Request;
|
|
2069
|
+
config: OcxConfig;
|
|
1958
2070
|
route: RouteResult;
|
|
1959
2071
|
authCtx: CodexAuthContext;
|
|
1960
2072
|
substituteMainCredential: boolean;
|
|
@@ -1963,7 +2075,7 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1963
2075
|
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1964
2076
|
| { ok: false; response: Response }
|
|
1965
2077
|
> {
|
|
1966
|
-
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
2078
|
+
const { req, config, route, authCtx, substituteMainCredential, options } = args;
|
|
1967
2079
|
if (authCtx.kind !== "main-pool") {
|
|
1968
2080
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
|
|
1969
2081
|
}
|
|
@@ -1986,6 +2098,9 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1986
2098
|
route.codexAccountMode,
|
|
1987
2099
|
);
|
|
1988
2100
|
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
2101
|
+
admission: options.admission,
|
|
2102
|
+
config: options.codexAuthPolicy ?? config,
|
|
2103
|
+
modelId: route.modelId,
|
|
1989
2104
|
substituteMainCredential,
|
|
1990
2105
|
signal: options.abortSignal,
|
|
1991
2106
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
@@ -1995,7 +2110,9 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1995
2110
|
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
1996
2111
|
return { ok: false, response: clientCancelledResponse() };
|
|
1997
2112
|
}
|
|
1998
|
-
return { ok: false, response:
|
|
2113
|
+
return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
|
|
2114
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
2115
|
+
}) ?? nativeMainRefreshFailureResponse(error) };
|
|
1999
2116
|
}
|
|
2000
2117
|
}
|
|
2001
2118
|
|
|
@@ -2056,6 +2173,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2056
2173
|
|
|
2057
2174
|
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
2058
2175
|
// this request will actually use (#404).
|
|
2176
|
+
route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
|
|
2059
2177
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2060
2178
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
2061
2179
|
logCtx.model = route.modelId;
|
|
@@ -2287,6 +2405,15 @@ export async function handleComboResponses(
|
|
|
2287
2405
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2288
2406
|
const initialNow = Date.now();
|
|
2289
2407
|
let pick: ReturnType<typeof pickComboTarget> = null;
|
|
2408
|
+
const pickWithWait = (pickOptions: {
|
|
2409
|
+
exclude?: Iterable<string>;
|
|
2410
|
+
eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
|
|
2411
|
+
now?: number;
|
|
2412
|
+
}) => pickComboTargetWithWait(config, comboId, {
|
|
2413
|
+
...pickOptions,
|
|
2414
|
+
waitForCooldownMs: combo.waitForCooldownMs,
|
|
2415
|
+
abortSignal: options.abortSignal,
|
|
2416
|
+
});
|
|
2290
2417
|
|
|
2291
2418
|
if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
|
|
2292
2419
|
const recovery = agentTaskRecoveryConfig(config);
|
|
@@ -2304,9 +2431,7 @@ export async function handleComboResponses(
|
|
|
2304
2431
|
);
|
|
2305
2432
|
return unreadableEncryptedAgentTaskResponse();
|
|
2306
2433
|
}
|
|
2307
|
-
pick =
|
|
2308
|
-
eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
|
|
2309
|
-
});
|
|
2434
|
+
pick = await pickWithWait({ now: initialNow });
|
|
2310
2435
|
if (!pick) {
|
|
2311
2436
|
discardEncryptedAgentTaskRecovery(
|
|
2312
2437
|
req,
|
|
@@ -2314,7 +2439,9 @@ export async function handleComboResponses(
|
|
|
2314
2439
|
config,
|
|
2315
2440
|
{ parentThreadId: inboundClientThreadId },
|
|
2316
2441
|
);
|
|
2317
|
-
return
|
|
2442
|
+
return options.abortSignal?.aborted
|
|
2443
|
+
? clientCancelledResponse()
|
|
2444
|
+
: comboUnavailable(comboId);
|
|
2318
2445
|
}
|
|
2319
2446
|
let recovered = false;
|
|
2320
2447
|
try {
|
|
@@ -2344,14 +2471,16 @@ export async function handleComboResponses(
|
|
|
2344
2471
|
comboPayloadReadable = true;
|
|
2345
2472
|
comboReplaySnapshot.recoveredPlaintext = true;
|
|
2346
2473
|
} else {
|
|
2347
|
-
pick =
|
|
2348
|
-
eligible:
|
|
2349
|
-
|
|
2474
|
+
pick = await pickWithWait({
|
|
2475
|
+
eligible: payloadEligible,
|
|
2476
|
+
now: initialNow,
|
|
2350
2477
|
});
|
|
2351
2478
|
}
|
|
2352
2479
|
|
|
2353
2480
|
if (!pick) {
|
|
2354
|
-
return
|
|
2481
|
+
return options.abortSignal?.aborted
|
|
2482
|
+
? clientCancelledResponse()
|
|
2483
|
+
: comboUnavailable(comboId);
|
|
2355
2484
|
}
|
|
2356
2485
|
// One immutable combo selection trace, before any child dispatch; child
|
|
2357
2486
|
// adoption below must never replace it with a concrete child route trace.
|
|
@@ -2555,9 +2684,12 @@ export async function handleComboResponses(
|
|
|
2555
2684
|
console.warn(
|
|
2556
2685
|
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
2557
2686
|
);
|
|
2687
|
+
const failureNow = Date.now();
|
|
2558
2688
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
2559
2689
|
retryAfter: failure.retryAfter,
|
|
2560
|
-
|
|
2690
|
+
resetAt: failure.resetAt,
|
|
2691
|
+
cooldownMs: combo.cooldownMs,
|
|
2692
|
+
now: failureNow,
|
|
2561
2693
|
cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
|
|
2562
2694
|
code: failure.upstreamCode,
|
|
2563
2695
|
}),
|
|
@@ -2566,8 +2698,19 @@ export async function handleComboResponses(
|
|
|
2566
2698
|
code: failure.upstreamCode,
|
|
2567
2699
|
message: failure.classificationText,
|
|
2568
2700
|
});
|
|
2569
|
-
if (
|
|
2570
|
-
|
|
2701
|
+
if (nextPick) {
|
|
2702
|
+
pick = nextPick;
|
|
2703
|
+
} else {
|
|
2704
|
+
pick = await pickWithWait({
|
|
2705
|
+
exclude: pick.attempted,
|
|
2706
|
+
eligible: payloadEligible,
|
|
2707
|
+
now: failureNow,
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
if (!pick) {
|
|
2711
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2712
|
+
adoptFailedChildLog(childLog);
|
|
2713
|
+
}
|
|
2571
2714
|
}
|
|
2572
2715
|
if (
|
|
2573
2716
|
lastFailure?.status === 413
|
|
@@ -2685,7 +2828,13 @@ export async function handleResponses(
|
|
|
2685
2828
|
const ownsBudget = options.translatorBudget === undefined;
|
|
2686
2829
|
const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
|
|
2687
2830
|
try {
|
|
2688
|
-
const response = await handleResponsesInner(req, config, logCtx, {
|
|
2831
|
+
const response = await handleResponsesInner(req, config, logCtx, {
|
|
2832
|
+
...options,
|
|
2833
|
+
// Capture before combo replay rebuilds the Request headers; children carry options.
|
|
2834
|
+
visionDescribeTerminal: options.visionDescribeTerminal === true
|
|
2835
|
+
|| req.headers.get("x-opencodex-vision-describe") === "1",
|
|
2836
|
+
translatorBudget,
|
|
2837
|
+
});
|
|
2689
2838
|
return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
|
|
2690
2839
|
} catch (error) {
|
|
2691
2840
|
if (ownsBudget) translatorBudget.dispose();
|
|
@@ -2722,10 +2871,22 @@ async function handleResponsesInner(
|
|
|
2722
2871
|
}
|
|
2723
2872
|
// An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
|
|
2724
2873
|
// as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
|
|
2725
|
-
const
|
|
2874
|
+
const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
|
|
2726
2875
|
&& typeof (body as { model?: unknown }).model === "string"
|
|
2727
|
-
|
|
2728
|
-
|
|
2876
|
+
// One parse for both grammars, from the selector as the client sent it. Parsing them
|
|
2877
|
+
// separately made the outcome depend on which ran first.
|
|
2878
|
+
? parseSyntheticRowId((body as { model: string }).model, config)
|
|
2879
|
+
: { fastRow: null, effortRow: null };
|
|
2880
|
+
const comboEffortRow = comboRows.effortRow;
|
|
2881
|
+
if (comboRows.fastRow) {
|
|
2882
|
+
// Same reason as the effort row above: the combo dispatcher reads `model` next, so the
|
|
2883
|
+
// selector has to be normalized before it, or a combo child is built from a synthetic id.
|
|
2884
|
+
const raw = body as Record<string, unknown>;
|
|
2885
|
+
raw.model = comboRows.fastRow.baseId;
|
|
2886
|
+
// A caller INTENT, not a decision. decideTier still rules on eligibility downstream, so
|
|
2887
|
+
// fastMode:false and an ineligible route both still suppress it.
|
|
2888
|
+
raw.service_tier = "priority";
|
|
2889
|
+
}
|
|
2729
2890
|
if (comboEffortRow) {
|
|
2730
2891
|
const raw = body as Record<string, unknown>;
|
|
2731
2892
|
raw.model = comboEffortRow.baseId;
|
|
@@ -2791,7 +2952,15 @@ async function handleResponsesInner(
|
|
|
2791
2952
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
2792
2953
|
try {
|
|
2793
2954
|
parsed = parseRequest(body);
|
|
2794
|
-
|
|
2955
|
+
// Captured before any parser mutates it, so both grammars see the client's id.
|
|
2956
|
+
const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
|
|
2957
|
+
if (fastRow) {
|
|
2958
|
+
parsed.modelId = fastRow.baseId;
|
|
2959
|
+
parsed.options.serviceTier = "priority";
|
|
2960
|
+
const raw = parsed._rawBody as Record<string, unknown>;
|
|
2961
|
+
raw.model = fastRow.baseId;
|
|
2962
|
+
raw.service_tier = "priority";
|
|
2963
|
+
}
|
|
2795
2964
|
if (effortRow) {
|
|
2796
2965
|
parsed.modelId = effortRow.baseId;
|
|
2797
2966
|
parsed.options.reasoning = effortRow.effort;
|
|
@@ -3029,6 +3198,7 @@ async function handleResponsesInner(
|
|
|
3029
3198
|
subagentFallbackAccountPreview,
|
|
3030
3199
|
subagentFallbackModelEligibleAccountIdsForModel,
|
|
3031
3200
|
fallbackChain,
|
|
3201
|
+
candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
|
|
3032
3202
|
);
|
|
3033
3203
|
if (fallback) {
|
|
3034
3204
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -3058,7 +3228,8 @@ async function handleResponsesInner(
|
|
|
3058
3228
|
previewSelectionAdmission?.release();
|
|
3059
3229
|
}
|
|
3060
3230
|
|
|
3061
|
-
// Native fallback
|
|
3231
|
+
// Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
|
|
3232
|
+
// so recover only after final route selection.
|
|
3062
3233
|
if (
|
|
3063
3234
|
inboundWire === "responses"
|
|
3064
3235
|
&&
|
|
@@ -3067,6 +3238,7 @@ async function handleResponsesInner(
|
|
|
3067
3238
|
&& agentTaskRecovery
|
|
3068
3239
|
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
3069
3240
|
&& !options.comboAttempt
|
|
3241
|
+
&& !canPassThroughEncryptedV2AgentTask(route, inboundWire)
|
|
3070
3242
|
) {
|
|
3071
3243
|
let recovered = false;
|
|
3072
3244
|
try {
|
|
@@ -3190,9 +3362,16 @@ async function handleResponsesInner(
|
|
|
3190
3362
|
|
|
3191
3363
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
3192
3364
|
|
|
3193
|
-
// Encrypted child tasks may
|
|
3194
|
-
// runs against the FINAL route so native-only fallback can
|
|
3195
|
-
|
|
3365
|
+
// Encrypted child tasks may reach the canonical native backend or an explicitly trusted
|
|
3366
|
+
// direct Responses route. This runs against the FINAL route so native-only fallback can
|
|
3367
|
+
// rescue an incompatible primary without weakening combo behavior.
|
|
3368
|
+
const finalRouteCanPassThroughEncryptedTask = !options.comboAttempt
|
|
3369
|
+
&& canPassThroughEncryptedV2AgentTask(route, inboundWire);
|
|
3370
|
+
if (
|
|
3371
|
+
(route.combo !== undefined || !isCanonicalOpenAiForwardProvider(route.provider))
|
|
3372
|
+
&& !finalRouteCanPassThroughEncryptedTask
|
|
3373
|
+
&& unreadableEncryptedAgentTask
|
|
3374
|
+
) {
|
|
3196
3375
|
return unreadableEncryptedAgentTaskResponse();
|
|
3197
3376
|
}
|
|
3198
3377
|
|
|
@@ -3231,6 +3410,12 @@ async function handleResponsesInner(
|
|
|
3231
3410
|
}
|
|
3232
3411
|
|
|
3233
3412
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
3413
|
+
// Resolve aliases/combo children before refusing helpers; do not spend main auth or host budget.
|
|
3414
|
+
if (isCanonicalOpenAiForwardProvider(route.provider)
|
|
3415
|
+
&& isCodexReserveHelperUnsupported(options.codexAuthPolicy ?? config, route.modelId,
|
|
3416
|
+
options.admission, options.visionDescribeTerminal === true)) {
|
|
3417
|
+
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE);
|
|
3418
|
+
}
|
|
3234
3419
|
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
3235
3420
|
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
3236
3421
|
//
|
|
@@ -3308,6 +3493,13 @@ async function handleResponsesInner(
|
|
|
3308
3493
|
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
3309
3494
|
let genericFailoverAccountId: string | null = null;
|
|
3310
3495
|
let genericFailovers = 0;
|
|
3496
|
+
/**
|
|
3497
|
+
* Config generation captured where the serving credential is RESOLVED, not where the
|
|
3498
|
+
* quota is written. A streaming turn is a long await, so a generation captured at write
|
|
3499
|
+
* time cannot see a config or account change that happened earlier in the same turn —
|
|
3500
|
+
* the case the fence exists for. Stays 0 for every provider without a passive quota.
|
|
3501
|
+
*/
|
|
3502
|
+
let passiveQuotaWriterGeneration = 0;
|
|
3311
3503
|
/**
|
|
3312
3504
|
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
3313
3505
|
*
|
|
@@ -3329,7 +3521,10 @@ async function handleResponsesInner(
|
|
|
3329
3521
|
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
3330
3522
|
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
3331
3523
|
*/
|
|
3332
|
-
const applyFailoverSnapshot = (
|
|
3524
|
+
const applyFailoverSnapshot = (
|
|
3525
|
+
snapshot: OAuthAccessSnapshot,
|
|
3526
|
+
retryParsed: OcxParsedRequest = parsed,
|
|
3527
|
+
): boolean => {
|
|
3333
3528
|
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
3334
3529
|
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
3335
3530
|
if (route.providerName === "github-copilot") {
|
|
@@ -3342,7 +3537,14 @@ async function handleResponsesInner(
|
|
|
3342
3537
|
}
|
|
3343
3538
|
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
3344
3539
|
route.provider = rotatedProvider;
|
|
3345
|
-
if (route.providerName === "kiro")
|
|
3540
|
+
if (route.providerName === "kiro") {
|
|
3541
|
+
const kiroContext = { ...(snapshot.kiro ?? {}) };
|
|
3542
|
+
// Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
|
|
3543
|
+
// outer request pairs the new bearer with the failed account's region/profile on
|
|
3544
|
+
// the retry. Keep both owners synchronized; for ordinary paths they are identical.
|
|
3545
|
+
parsed._kiroAuthContext = kiroContext;
|
|
3546
|
+
if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
|
|
3547
|
+
}
|
|
3346
3548
|
// Re-stamp: a request that rotated accounts must be attributed to the account that actually
|
|
3347
3549
|
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
3348
3550
|
// needed -- and putting it anywhere else would let one of the three drift.
|
|
@@ -3443,6 +3645,18 @@ async function handleResponsesInner(
|
|
|
3443
3645
|
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
3444
3646
|
genericFailoverAccountId = resolved.accountId;
|
|
3445
3647
|
}
|
|
3648
|
+
// Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
|
|
3649
|
+
// a fail-closed local-cli credential rule -- so without this stamp its identity is
|
|
3650
|
+
// dropped whenever the pool flag is off, and a later 429 has no account to cool. Reactive
|
|
3651
|
+
// failover needs only the id: no affinity bind, no promotion, no quota-ranked pick. Those
|
|
3652
|
+
// are proactive and stay behind anthropicAccountPool.enabled.
|
|
3653
|
+
if (route.providerName === "anthropic" && hasAnthropicFailoverQuorum()) {
|
|
3654
|
+
anthropicPoolAccountId = resolved.accountId;
|
|
3655
|
+
}
|
|
3656
|
+
// Captured beside the account it fences, so the two can never disagree.
|
|
3657
|
+
if (hasPassiveAccountQuota(route.providerName)) {
|
|
3658
|
+
passiveQuotaWriterGeneration = captureConfigGeneration();
|
|
3659
|
+
}
|
|
3446
3660
|
if (route.providerName === "kiro") {
|
|
3447
3661
|
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
3448
3662
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
@@ -3553,6 +3767,17 @@ async function handleResponsesInner(
|
|
|
3553
3767
|
}
|
|
3554
3768
|
const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
|
|
3555
3769
|
|
|
3770
|
+
const rawInput = (parsed._rawBody as { input?: unknown }).input;
|
|
3771
|
+
if (!isPassthrough && Array.isArray(rawInput) && rawInput.some(
|
|
3772
|
+
item => item !== null && typeof item === "object" && item.type === "computer_call_output",
|
|
3773
|
+
)) {
|
|
3774
|
+
return formatErrorResponse(
|
|
3775
|
+
400,
|
|
3776
|
+
"invalid_request_error",
|
|
3777
|
+
"computer_call_output requires a Responses passthrough route; send screenshots as user input_image content on translated routes.",
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3556
3781
|
if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
3557
3782
|
return formatErrorResponse(
|
|
3558
3783
|
400,
|
|
@@ -3571,6 +3796,8 @@ async function handleResponsesInner(
|
|
|
3571
3796
|
req.headers,
|
|
3572
3797
|
config,
|
|
3573
3798
|
{
|
|
3799
|
+
admission: options.admission,
|
|
3800
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
3574
3801
|
// Account-qualified native routes are passthrough, so their in-turn helper is vision.
|
|
3575
3802
|
// Scope its cooldown and outcome to the helper model, not the routed text model.
|
|
3576
3803
|
...(route.codexAccountId !== undefined
|
|
@@ -3600,11 +3827,12 @@ async function handleResponsesInner(
|
|
|
3600
3827
|
// call must never plan another describe. The flag arrives from the Chat
|
|
3601
3828
|
// surface (whose bridge rebuilds headers) or as the raw header for native
|
|
3602
3829
|
// Responses callers. Marked + text-only routed model → strip, depth cap 1.
|
|
3603
|
-
const visionDescribeTerminal = options.visionDescribeTerminal === true
|
|
3604
|
-
|| req.headers.get("x-opencodex-vision-describe") === "1";
|
|
3830
|
+
const visionDescribeTerminal = options.visionDescribeTerminal === true;
|
|
3605
3831
|
const visionPlan = visionDescribeTerminal
|
|
3606
3832
|
? undefined
|
|
3607
|
-
: planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar
|
|
3833
|
+
: planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, {
|
|
3834
|
+
admission: options.admission, codexAuthPolicy: options.codexAuthPolicy,
|
|
3835
|
+
});
|
|
3608
3836
|
const recordSidecarOutcome = openAiSidecar?.recordOutcome;
|
|
3609
3837
|
if (visionPlan) {
|
|
3610
3838
|
await describeImagesInPlace(
|
|
@@ -3779,7 +4007,7 @@ async function handleResponsesInner(
|
|
|
3779
4007
|
// The guard needs a catalog to compare against, so it stands down when the request omits one.
|
|
3780
4008
|
// An explicit empty catalog is still authoritative: it declares that no client tools may be
|
|
3781
4009
|
// called. A passthrough request can legitimately omit `tools` entirely and still receive a call
|
|
3782
|
-
// the client understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
4010
|
+
// the client understands — `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` sends
|
|
3783
4011
|
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
3784
4012
|
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
3785
4013
|
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
@@ -3888,7 +4116,27 @@ async function handleResponsesInner(
|
|
|
3888
4116
|
// check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
|
|
3889
4117
|
// rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
|
|
3890
4118
|
let inspectionSawUndeclaredTool = false;
|
|
4119
|
+
const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName)
|
|
4120
|
+
&& route.provider.authMode === "oauth";
|
|
3891
4121
|
const noteInspectedPayload = (payload: unknown) => {
|
|
4122
|
+
// Meta reports subscription usage ONLY as an in-stream event; there is no endpoint
|
|
4123
|
+
// to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a
|
|
4124
|
+
// dedicated inspector handler because onParsedPayload already reaches every
|
|
4125
|
+
// passthrough shape -- eager relay and both tee consumers -- through this one
|
|
4126
|
+
// function.
|
|
4127
|
+
//
|
|
4128
|
+
// Placed BEFORE the undeclared-tool early return below, which is load-bearing: that
|
|
4129
|
+
// guard latches for the rest of the turn once it fires, and a turn that tripped it
|
|
4130
|
+
// still legitimately reports usage.
|
|
4131
|
+
if (passiveQuotaObserved && isMuseSubscriptionUsagePayload(payload)) {
|
|
4132
|
+
const quota = parseMuseSubscriptionUsage(payload);
|
|
4133
|
+
// Read at EVENT time, not at handler construction: failover rebinds this, and the
|
|
4134
|
+
// quota belongs to the account that actually served the turn.
|
|
4135
|
+
const servingAccountId = genericFailoverAccountId;
|
|
4136
|
+
if (quota && servingAccountId) {
|
|
4137
|
+
recordPassiveAccountQuota(route.providerName, servingAccountId, quota, passiveQuotaWriterGeneration);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
3892
4140
|
// Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
|
|
3893
4141
|
// provider) every name looks undeclared, and flipping this would stop recording continuation
|
|
3894
4142
|
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
@@ -4028,6 +4276,15 @@ async function handleResponsesInner(
|
|
|
4028
4276
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
4029
4277
|
return clientCancelledResponse();
|
|
4030
4278
|
}
|
|
4279
|
+
const localRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
4280
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
4281
|
+
});
|
|
4282
|
+
if (localRefusal) {
|
|
4283
|
+
releaseUpstreamHostAdmission(hostAdmissionLease);
|
|
4284
|
+
hostAdmissionLease = null;
|
|
4285
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
4286
|
+
return localRefusal;
|
|
4287
|
+
}
|
|
4031
4288
|
const outcome = classifyTransportFailureKind(err);
|
|
4032
4289
|
// Host-level evidence stands regardless of pool membership: a direct
|
|
4033
4290
|
// forward send has no pool accounting, but the reachability failure is
|
|
@@ -4079,6 +4336,9 @@ async function handleResponsesInner(
|
|
|
4079
4336
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4080
4337
|
providerName: route.providerName,
|
|
4081
4338
|
modelId: route.modelId,
|
|
4339
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
4340
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
4341
|
+
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
4082
4342
|
}),
|
|
4083
4343
|
route.provider.authMode === "forward")
|
|
4084
4344
|
// Every real attempt response — including an intermediate 5xx the
|
|
@@ -4152,6 +4412,9 @@ async function handleResponsesInner(
|
|
|
4152
4412
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4153
4413
|
providerName: route.providerName,
|
|
4154
4414
|
modelId: route.modelId,
|
|
4415
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
4416
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
4417
|
+
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
4155
4418
|
}),
|
|
4156
4419
|
route.provider.authMode === "forward")
|
|
4157
4420
|
.then(response => {
|
|
@@ -4181,10 +4444,10 @@ async function handleResponsesInner(
|
|
|
4181
4444
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
|
|
4182
4445
|
const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
|
|
4183
4446
|
const poolReplay = poolAuthCtx
|
|
4184
|
-
? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
|
|
4447
|
+
? await refreshPoolForwardAuth({ req, config, route, authCtx: poolAuthCtx, substituteMainCredential, options })
|
|
4185
4448
|
: undefined;
|
|
4186
4449
|
const replay = poolReplay
|
|
4187
|
-
?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
|
|
4450
|
+
?? await refreshNativeMainForwardAuth({ req, config, route, authCtx, substituteMainCredential, options });
|
|
4188
4451
|
if (!replay.ok) {
|
|
4189
4452
|
// Compact already records this; core historically returned without recording,
|
|
4190
4453
|
// so a dead grant stayed selectable and every request repeated the same doomed
|
|
@@ -4253,6 +4516,9 @@ async function handleResponsesInner(
|
|
|
4253
4516
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4254
4517
|
providerName: route.providerName,
|
|
4255
4518
|
modelId: route.modelId,
|
|
4519
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
4520
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
4521
|
+
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
4256
4522
|
}),
|
|
4257
4523
|
codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
|
|
4258
4524
|
),
|
|
@@ -4359,6 +4625,9 @@ async function handleResponsesInner(
|
|
|
4359
4625
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4360
4626
|
providerName: route.providerName,
|
|
4361
4627
|
modelId: route.modelId,
|
|
4628
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
4629
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
4630
|
+
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
4362
4631
|
}),
|
|
4363
4632
|
route.provider.authMode === "forward")
|
|
4364
4633
|
.then(res => {
|
|
@@ -4375,6 +4644,43 @@ async function handleResponsesInner(
|
|
|
4375
4644
|
}
|
|
4376
4645
|
}
|
|
4377
4646
|
|
|
4647
|
+
// Native Responses returns before the generic adapter's OAuth rotation loop. Keep
|
|
4648
|
+
// the same quorum, cooldown and request budget here, before any client bytes flow.
|
|
4649
|
+
if (
|
|
4650
|
+
upstreamResponse.status === 429
|
|
4651
|
+
&& genericFailoverAccountId
|
|
4652
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4653
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4654
|
+
) {
|
|
4655
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4656
|
+
config, route.providerName, genericFailoverAccountId,
|
|
4657
|
+
upstreamResponse.headers.get("retry-after"),
|
|
4658
|
+
);
|
|
4659
|
+
let snapshot: OAuthAccessSnapshot | undefined;
|
|
4660
|
+
if (nextAccountId) {
|
|
4661
|
+
try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
|
|
4662
|
+
catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
|
|
4663
|
+
}
|
|
4664
|
+
if (snapshot && applyFailoverSnapshot(snapshot)) {
|
|
4665
|
+
genericFailoverAccountId = snapshot.accountId;
|
|
4666
|
+
genericFailovers += 1;
|
|
4667
|
+
sentOAuthSnapshot = snapshot;
|
|
4668
|
+
replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
|
|
4669
|
+
route.provider = resolveProviderTransport(
|
|
4670
|
+
route.providerName, route.provider, parsed.options.promptCacheKey, snapshot.apiBaseUrl,
|
|
4671
|
+
);
|
|
4672
|
+
bindRouteReasoningReplayScope({
|
|
4673
|
+
parsed, providerName: route.providerName, provider: route.provider,
|
|
4674
|
+
adapterName: "openai-responses", oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
|
|
4675
|
+
});
|
|
4676
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
4677
|
+
const result = await rebuildAndRefetch("oauth-account-429");
|
|
4678
|
+
if ("failed" in result) return result.failed;
|
|
4679
|
+
upstreamResponse = result;
|
|
4680
|
+
continue passthroughRecovery;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4378
4684
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
|
|
4379
4685
|
// passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
|
|
4380
4686
|
// key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
|
|
@@ -4421,6 +4727,9 @@ async function handleResponsesInner(
|
|
|
4421
4727
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4422
4728
|
providerName: route.providerName,
|
|
4423
4729
|
modelId: route.modelId,
|
|
4730
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
4731
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
4732
|
+
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
4424
4733
|
}),
|
|
4425
4734
|
route.provider.authMode === "forward")
|
|
4426
4735
|
.then(res => {
|
|
@@ -4578,11 +4887,10 @@ async function handleResponsesInner(
|
|
|
4578
4887
|
// Prefer primary when present, fall back to secondary for compatibility.
|
|
4579
4888
|
const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
|
|
4580
4889
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
4581
|
-
|
|
4582
|
-
authCtx.accountId,
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
);
|
|
4890
|
+
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
|
|
4891
|
+
applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
|
|
4892
|
+
authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined);
|
|
4893
|
+
}
|
|
4586
4894
|
if (terminalBodyWillRecord) {
|
|
4587
4895
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
4588
4896
|
terminalRecorder(status, httpStatusOverride);
|
|
@@ -4836,7 +5144,10 @@ async function handleResponsesInner(
|
|
|
4836
5144
|
},
|
|
4837
5145
|
onClientCancel: () => options.onNativePassthroughCancel?.(),
|
|
4838
5146
|
onDone: () => unregisterTurn(turnAc),
|
|
4839
|
-
},
|
|
5147
|
+
}, {
|
|
5148
|
+
clientGoneSignal: options.abortSignal,
|
|
5149
|
+
...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
|
|
5150
|
+
});
|
|
4840
5151
|
// When selected, this relay closes response.completed even if upstream
|
|
4841
5152
|
// keeps the connection alive. Marked Codex WS traffic, Windows
|
|
4842
5153
|
// forced-rewrite traffic, and Darwin explicit eager traffic apply
|
|
@@ -4855,7 +5166,10 @@ async function handleResponsesInner(
|
|
|
4855
5166
|
linkAbortSignal(upstream, turnAc.signal);
|
|
4856
5167
|
registerTurn(turnAc, options.turnAdmissionLease);
|
|
4857
5168
|
const inspectionConsumerOptions = {
|
|
4858
|
-
|
|
5169
|
+
// Request abort can reject the fetch body before the response cancel hook runs.
|
|
5170
|
+
clientGoneSignal: options.abortSignal
|
|
5171
|
+
? AbortSignal.any([clientGone.signal, options.abortSignal])
|
|
5172
|
+
: clientGone.signal,
|
|
4859
5173
|
drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
|
|
4860
5174
|
upstream,
|
|
4861
5175
|
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
@@ -5094,6 +5408,36 @@ async function handleResponsesInner(
|
|
|
5094
5408
|
}
|
|
5095
5409
|
}
|
|
5096
5410
|
|
|
5411
|
+
// Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId
|
|
5412
|
+
// (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all
|
|
5413
|
+
// (schema.ts:106) accepts a tool item whose strict schema failed only for a missing call_id. A
|
|
5414
|
+
// translating adapter then consumes `toolCallId: string` holding undefined: kiro-wire.ts:32
|
|
5415
|
+
// TypeErrors, ollama-native.ts:334 throws, and anthropic.ts:775 sends
|
|
5416
|
+
// "[tool_result without adjacent tool_use: undefined]" upstream (issue #3259).
|
|
5417
|
+
//
|
|
5418
|
+
// This CANNOT move into the schema. parseRequest (:2812) runs before the passthrough branch
|
|
5419
|
+
// (:3719), so a parse-time rejection would also kill forward/key passthrough and routed
|
|
5420
|
+
// compaction — paths that never read context.messages, build from _rawBody, and already
|
|
5421
|
+
// degrade an unpaired output to "[tool output for unknown call]" on their own.
|
|
5422
|
+
//
|
|
5423
|
+
// Keyed on the adapter, not on position: routedCompaction skips the passthrough branch above
|
|
5424
|
+
// yet still builds from _rawBody (see the :3703 comment).
|
|
5425
|
+
if (!("passthrough" in adapter && adapter.passthrough)) {
|
|
5426
|
+
const unpaired = parsed.context.messages.find(
|
|
5427
|
+
message => message.role === "toolResult"
|
|
5428
|
+
&& (typeof (message as { toolCallId?: unknown }).toolCallId !== "string"
|
|
5429
|
+
|| (message as { toolCallId: string }).toolCallId.length === 0),
|
|
5430
|
+
);
|
|
5431
|
+
if (unpaired) {
|
|
5432
|
+
// Never interpolate the tool output: this message reaches the client and the logs.
|
|
5433
|
+
return formatErrorResponse(
|
|
5434
|
+
400,
|
|
5435
|
+
"invalid_request_error",
|
|
5436
|
+
"tool result requires a non-empty string call_id",
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5097
5441
|
// Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
|
|
5098
5442
|
// Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
|
|
5099
5443
|
// leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
|
|
@@ -5105,7 +5449,9 @@ async function handleResponsesInner(
|
|
|
5105
5449
|
// - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
|
|
5106
5450
|
// can proceed for web-search-only turns
|
|
5107
5451
|
const wsPlan = !routedCompaction
|
|
5108
|
-
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar
|
|
5452
|
+
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, {
|
|
5453
|
+
admission: options.admission, codexAuthPolicy: options.codexAuthPolicy,
|
|
5454
|
+
})
|
|
5109
5455
|
: undefined;
|
|
5110
5456
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
5111
5457
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
@@ -5119,12 +5465,15 @@ async function handleResponsesInner(
|
|
|
5119
5465
|
});
|
|
5120
5466
|
if (rotated) {
|
|
5121
5467
|
route.provider = rotated;
|
|
5122
|
-
} else
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5468
|
+
} else if (
|
|
5469
|
+
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
5470
|
+
// unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
|
|
5471
|
+
// excludes it), so its sidecar 429s died on this guard before the Anthropic arm below
|
|
5472
|
+
// could ever be considered.
|
|
5473
|
+
genericFailoverAccountId
|
|
5474
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
5475
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
5476
|
+
) {
|
|
5128
5477
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
5129
5478
|
config,
|
|
5130
5479
|
route.providerName,
|
|
@@ -5140,6 +5489,39 @@ async function handleResponsesInner(
|
|
|
5140
5489
|
} catch {
|
|
5141
5490
|
return null;
|
|
5142
5491
|
}
|
|
5492
|
+
} else if (
|
|
5493
|
+
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
5494
|
+
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
5495
|
+
// the very same 429 on the main response path rotated.
|
|
5496
|
+
anthropicPoolAccountId
|
|
5497
|
+
&& anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
5498
|
+
) {
|
|
5499
|
+
const nextAccountId = rotateAnthropicAccountOn429(
|
|
5500
|
+
config,
|
|
5501
|
+
anthropicPoolAccountId,
|
|
5502
|
+
retryAfter,
|
|
5503
|
+
anthropicSessionKey,
|
|
5504
|
+
);
|
|
5505
|
+
if (!nextAccountId) return null;
|
|
5506
|
+
try {
|
|
5507
|
+
// Deliberately NOT applyFailoverSnapshot: that helper exists to pair per-account routing
|
|
5508
|
+
// metadata (Copilot origin, Antigravity project, Kiro context) with its bearer. Anthropic
|
|
5509
|
+
// carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
|
|
5510
|
+
// local-cli credential rule. Both existing Anthropic rotation sites apply the token the
|
|
5511
|
+
// same way.
|
|
5512
|
+
const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
|
|
5513
|
+
anthropicPoolAccountId = nextAccountId;
|
|
5514
|
+
anthropicPoolFailovers += 1;
|
|
5515
|
+
route.provider = { ...route.provider, apiKey: accessToken };
|
|
5516
|
+
promoteAnthropicActiveAccount(nextAccountId);
|
|
5517
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
5518
|
+
} catch {
|
|
5519
|
+
return null;
|
|
5520
|
+
}
|
|
5521
|
+
} else {
|
|
5522
|
+
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
5523
|
+
// credential. The 429 is terminal for this sidecar turn.
|
|
5524
|
+
return null;
|
|
5143
5525
|
}
|
|
5144
5526
|
const rotatedAdapter = resolveAdapter(
|
|
5145
5527
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
@@ -6008,6 +6390,37 @@ async function handleResponsesInner(
|
|
|
6008
6390
|
continue recovery;
|
|
6009
6391
|
}
|
|
6010
6392
|
|
|
6393
|
+
// Static API-key pools can recover a credential-scoped 401 without abandoning the
|
|
6394
|
+
// provider: one revoked or mistyped key says nothing about its siblings. OAuth providers
|
|
6395
|
+
// refresh above and never enter here — `hasKeyPoolFailover` rejects oauth/forward modes.
|
|
6396
|
+
// Runs after the OAuth replay so a refreshable token is never treated as a dead key.
|
|
6397
|
+
while (upstreamResponse.status === 401 && hasKeyPoolFailover(route.provider)) {
|
|
6398
|
+
const rotated = rotateProviderTransportOn401(config, route.providerName, route.provider, {
|
|
6399
|
+
now: Date.now(),
|
|
6400
|
+
attemptedKey: route.provider.apiKey,
|
|
6401
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
6402
|
+
});
|
|
6403
|
+
if (!rotated) break;
|
|
6404
|
+
// Release the failed response's socket before retrying; unread bodies otherwise linger
|
|
6405
|
+
// until runtime cleanup (one per rotated key).
|
|
6406
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
6407
|
+
route.provider = rotated;
|
|
6408
|
+
invalidateSameTargetRequest();
|
|
6409
|
+
activeAdapter = resolveAdapter(
|
|
6410
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6411
|
+
config.cacheRetention,
|
|
6412
|
+
);
|
|
6413
|
+
bindRouteReasoningReplayScope({
|
|
6414
|
+
parsed,
|
|
6415
|
+
providerName: route.providerName,
|
|
6416
|
+
provider: route.provider,
|
|
6417
|
+
adapterName: activeAdapter.name,
|
|
6418
|
+
});
|
|
6419
|
+
const result = await rebuildAndRefetch("key-401");
|
|
6420
|
+
if ("failed" in result) return result.failed;
|
|
6421
|
+
upstreamResponse = result;
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6011
6424
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
|
|
6012
6425
|
// 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
|
|
6013
6426
|
// so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
|
|
@@ -6083,7 +6496,6 @@ async function handleResponsesInner(
|
|
|
6083
6496
|
while (
|
|
6084
6497
|
upstreamResponse.status === 429
|
|
6085
6498
|
&& anthropicPoolAccountId
|
|
6086
|
-
&& isAnthropicAccountPoolEnabled(config)
|
|
6087
6499
|
&& anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
6088
6500
|
) {
|
|
6089
6501
|
const nextAccountId = rotateAnthropicAccountOn429(
|
|
@@ -6494,7 +6906,6 @@ async function handleResponsesInner(
|
|
|
6494
6906
|
if (
|
|
6495
6907
|
response.status === 429
|
|
6496
6908
|
&& anthropicPoolAccountId
|
|
6497
|
-
&& isAnthropicAccountPoolEnabled(config)
|
|
6498
6909
|
&& anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
6499
6910
|
) {
|
|
6500
6911
|
const nextAccountId = rotateAnthropicAccountOn429(
|
|
@@ -6525,6 +6936,48 @@ async function handleResponsesInner(
|
|
|
6525
6936
|
}
|
|
6526
6937
|
}
|
|
6527
6938
|
}
|
|
6939
|
+
// Generic OAuth rotation for the continuation loop. The streaming loop grew this arm with
|
|
6940
|
+
// #2568 and this one did not, so an xAI/Cursor/Kimi/Copilot/Antigravity/Nous continuation
|
|
6941
|
+
// 429 stayed terminal even with failover fully active -- the same class of divergence the
|
|
6942
|
+
// two sidecars already produced once. Request-local state is shared with the other arms so
|
|
6943
|
+
// the per-request bound cannot be silently re-armed by reaching a different loop.
|
|
6944
|
+
if (
|
|
6945
|
+
response.status === 429
|
|
6946
|
+
&& genericFailoverAccountId
|
|
6947
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
6948
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
6949
|
+
) {
|
|
6950
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
6951
|
+
config,
|
|
6952
|
+
route.providerName,
|
|
6953
|
+
genericFailoverAccountId,
|
|
6954
|
+
response.headers.get("retry-after"),
|
|
6955
|
+
);
|
|
6956
|
+
if (nextAccountId) {
|
|
6957
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
6958
|
+
try {
|
|
6959
|
+
// The FULL snapshot through the shared helper, never a bare bearer: Antigravity
|
|
6960
|
+
// pairs an account-matched projectId with its token and Kiro carries routing
|
|
6961
|
+
// metadata, so a token-only swap would mix one account's credential with another's
|
|
6962
|
+
// routing data.
|
|
6963
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
6964
|
+
genericFailoverAccountId = nextAccountId;
|
|
6965
|
+
genericFailovers += 1;
|
|
6966
|
+
if (applyFailoverSnapshot(snapshot, nextParsed)) {
|
|
6967
|
+
invalidateSameTargetRequest();
|
|
6968
|
+
activeAdapter = resolveAdapter(
|
|
6969
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6970
|
+
config.cacheRetention,
|
|
6971
|
+
);
|
|
6972
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
6973
|
+
nextContinuationRecoveryKind = "oauth-account-429";
|
|
6974
|
+
continue;
|
|
6975
|
+
}
|
|
6976
|
+
} catch {
|
|
6977
|
+
// fall through to emit continuation error below
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6528
6981
|
if (shouldAttemptImageTierRetry({
|
|
6529
6982
|
status: response.status,
|
|
6530
6983
|
adapterName: activeAdapter.name,
|