@bitkyc08/opencodex 2.42.0 → 2.43.0
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-Djowl68T.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 +13 -3
- 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/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- 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 +22 -10
- 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 +21 -6
- package/src/cli/capabilities.ts +13 -2
- 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/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 +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- 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 +454 -166
- 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/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- 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/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 +535 -104
- 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 +107 -21
- 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 +38 -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 +476 -57
- 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/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-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -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,
|
|
@@ -237,10 +246,12 @@ import {
|
|
|
237
246
|
rateLimitRetryDelayMs,
|
|
238
247
|
rateLimitRetryPolicyFor,
|
|
239
248
|
rotateProviderTransportOn429,
|
|
249
|
+
rotateProviderTransportOn401,
|
|
240
250
|
transientRetryPolicyFor,
|
|
241
251
|
} from "../../providers/key-failover";
|
|
242
252
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
243
253
|
import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
|
|
254
|
+
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
244
255
|
import type { WsData } from "../ws-bridge";
|
|
245
256
|
import {
|
|
246
257
|
codexAccountSelectionForTurn,
|
|
@@ -287,6 +298,7 @@ import {
|
|
|
287
298
|
conversationIdFromResponsesRequest,
|
|
288
299
|
normalizeLogConversationId,
|
|
289
300
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
301
|
+
sessionLaneIdFromRequest,
|
|
290
302
|
sessionIdHeaderFromRequest,
|
|
291
303
|
} from "../request-log-conversation";
|
|
292
304
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -332,6 +344,7 @@ import {
|
|
|
332
344
|
} from "../responses-image-gen-repair";
|
|
333
345
|
import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
|
|
334
346
|
import { parseRequestEffortRowId } from "../effort-row";
|
|
347
|
+
import { parseSyntheticRowId } from "../fast-row";
|
|
335
348
|
import {
|
|
336
349
|
collectSelfNamedNamespaceScrubAuthorization,
|
|
337
350
|
createSelfNamedToolCallNamespaceScrubRewrite,
|
|
@@ -861,6 +874,13 @@ export function usesCodexForwardPoolAuth(
|
|
|
861
874
|
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
862
875
|
}
|
|
863
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
|
+
|
|
864
884
|
export function preAuthUpstreamHostCircuitKey(
|
|
865
885
|
route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
|
|
866
886
|
config: OcxConfig,
|
|
@@ -968,6 +988,9 @@ interface CodexPoolAccountRetryArgs {
|
|
|
968
988
|
parsed: OcxParsedRequest;
|
|
969
989
|
logCtx: RequestLogContext;
|
|
970
990
|
options: {
|
|
991
|
+
admission?: DataPlaneAdmission;
|
|
992
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
993
|
+
visionDescribeTerminal?: boolean;
|
|
971
994
|
abortSignal?: AbortSignal;
|
|
972
995
|
onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
|
|
973
996
|
deferCodexResetDerivedCooldown?: boolean;
|
|
@@ -1164,6 +1187,8 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1164
1187
|
"pool",
|
|
1165
1188
|
{
|
|
1166
1189
|
excludeAccountId: firstAuthCtx.accountId,
|
|
1190
|
+
admission: options.admission,
|
|
1191
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
1167
1192
|
modelId: route.modelId,
|
|
1168
1193
|
requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
|
|
1169
1194
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
@@ -1213,6 +1238,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1213
1238
|
firstAuthCtx.accountId,
|
|
1214
1239
|
firstResponse.headers,
|
|
1215
1240
|
firstAuthCtx.writerGeneration,
|
|
1241
|
+
firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
|
|
1216
1242
|
);
|
|
1217
1243
|
}
|
|
1218
1244
|
const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
|
|
@@ -1234,7 +1260,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1234
1260
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
1235
1261
|
// ordinary requests must block the first account before the alternate send.
|
|
1236
1262
|
if (!deferFirstOutcome) recordFirstOutcome();
|
|
1237
|
-
const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
|
|
1263
|
+
const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
|
|
1238
1264
|
const retryProvider = applyCodexAuthContextToProvider(
|
|
1239
1265
|
stripCodexRuntimeProviderFields(route.provider),
|
|
1240
1266
|
retryAuthCtx,
|
|
@@ -1303,6 +1329,9 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1303
1329
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1304
1330
|
providerName: route.providerName,
|
|
1305
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,
|
|
1306
1335
|
}),
|
|
1307
1336
|
// Credential-bearing forward send: never follow a redirect into a
|
|
1308
1337
|
// dead-host rejection after the credential was seen (#914).
|
|
@@ -1472,6 +1501,8 @@ export interface ConsumedComboFailure {
|
|
|
1472
1501
|
upstreamCode?: string;
|
|
1473
1502
|
/** Valid numeric/date value used only for cooldown calculation. */
|
|
1474
1503
|
retryAfter?: string;
|
|
1504
|
+
/** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
|
|
1505
|
+
resetAt?: string[];
|
|
1475
1506
|
/** Reserved for 040 usage attribution without adding another body read. */
|
|
1476
1507
|
usage?: OcxUsage;
|
|
1477
1508
|
}
|
|
@@ -1479,6 +1510,8 @@ export interface ConsumedComboFailure {
|
|
|
1479
1510
|
|
|
1480
1511
|
|
|
1481
1512
|
export interface HandleResponsesOptions {
|
|
1513
|
+
/** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
|
|
1514
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
1482
1515
|
turnAdmissionLease?: AdmissionLease;
|
|
1483
1516
|
/**
|
|
1484
1517
|
* How the caller proved data-plane admission (#1686).
|
|
@@ -1588,9 +1621,26 @@ export async function consumeComboFailure(
|
|
|
1588
1621
|
let upstreamCode: string | undefined;
|
|
1589
1622
|
let upstreamMessage: string | undefined;
|
|
1590
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;
|
|
1591
1629
|
try {
|
|
1592
|
-
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
|
+
});
|
|
1593
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
|
+
}
|
|
1594
1644
|
if (body.displaySafe) {
|
|
1595
1645
|
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1596
1646
|
classificationText = normalized.safeText;
|
|
@@ -1611,18 +1661,24 @@ export async function consumeComboFailure(
|
|
|
1611
1661
|
? fallback
|
|
1612
1662
|
: `${fallback}: ${classificationText}`;
|
|
1613
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;
|
|
1614
1670
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1615
1671
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
1616
1672
|
const clientRetryAfter = resolveClientRetryAfter({
|
|
1617
1673
|
status: response.status,
|
|
1618
1674
|
message,
|
|
1619
|
-
upstreamRetryAfter,
|
|
1675
|
+
upstreamRetryAfter: effectiveRetryAfter,
|
|
1620
1676
|
now,
|
|
1621
1677
|
});
|
|
1622
1678
|
const cooldownRetryAfter = resolveClientRetryAfter({
|
|
1623
1679
|
status: response.status,
|
|
1624
1680
|
message,
|
|
1625
|
-
upstreamRetryAfter,
|
|
1681
|
+
upstreamRetryAfter: effectiveRetryAfter,
|
|
1626
1682
|
now,
|
|
1627
1683
|
includeDefault: false,
|
|
1628
1684
|
});
|
|
@@ -1639,6 +1695,14 @@ export async function consumeComboFailure(
|
|
|
1639
1695
|
classificationText,
|
|
1640
1696
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1641
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
|
+
: {}),
|
|
1642
1706
|
...(usage ? { usage } : {}),
|
|
1643
1707
|
};
|
|
1644
1708
|
}
|
|
@@ -1754,6 +1818,31 @@ function unreadableEncryptedAgentTaskResponse(): Response {
|
|
|
1754
1818
|
);
|
|
1755
1819
|
}
|
|
1756
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
|
+
|
|
1757
1846
|
type ResponsesAuthResolution =
|
|
1758
1847
|
| { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
|
|
1759
1848
|
| { ok: false; response: Response };
|
|
@@ -1801,6 +1890,8 @@ async function resolveResponsesCodexAuth(
|
|
|
1801
1890
|
let authCtx: CodexAuthContext;
|
|
1802
1891
|
if (route.codexAccountMode) {
|
|
1803
1892
|
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
1893
|
+
admission: options.admission,
|
|
1894
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
1804
1895
|
accountId: route.codexAccountId,
|
|
1805
1896
|
modelId: route.modelId,
|
|
1806
1897
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
@@ -1829,6 +1920,23 @@ async function resolveResponsesCodexAuth(
|
|
|
1829
1920
|
authCtx = { kind: "main", accountId: null };
|
|
1830
1921
|
options.onCodexAuthContextResolved?.(undefined);
|
|
1831
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);
|
|
1832
1940
|
if (!isCodexAuthContextUsable(authCtx, config)) {
|
|
1833
1941
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
1834
1942
|
return {
|
|
@@ -1839,11 +1947,7 @@ async function resolveResponsesCodexAuth(
|
|
|
1839
1947
|
return {
|
|
1840
1948
|
ok: true,
|
|
1841
1949
|
authCtx,
|
|
1842
|
-
headers
|
|
1843
|
-
substituteMainCredential,
|
|
1844
|
-
signal: options.abortSignal,
|
|
1845
|
-
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1846
|
-
}),
|
|
1950
|
+
headers,
|
|
1847
1951
|
substituteMainCredential,
|
|
1848
1952
|
};
|
|
1849
1953
|
} catch (err) {
|
|
@@ -1886,6 +1990,7 @@ function isTerminalPoolRefreshFailure(error: unknown): boolean {
|
|
|
1886
1990
|
*/
|
|
1887
1991
|
async function refreshPoolForwardAuth(args: {
|
|
1888
1992
|
req: Request;
|
|
1993
|
+
config: OcxConfig;
|
|
1889
1994
|
route: RouteResult;
|
|
1890
1995
|
authCtx: CodexAuthContext & { kind: "pool" };
|
|
1891
1996
|
substituteMainCredential: boolean;
|
|
@@ -1894,7 +1999,7 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1894
1999
|
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1895
2000
|
| { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
|
|
1896
2001
|
> {
|
|
1897
|
-
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
2002
|
+
const { req, config, route, authCtx, substituteMainCredential, options } = args;
|
|
1898
2003
|
try {
|
|
1899
2004
|
const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
|
|
1900
2005
|
rejectedGeneration: authCtx.generation,
|
|
@@ -1932,6 +2037,9 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1932
2037
|
route.codexAccountMode,
|
|
1933
2038
|
);
|
|
1934
2039
|
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
2040
|
+
admission: options.admission,
|
|
2041
|
+
config: options.codexAuthPolicy ?? config,
|
|
2042
|
+
modelId: route.modelId,
|
|
1935
2043
|
substituteMainCredential,
|
|
1936
2044
|
signal: options.abortSignal,
|
|
1937
2045
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
@@ -1958,6 +2066,7 @@ async function refreshPoolForwardAuth(args: {
|
|
|
1958
2066
|
|
|
1959
2067
|
async function refreshNativeMainForwardAuth(args: {
|
|
1960
2068
|
req: Request;
|
|
2069
|
+
config: OcxConfig;
|
|
1961
2070
|
route: RouteResult;
|
|
1962
2071
|
authCtx: CodexAuthContext;
|
|
1963
2072
|
substituteMainCredential: boolean;
|
|
@@ -1966,7 +2075,7 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1966
2075
|
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1967
2076
|
| { ok: false; response: Response }
|
|
1968
2077
|
> {
|
|
1969
|
-
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
2078
|
+
const { req, config, route, authCtx, substituteMainCredential, options } = args;
|
|
1970
2079
|
if (authCtx.kind !== "main-pool") {
|
|
1971
2080
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
|
|
1972
2081
|
}
|
|
@@ -1989,6 +2098,9 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1989
2098
|
route.codexAccountMode,
|
|
1990
2099
|
);
|
|
1991
2100
|
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
2101
|
+
admission: options.admission,
|
|
2102
|
+
config: options.codexAuthPolicy ?? config,
|
|
2103
|
+
modelId: route.modelId,
|
|
1992
2104
|
substituteMainCredential,
|
|
1993
2105
|
signal: options.abortSignal,
|
|
1994
2106
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
@@ -1998,7 +2110,9 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1998
2110
|
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
1999
2111
|
return { ok: false, response: clientCancelledResponse() };
|
|
2000
2112
|
}
|
|
2001
|
-
return { ok: false, response:
|
|
2113
|
+
return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
|
|
2114
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
2115
|
+
}) ?? nativeMainRefreshFailureResponse(error) };
|
|
2002
2116
|
}
|
|
2003
2117
|
}
|
|
2004
2118
|
|
|
@@ -2059,6 +2173,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2059
2173
|
|
|
2060
2174
|
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
2061
2175
|
// this request will actually use (#404).
|
|
2176
|
+
route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
|
|
2062
2177
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2063
2178
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
2064
2179
|
logCtx.model = route.modelId;
|
|
@@ -2290,6 +2405,15 @@ export async function handleComboResponses(
|
|
|
2290
2405
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2291
2406
|
const initialNow = Date.now();
|
|
2292
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
|
+
});
|
|
2293
2417
|
|
|
2294
2418
|
if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
|
|
2295
2419
|
const recovery = agentTaskRecoveryConfig(config);
|
|
@@ -2307,9 +2431,7 @@ export async function handleComboResponses(
|
|
|
2307
2431
|
);
|
|
2308
2432
|
return unreadableEncryptedAgentTaskResponse();
|
|
2309
2433
|
}
|
|
2310
|
-
pick =
|
|
2311
|
-
eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
|
|
2312
|
-
});
|
|
2434
|
+
pick = await pickWithWait({ now: initialNow });
|
|
2313
2435
|
if (!pick) {
|
|
2314
2436
|
discardEncryptedAgentTaskRecovery(
|
|
2315
2437
|
req,
|
|
@@ -2317,7 +2439,9 @@ export async function handleComboResponses(
|
|
|
2317
2439
|
config,
|
|
2318
2440
|
{ parentThreadId: inboundClientThreadId },
|
|
2319
2441
|
);
|
|
2320
|
-
return
|
|
2442
|
+
return options.abortSignal?.aborted
|
|
2443
|
+
? clientCancelledResponse()
|
|
2444
|
+
: comboUnavailable(comboId);
|
|
2321
2445
|
}
|
|
2322
2446
|
let recovered = false;
|
|
2323
2447
|
try {
|
|
@@ -2347,14 +2471,16 @@ export async function handleComboResponses(
|
|
|
2347
2471
|
comboPayloadReadable = true;
|
|
2348
2472
|
comboReplaySnapshot.recoveredPlaintext = true;
|
|
2349
2473
|
} else {
|
|
2350
|
-
pick =
|
|
2351
|
-
eligible:
|
|
2352
|
-
|
|
2474
|
+
pick = await pickWithWait({
|
|
2475
|
+
eligible: payloadEligible,
|
|
2476
|
+
now: initialNow,
|
|
2353
2477
|
});
|
|
2354
2478
|
}
|
|
2355
2479
|
|
|
2356
2480
|
if (!pick) {
|
|
2357
|
-
return
|
|
2481
|
+
return options.abortSignal?.aborted
|
|
2482
|
+
? clientCancelledResponse()
|
|
2483
|
+
: comboUnavailable(comboId);
|
|
2358
2484
|
}
|
|
2359
2485
|
// One immutable combo selection trace, before any child dispatch; child
|
|
2360
2486
|
// adoption below must never replace it with a concrete child route trace.
|
|
@@ -2558,9 +2684,12 @@ export async function handleComboResponses(
|
|
|
2558
2684
|
console.warn(
|
|
2559
2685
|
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
2560
2686
|
);
|
|
2687
|
+
const failureNow = Date.now();
|
|
2561
2688
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
2562
2689
|
retryAfter: failure.retryAfter,
|
|
2563
|
-
|
|
2690
|
+
resetAt: failure.resetAt,
|
|
2691
|
+
cooldownMs: combo.cooldownMs,
|
|
2692
|
+
now: failureNow,
|
|
2564
2693
|
cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
|
|
2565
2694
|
code: failure.upstreamCode,
|
|
2566
2695
|
}),
|
|
@@ -2569,8 +2698,19 @@ export async function handleComboResponses(
|
|
|
2569
2698
|
code: failure.upstreamCode,
|
|
2570
2699
|
message: failure.classificationText,
|
|
2571
2700
|
});
|
|
2572
|
-
if (
|
|
2573
|
-
|
|
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
|
+
}
|
|
2574
2714
|
}
|
|
2575
2715
|
if (
|
|
2576
2716
|
lastFailure?.status === 413
|
|
@@ -2688,7 +2828,13 @@ export async function handleResponses(
|
|
|
2688
2828
|
const ownsBudget = options.translatorBudget === undefined;
|
|
2689
2829
|
const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
|
|
2690
2830
|
try {
|
|
2691
|
-
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
|
+
});
|
|
2692
2838
|
return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
|
|
2693
2839
|
} catch (error) {
|
|
2694
2840
|
if (ownsBudget) translatorBudget.dispose();
|
|
@@ -2725,10 +2871,22 @@ async function handleResponsesInner(
|
|
|
2725
2871
|
}
|
|
2726
2872
|
// An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
|
|
2727
2873
|
// as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
|
|
2728
|
-
const
|
|
2874
|
+
const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
|
|
2729
2875
|
&& typeof (body as { model?: unknown }).model === "string"
|
|
2730
|
-
|
|
2731
|
-
|
|
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
|
+
}
|
|
2732
2890
|
if (comboEffortRow) {
|
|
2733
2891
|
const raw = body as Record<string, unknown>;
|
|
2734
2892
|
raw.model = comboEffortRow.baseId;
|
|
@@ -2794,7 +2952,15 @@ async function handleResponsesInner(
|
|
|
2794
2952
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
2795
2953
|
try {
|
|
2796
2954
|
parsed = parseRequest(body);
|
|
2797
|
-
|
|
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
|
+
}
|
|
2798
2964
|
if (effortRow) {
|
|
2799
2965
|
parsed.modelId = effortRow.baseId;
|
|
2800
2966
|
parsed.options.reasoning = effortRow.effort;
|
|
@@ -3032,6 +3198,7 @@ async function handleResponsesInner(
|
|
|
3032
3198
|
subagentFallbackAccountPreview,
|
|
3033
3199
|
subagentFallbackModelEligibleAccountIdsForModel,
|
|
3034
3200
|
fallbackChain,
|
|
3201
|
+
candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
|
|
3035
3202
|
);
|
|
3036
3203
|
if (fallback) {
|
|
3037
3204
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -3061,7 +3228,8 @@ async function handleResponsesInner(
|
|
|
3061
3228
|
previewSelectionAdmission?.release();
|
|
3062
3229
|
}
|
|
3063
3230
|
|
|
3064
|
-
// Native fallback
|
|
3231
|
+
// Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
|
|
3232
|
+
// so recover only after final route selection.
|
|
3065
3233
|
if (
|
|
3066
3234
|
inboundWire === "responses"
|
|
3067
3235
|
&&
|
|
@@ -3070,6 +3238,7 @@ async function handleResponsesInner(
|
|
|
3070
3238
|
&& agentTaskRecovery
|
|
3071
3239
|
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
3072
3240
|
&& !options.comboAttempt
|
|
3241
|
+
&& !canPassThroughEncryptedV2AgentTask(route, inboundWire)
|
|
3073
3242
|
) {
|
|
3074
3243
|
let recovered = false;
|
|
3075
3244
|
try {
|
|
@@ -3193,9 +3362,16 @@ async function handleResponsesInner(
|
|
|
3193
3362
|
|
|
3194
3363
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
3195
3364
|
|
|
3196
|
-
// Encrypted child tasks may
|
|
3197
|
-
// runs against the FINAL route so native-only fallback can
|
|
3198
|
-
|
|
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
|
+
) {
|
|
3199
3375
|
return unreadableEncryptedAgentTaskResponse();
|
|
3200
3376
|
}
|
|
3201
3377
|
|
|
@@ -3234,6 +3410,12 @@ async function handleResponsesInner(
|
|
|
3234
3410
|
}
|
|
3235
3411
|
|
|
3236
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
|
+
}
|
|
3237
3419
|
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
3238
3420
|
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
3239
3421
|
//
|
|
@@ -3339,7 +3521,10 @@ async function handleResponsesInner(
|
|
|
3339
3521
|
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
3340
3522
|
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
3341
3523
|
*/
|
|
3342
|
-
const applyFailoverSnapshot = (
|
|
3524
|
+
const applyFailoverSnapshot = (
|
|
3525
|
+
snapshot: OAuthAccessSnapshot,
|
|
3526
|
+
retryParsed: OcxParsedRequest = parsed,
|
|
3527
|
+
): boolean => {
|
|
3343
3528
|
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
3344
3529
|
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
3345
3530
|
if (route.providerName === "github-copilot") {
|
|
@@ -3352,7 +3537,14 @@ async function handleResponsesInner(
|
|
|
3352
3537
|
}
|
|
3353
3538
|
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
3354
3539
|
route.provider = rotatedProvider;
|
|
3355
|
-
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
|
+
}
|
|
3356
3548
|
// Re-stamp: a request that rotated accounts must be attributed to the account that actually
|
|
3357
3549
|
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
3358
3550
|
// needed -- and putting it anywhere else would let one of the three drift.
|
|
@@ -3453,6 +3645,14 @@ async function handleResponsesInner(
|
|
|
3453
3645
|
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
3454
3646
|
genericFailoverAccountId = resolved.accountId;
|
|
3455
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
|
+
}
|
|
3456
3656
|
// Captured beside the account it fences, so the two can never disagree.
|
|
3457
3657
|
if (hasPassiveAccountQuota(route.providerName)) {
|
|
3458
3658
|
passiveQuotaWriterGeneration = captureConfigGeneration();
|
|
@@ -3567,6 +3767,17 @@ async function handleResponsesInner(
|
|
|
3567
3767
|
}
|
|
3568
3768
|
const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
|
|
3569
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
|
+
|
|
3570
3781
|
if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
3571
3782
|
return formatErrorResponse(
|
|
3572
3783
|
400,
|
|
@@ -3585,6 +3796,8 @@ async function handleResponsesInner(
|
|
|
3585
3796
|
req.headers,
|
|
3586
3797
|
config,
|
|
3587
3798
|
{
|
|
3799
|
+
admission: options.admission,
|
|
3800
|
+
codexAuthPolicy: options.codexAuthPolicy,
|
|
3588
3801
|
// Account-qualified native routes are passthrough, so their in-turn helper is vision.
|
|
3589
3802
|
// Scope its cooldown and outcome to the helper model, not the routed text model.
|
|
3590
3803
|
...(route.codexAccountId !== undefined
|
|
@@ -3614,11 +3827,12 @@ async function handleResponsesInner(
|
|
|
3614
3827
|
// call must never plan another describe. The flag arrives from the Chat
|
|
3615
3828
|
// surface (whose bridge rebuilds headers) or as the raw header for native
|
|
3616
3829
|
// Responses callers. Marked + text-only routed model → strip, depth cap 1.
|
|
3617
|
-
const visionDescribeTerminal = options.visionDescribeTerminal === true
|
|
3618
|
-
|| req.headers.get("x-opencodex-vision-describe") === "1";
|
|
3830
|
+
const visionDescribeTerminal = options.visionDescribeTerminal === true;
|
|
3619
3831
|
const visionPlan = visionDescribeTerminal
|
|
3620
3832
|
? undefined
|
|
3621
|
-
: 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
|
+
});
|
|
3622
3836
|
const recordSidecarOutcome = openAiSidecar?.recordOutcome;
|
|
3623
3837
|
if (visionPlan) {
|
|
3624
3838
|
await describeImagesInPlace(
|
|
@@ -3793,7 +4007,7 @@ async function handleResponsesInner(
|
|
|
3793
4007
|
// The guard needs a catalog to compare against, so it stands down when the request omits one.
|
|
3794
4008
|
// An explicit empty catalog is still authoritative: it declares that no client tools may be
|
|
3795
4009
|
// called. A passthrough request can legitimately omit `tools` entirely and still receive a call
|
|
3796
|
-
// 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
|
|
3797
4011
|
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
3798
4012
|
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
3799
4013
|
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
@@ -4062,6 +4276,15 @@ async function handleResponsesInner(
|
|
|
4062
4276
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
4063
4277
|
return clientCancelledResponse();
|
|
4064
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
|
+
}
|
|
4065
4288
|
const outcome = classifyTransportFailureKind(err);
|
|
4066
4289
|
// Host-level evidence stands regardless of pool membership: a direct
|
|
4067
4290
|
// forward send has no pool accounting, but the reachability failure is
|
|
@@ -4113,6 +4336,9 @@ async function handleResponsesInner(
|
|
|
4113
4336
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4114
4337
|
providerName: route.providerName,
|
|
4115
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,
|
|
4116
4342
|
}),
|
|
4117
4343
|
route.provider.authMode === "forward")
|
|
4118
4344
|
// Every real attempt response — including an intermediate 5xx the
|
|
@@ -4186,6 +4412,9 @@ async function handleResponsesInner(
|
|
|
4186
4412
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4187
4413
|
providerName: route.providerName,
|
|
4188
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,
|
|
4189
4418
|
}),
|
|
4190
4419
|
route.provider.authMode === "forward")
|
|
4191
4420
|
.then(response => {
|
|
@@ -4215,10 +4444,10 @@ async function handleResponsesInner(
|
|
|
4215
4444
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
|
|
4216
4445
|
const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
|
|
4217
4446
|
const poolReplay = poolAuthCtx
|
|
4218
|
-
? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
|
|
4447
|
+
? await refreshPoolForwardAuth({ req, config, route, authCtx: poolAuthCtx, substituteMainCredential, options })
|
|
4219
4448
|
: undefined;
|
|
4220
4449
|
const replay = poolReplay
|
|
4221
|
-
?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
|
|
4450
|
+
?? await refreshNativeMainForwardAuth({ req, config, route, authCtx, substituteMainCredential, options });
|
|
4222
4451
|
if (!replay.ok) {
|
|
4223
4452
|
// Compact already records this; core historically returned without recording,
|
|
4224
4453
|
// so a dead grant stayed selectable and every request repeated the same doomed
|
|
@@ -4287,6 +4516,9 @@ async function handleResponsesInner(
|
|
|
4287
4516
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4288
4517
|
providerName: route.providerName,
|
|
4289
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,
|
|
4290
4522
|
}),
|
|
4291
4523
|
codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
|
|
4292
4524
|
),
|
|
@@ -4393,6 +4625,9 @@ async function handleResponsesInner(
|
|
|
4393
4625
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4394
4626
|
providerName: route.providerName,
|
|
4395
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,
|
|
4396
4631
|
}),
|
|
4397
4632
|
route.provider.authMode === "forward")
|
|
4398
4633
|
.then(res => {
|
|
@@ -4409,6 +4644,43 @@ async function handleResponsesInner(
|
|
|
4409
4644
|
}
|
|
4410
4645
|
}
|
|
4411
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
|
+
|
|
4412
4684
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
|
|
4413
4685
|
// passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
|
|
4414
4686
|
// key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
|
|
@@ -4455,6 +4727,9 @@ async function handleResponsesInner(
|
|
|
4455
4727
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4456
4728
|
providerName: route.providerName,
|
|
4457
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,
|
|
4458
4733
|
}),
|
|
4459
4734
|
route.provider.authMode === "forward")
|
|
4460
4735
|
.then(res => {
|
|
@@ -4612,11 +4887,10 @@ async function handleResponsesInner(
|
|
|
4612
4887
|
// Prefer primary when present, fall back to secondary for compatibility.
|
|
4613
4888
|
const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
|
|
4614
4889
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
4615
|
-
|
|
4616
|
-
authCtx.accountId,
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
);
|
|
4890
|
+
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
|
|
4891
|
+
applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
|
|
4892
|
+
authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined);
|
|
4893
|
+
}
|
|
4620
4894
|
if (terminalBodyWillRecord) {
|
|
4621
4895
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
4622
4896
|
terminalRecorder(status, httpStatusOverride);
|
|
@@ -4870,7 +5144,10 @@ async function handleResponsesInner(
|
|
|
4870
5144
|
},
|
|
4871
5145
|
onClientCancel: () => options.onNativePassthroughCancel?.(),
|
|
4872
5146
|
onDone: () => unregisterTurn(turnAc),
|
|
4873
|
-
},
|
|
5147
|
+
}, {
|
|
5148
|
+
clientGoneSignal: options.abortSignal,
|
|
5149
|
+
...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
|
|
5150
|
+
});
|
|
4874
5151
|
// When selected, this relay closes response.completed even if upstream
|
|
4875
5152
|
// keeps the connection alive. Marked Codex WS traffic, Windows
|
|
4876
5153
|
// forced-rewrite traffic, and Darwin explicit eager traffic apply
|
|
@@ -4889,7 +5166,10 @@ async function handleResponsesInner(
|
|
|
4889
5166
|
linkAbortSignal(upstream, turnAc.signal);
|
|
4890
5167
|
registerTurn(turnAc, options.turnAdmissionLease);
|
|
4891
5168
|
const inspectionConsumerOptions = {
|
|
4892
|
-
|
|
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,
|
|
4893
5173
|
drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
|
|
4894
5174
|
upstream,
|
|
4895
5175
|
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
@@ -5128,6 +5408,36 @@ async function handleResponsesInner(
|
|
|
5128
5408
|
}
|
|
5129
5409
|
}
|
|
5130
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
|
+
|
|
5131
5441
|
// Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
|
|
5132
5442
|
// Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
|
|
5133
5443
|
// leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
|
|
@@ -5139,7 +5449,9 @@ async function handleResponsesInner(
|
|
|
5139
5449
|
// - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
|
|
5140
5450
|
// can proceed for web-search-only turns
|
|
5141
5451
|
const wsPlan = !routedCompaction
|
|
5142
|
-
? 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
|
+
})
|
|
5143
5455
|
: undefined;
|
|
5144
5456
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
5145
5457
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
@@ -5153,12 +5465,15 @@ async function handleResponsesInner(
|
|
|
5153
5465
|
});
|
|
5154
5466
|
if (rotated) {
|
|
5155
5467
|
route.provider = rotated;
|
|
5156
|
-
} else
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
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
|
+
) {
|
|
5162
5477
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
5163
5478
|
config,
|
|
5164
5479
|
route.providerName,
|
|
@@ -5174,6 +5489,39 @@ async function handleResponsesInner(
|
|
|
5174
5489
|
} catch {
|
|
5175
5490
|
return null;
|
|
5176
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;
|
|
5177
5525
|
}
|
|
5178
5526
|
const rotatedAdapter = resolveAdapter(
|
|
5179
5527
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
@@ -6042,6 +6390,37 @@ async function handleResponsesInner(
|
|
|
6042
6390
|
continue recovery;
|
|
6043
6391
|
}
|
|
6044
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
|
+
|
|
6045
6424
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
|
|
6046
6425
|
// 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
|
|
6047
6426
|
// so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
|
|
@@ -6117,7 +6496,6 @@ async function handleResponsesInner(
|
|
|
6117
6496
|
while (
|
|
6118
6497
|
upstreamResponse.status === 429
|
|
6119
6498
|
&& anthropicPoolAccountId
|
|
6120
|
-
&& isAnthropicAccountPoolEnabled(config)
|
|
6121
6499
|
&& anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
6122
6500
|
) {
|
|
6123
6501
|
const nextAccountId = rotateAnthropicAccountOn429(
|
|
@@ -6528,7 +6906,6 @@ async function handleResponsesInner(
|
|
|
6528
6906
|
if (
|
|
6529
6907
|
response.status === 429
|
|
6530
6908
|
&& anthropicPoolAccountId
|
|
6531
|
-
&& isAnthropicAccountPoolEnabled(config)
|
|
6532
6909
|
&& anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
6533
6910
|
) {
|
|
6534
6911
|
const nextAccountId = rotateAnthropicAccountOn429(
|
|
@@ -6559,6 +6936,48 @@ async function handleResponsesInner(
|
|
|
6559
6936
|
}
|
|
6560
6937
|
}
|
|
6561
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
|
+
}
|
|
6562
6981
|
if (shouldAttemptImageTierRetry({
|
|
6563
6982
|
status: response.status,
|
|
6564
6983
|
adapterName: activeAdapter.name,
|