@yansigit/opencodex 2.33.0 → 2.35.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/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../../config";
|
|
16
16
|
import { parseRequest } from "../../responses/parser";
|
|
17
17
|
import { googleProviderOptionsRouteError } from "../../responses/google-provider-options";
|
|
18
|
+
import { interceptRuntimeFailure } from "../../telemetry/hook";
|
|
18
19
|
import type { ProviderAdapter } from "../../adapters/base";
|
|
19
20
|
import {
|
|
20
21
|
bindReasoningReplayScope,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
|
|
33
34
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
34
35
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
36
|
+
import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
|
|
35
37
|
import {
|
|
36
38
|
copyPreviousResponseReplayProvenance,
|
|
37
39
|
expandPreviousResponseInput,
|
|
@@ -74,6 +76,13 @@ import {
|
|
|
74
76
|
} from "../../combos";
|
|
75
77
|
import { isDebugEnabled, isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
76
78
|
import { debugStreamDiagnostic } from "../../lib/debug";
|
|
79
|
+
import {
|
|
80
|
+
CYBER_POLICY_ERROR_CODE,
|
|
81
|
+
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
82
|
+
adapterFailureFromMessage,
|
|
83
|
+
isCyberPolicyCode,
|
|
84
|
+
isCyberPolicyMessage,
|
|
85
|
+
} from "../../lib/errors";
|
|
77
86
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
78
87
|
import { OcxRequestValidationError } from "../../lib/errors";
|
|
79
88
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
@@ -133,6 +142,13 @@ import {
|
|
|
133
142
|
rotateCursorAccountOnAuth,
|
|
134
143
|
} from "../../oauth/cursor-routing";
|
|
135
144
|
import { getAccountCredential } from "../../oauth/store";
|
|
145
|
+
import {
|
|
146
|
+
failoverAccountSnapshot,
|
|
147
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
148
|
+
isGenericFailoverProvider,
|
|
149
|
+
isGenericOAuthFailoverEnabled,
|
|
150
|
+
rotateGenericOAuthAccountOn429,
|
|
151
|
+
} from "../../oauth/generic-account-failover";
|
|
136
152
|
import { buildWebSearchTool, mediaBridgeWillRun, planWebSearch, resolveCcaInTurnGrounding, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
137
153
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
138
154
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
@@ -157,10 +173,12 @@ import {
|
|
|
157
173
|
} from "../../codex/auth-context";
|
|
158
174
|
import {
|
|
159
175
|
entitledCodexAccountIdsForModel,
|
|
176
|
+
isDirectCallerEntitledToCodexModel,
|
|
160
177
|
invalidateCodexModelEntitlementsForAccount,
|
|
161
178
|
resolveCodexModelEntitlements,
|
|
162
179
|
} from "../../codex/model-entitlements";
|
|
163
180
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
181
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
164
182
|
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
165
183
|
import {
|
|
166
184
|
computeQuotaCooldown,
|
|
@@ -230,6 +248,10 @@ import {
|
|
|
230
248
|
applySubagentModelFallback,
|
|
231
249
|
maybePrimeSubagentQuota,
|
|
232
250
|
recordSubagentQuotaFailureForThreadSpawn,
|
|
251
|
+
resolveSubagentFallbackChain,
|
|
252
|
+
subagentFallbackNeedsModelEntitlements,
|
|
253
|
+
type SubagentModelEligibleAccountIds,
|
|
254
|
+
type SubagentPoolAccountPreview,
|
|
233
255
|
} from "../../codex/subagent-model-fallback";
|
|
234
256
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
235
257
|
import {
|
|
@@ -249,6 +271,7 @@ import {
|
|
|
249
271
|
} from "../request-log";
|
|
250
272
|
import {
|
|
251
273
|
conversationIdFromResponsesRequest,
|
|
274
|
+
inboundClientThreadIdFromRequest,
|
|
252
275
|
normalizeLogConversationId,
|
|
253
276
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
254
277
|
sessionIdHeaderFromRequest,
|
|
@@ -323,7 +346,7 @@ import {
|
|
|
323
346
|
payloadRewriteAsBlockRewrite,
|
|
324
347
|
relaySseWithBlockRewrite,
|
|
325
348
|
} from "../sse-payload-rewrite";
|
|
326
|
-
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
349
|
+
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
327
350
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
328
351
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
329
352
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
@@ -350,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
|
|
|
350
373
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
351
374
|
import {
|
|
352
375
|
emptyCompletionRetryEnabled,
|
|
376
|
+
observeEmptyCompletion,
|
|
353
377
|
guardEmptyCompletionEventStream,
|
|
354
378
|
} from "./empty-completion-guard";
|
|
355
379
|
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
@@ -718,6 +742,48 @@ export async function readDisplaySafeErrorText(
|
|
|
718
742
|
}
|
|
719
743
|
}
|
|
720
744
|
|
|
745
|
+
interface NormalizedUpstreamErrorText {
|
|
746
|
+
safeText: string;
|
|
747
|
+
message?: string;
|
|
748
|
+
type?: string;
|
|
749
|
+
code?: string;
|
|
750
|
+
cyberPolicy: boolean;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Extract the structured provider error envelope without making `error.type` authoritative.
|
|
755
|
+
* Policy identity comes from the dedicated code (or the legacy message fallback); a credible
|
|
756
|
+
* upstream type is only carried through so callers do not erase provider diagnostics.
|
|
757
|
+
*/
|
|
758
|
+
function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedUpstreamErrorText {
|
|
759
|
+
const safeText = redactSecretString(text).slice(0, 500).trim() || fallback;
|
|
760
|
+
let message: string | undefined;
|
|
761
|
+
let type: string | undefined;
|
|
762
|
+
let code: string | undefined;
|
|
763
|
+
try {
|
|
764
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
765
|
+
const response = parsed.response && typeof parsed.response === "object" && !Array.isArray(parsed.response)
|
|
766
|
+
? parsed.response as Record<string, unknown>
|
|
767
|
+
: undefined;
|
|
768
|
+
const candidates = [parsed.error, response?.error, response?.last_error, parsed.last_error, parsed];
|
|
769
|
+
const source = candidates.find((candidate): candidate is Record<string, unknown> => {
|
|
770
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return false;
|
|
771
|
+
const record = candidate as Record<string, unknown>;
|
|
772
|
+
return [record.message, record.type, record.code].some(value => typeof value === "string");
|
|
773
|
+
});
|
|
774
|
+
if (!source) return { safeText, cyberPolicy: isCyberPolicyMessage(safeText) };
|
|
775
|
+
if (typeof source.message === "string" && source.message.trim()) {
|
|
776
|
+
message = redactSecretString(source.message.trim()).slice(0, 500);
|
|
777
|
+
}
|
|
778
|
+
if (typeof source.type === "string" && source.type.trim()) type = source.type.trim();
|
|
779
|
+
if (typeof source.code === "string" && source.code.trim()) code = source.code.trim();
|
|
780
|
+
} catch {
|
|
781
|
+
/* non-JSON upstream body — retain the bounded display-safe text */
|
|
782
|
+
}
|
|
783
|
+
const cyberPolicy = isCyberPolicyCode(code) || isCyberPolicyMessage(message ?? safeText);
|
|
784
|
+
return { safeText, message, type, code, cyberPolicy };
|
|
785
|
+
}
|
|
786
|
+
|
|
721
787
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
722
788
|
parsed._stripReasoningEncryptedContent = true;
|
|
723
789
|
}
|
|
@@ -929,11 +995,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
|
|
|
929
995
|
return undefined;
|
|
930
996
|
}
|
|
931
997
|
|
|
932
|
-
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
|
|
998
|
+
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
|
|
933
999
|
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
934
1000
|
const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
|
|
935
1001
|
if (!wireModel) return;
|
|
936
1002
|
|
|
1003
|
+
if (logCtx) {
|
|
1004
|
+
logCtx.preserveResolvedModelFromRoute = true;
|
|
1005
|
+
delete logCtx.resolvedModel;
|
|
1006
|
+
}
|
|
937
1007
|
parsed.modelId = wireModel;
|
|
938
1008
|
if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
|
|
939
1009
|
const raw = parsed._rawBody as Record<string, unknown>;
|
|
@@ -1223,6 +1293,34 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1223
1293
|
};
|
|
1224
1294
|
}
|
|
1225
1295
|
|
|
1296
|
+
function subagentSpawnFailureMessageForTerminal(
|
|
1297
|
+
status: ResponsesTerminalStatus,
|
|
1298
|
+
httpStatusOverride?: number,
|
|
1299
|
+
logCtx?: RequestLogContext,
|
|
1300
|
+
): string | number | undefined {
|
|
1301
|
+
if (status !== "failed") return undefined;
|
|
1302
|
+
const statusCode = httpStatusOverride ?? logCtx?.terminalHttpStatus;
|
|
1303
|
+
if (statusCode === 429 || statusCode === 402) return statusCode;
|
|
1304
|
+
if (typeof statusCode === "number" && statusCode >= 500 && statusCode < 600) return statusCode;
|
|
1305
|
+
const terminalMessage = logCtx?.upstreamError?.trim();
|
|
1306
|
+
if (terminalMessage) return terminalMessage;
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function recordSubagentSpawnFailureForTerminal(
|
|
1311
|
+
headers: Headers,
|
|
1312
|
+
model: string,
|
|
1313
|
+
status: ResponsesTerminalStatus,
|
|
1314
|
+
config: OcxConfig,
|
|
1315
|
+
accountId: string | null,
|
|
1316
|
+
httpStatusOverride?: number,
|
|
1317
|
+
logCtx?: RequestLogContext,
|
|
1318
|
+
): void {
|
|
1319
|
+
const failureMessage = subagentSpawnFailureMessageForTerminal(status, httpStatusOverride, logCtx);
|
|
1320
|
+
if (failureMessage === undefined) return;
|
|
1321
|
+
recordSubagentQuotaFailureForThreadSpawn(headers, model, failureMessage, config, accountId);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1226
1324
|
|
|
1227
1325
|
|
|
1228
1326
|
export function decodeRequestErrorResponse(err: unknown, label: string): Response {
|
|
@@ -1287,6 +1385,10 @@ export interface HandleResponsesOptions {
|
|
|
1287
1385
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
1288
1386
|
onFirstOutput?: () => void;
|
|
1289
1387
|
onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
|
|
1388
|
+
/** Internal deterministic seam for account-gated native fallback tests. */
|
|
1389
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
1390
|
+
/** Internal deterministic seam for Direct account-gated native auth tests. */
|
|
1391
|
+
isDirectCallerEntitledToCodexModel?: typeof isDirectCallerEntitledToCodexModel;
|
|
1290
1392
|
recordTerminalOutcomes?: boolean;
|
|
1291
1393
|
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
1292
1394
|
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -1370,27 +1472,30 @@ export async function consumeComboFailure(
|
|
|
1370
1472
|
let classificationText = fallback;
|
|
1371
1473
|
let usage: OcxUsage | undefined;
|
|
1372
1474
|
let upstreamCode: string | undefined;
|
|
1475
|
+
let upstreamMessage: string | undefined;
|
|
1476
|
+
let upstreamType: string | undefined;
|
|
1373
1477
|
try {
|
|
1374
1478
|
const body = await readBoundedResponseBody(response, { signal });
|
|
1375
1479
|
usage = usageFromComboFailureText(body.text);
|
|
1376
1480
|
if (body.displaySafe) {
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
|
|
1383
|
-
} catch {
|
|
1384
|
-
/* non-JSON upstream body — message-only classification */
|
|
1385
|
-
}
|
|
1481
|
+
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1482
|
+
classificationText = normalized.safeText;
|
|
1483
|
+
upstreamCode = normalized.code;
|
|
1484
|
+
upstreamMessage = normalized.message;
|
|
1485
|
+
upstreamType = normalized.type;
|
|
1386
1486
|
}
|
|
1387
1487
|
} catch (error) {
|
|
1388
1488
|
if (signal?.aborted) throw error;
|
|
1389
1489
|
classificationText = fallback;
|
|
1390
1490
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1491
|
+
const cyberFailure = isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(classificationText);
|
|
1492
|
+
const normalizedUpstreamCode = cyberFailure ? CYBER_POLICY_ERROR_CODE : upstreamCode;
|
|
1493
|
+
const message = cyberFailure
|
|
1494
|
+
? upstreamMessage
|
|
1495
|
+
?? (isCyberPolicyCode(upstreamCode) ? CYBER_POLICY_FALLBACK_MESSAGE : classificationText)
|
|
1496
|
+
: classificationText === fallback
|
|
1497
|
+
? fallback
|
|
1498
|
+
: `${fallback}: ${classificationText}`;
|
|
1394
1499
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1395
1500
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1396
1501
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
@@ -1408,13 +1513,18 @@ export async function consumeComboFailure(
|
|
|
1408
1513
|
includeDefault: false,
|
|
1409
1514
|
});
|
|
1410
1515
|
return {
|
|
1411
|
-
response: formatErrorResponse(
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1516
|
+
response: formatErrorResponse(
|
|
1517
|
+
response.status,
|
|
1518
|
+
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
1519
|
+
message,
|
|
1520
|
+
{
|
|
1521
|
+
...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
|
|
1522
|
+
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
1523
|
+
},
|
|
1524
|
+
),
|
|
1415
1525
|
classificationText,
|
|
1416
|
-
...(
|
|
1417
|
-
...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1526
|
+
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1527
|
+
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1418
1528
|
...(usage ? { usage } : {}),
|
|
1419
1529
|
};
|
|
1420
1530
|
}
|
|
@@ -1578,6 +1688,8 @@ async function resolveResponsesCodexAuth(
|
|
|
1578
1688
|
modelId: route.modelId,
|
|
1579
1689
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
1580
1690
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1691
|
+
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
1692
|
+
isDirectCallerEntitledToCodexModel: options.isDirectCallerEntitledToCodexModel,
|
|
1581
1693
|
});
|
|
1582
1694
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
1583
1695
|
} else {
|
|
@@ -1616,6 +1728,25 @@ async function resolveResponsesCodexAuth(
|
|
|
1616
1728
|
}
|
|
1617
1729
|
}
|
|
1618
1730
|
|
|
1731
|
+
async function resolveSubagentFallbackModelEligibility(args: {
|
|
1732
|
+
config: OcxConfig;
|
|
1733
|
+
fallbackChain: readonly string[] | null;
|
|
1734
|
+
nativeMainReadsForbidden: boolean;
|
|
1735
|
+
resolver: typeof resolveCodexModelEntitlements;
|
|
1736
|
+
}): Promise<SubagentModelEligibleAccountIds | undefined> {
|
|
1737
|
+
if (!subagentFallbackNeedsModelEntitlements(args.fallbackChain, args.config)) return undefined;
|
|
1738
|
+
const excludeAccountIds = args.nativeMainReadsForbidden
|
|
1739
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
1740
|
+
: undefined;
|
|
1741
|
+
const snapshot = await args.resolver(args.config, { excludeAccountIds });
|
|
1742
|
+
return (modelId) => {
|
|
1743
|
+
const entitledAccountIds = entitledCodexAccountIdsForModel(snapshot, modelId);
|
|
1744
|
+
return entitledAccountIds
|
|
1745
|
+
? new Set([...entitledAccountIds].filter(accountId => !excludeAccountIds?.has(accountId)))
|
|
1746
|
+
: undefined;
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1619
1750
|
/**
|
|
1620
1751
|
* Apply every route-dependent request mutation against the final selected route.
|
|
1621
1752
|
* Must run only after subagent fallback has settled the model/provider.
|
|
@@ -1660,6 +1791,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1660
1791
|
logCtx.provider = route.providerName;
|
|
1661
1792
|
logCtx.providerAdapter = route.provider.adapter;
|
|
1662
1793
|
logCtx.routeDecision = route.routeDecision;
|
|
1794
|
+
if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
|
|
1663
1795
|
|
|
1664
1796
|
if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
|
|
1665
1797
|
parsed.stream = false;
|
|
@@ -1702,7 +1834,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1702
1834
|
);
|
|
1703
1835
|
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
1704
1836
|
const callerTier = parsed.options.serviceTier;
|
|
1705
|
-
|
|
1837
|
+
// The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
|
|
1838
|
+
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
|
|
1839
|
+
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
|
|
1840
|
+
parsed.options.tierObservation = tierObservationContext(
|
|
1841
|
+
fastPolicy,
|
|
1842
|
+
config.fastMode,
|
|
1843
|
+
callerTier,
|
|
1844
|
+
isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
|
|
1845
|
+
);
|
|
1706
1846
|
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
1707
1847
|
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
1708
1848
|
if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
|
|
@@ -1759,6 +1899,19 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1759
1899
|
}
|
|
1760
1900
|
}
|
|
1761
1901
|
|
|
1902
|
+
{
|
|
1903
|
+
const { sanitizeEffortForModel, supportedLadderFor } = await import("../effort-policy");
|
|
1904
|
+
const previousEffort = parsed.options.reasoning;
|
|
1905
|
+
const ladder = supportedLadderFor(route);
|
|
1906
|
+
if (ladder !== undefined && ladder.length === 0 && previousEffort) {
|
|
1907
|
+
sanitizeEffortForModel(parsed, ladder);
|
|
1908
|
+
logCtx.requestedEffort = `${logCtx.requestedEffort ?? previousEffort}->none`;
|
|
1909
|
+
if (isInjectionDebugEnabled()) {
|
|
1910
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: stripped reasoning effort for effortless model`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1762
1915
|
{
|
|
1763
1916
|
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
|
|
1764
1917
|
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
|
|
@@ -1803,7 +1956,7 @@ export async function handleComboResponses(
|
|
|
1803
1956
|
// Expand previous_response_id before image policy and child dispatch so a
|
|
1804
1957
|
// continuation that only references prior images still fails closed when
|
|
1805
1958
|
// imageInput is disabled (and so targets see the full replayed input).
|
|
1806
|
-
const inboundClientThreadId = req.headers
|
|
1959
|
+
const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
|
|
1807
1960
|
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
1808
1961
|
const scopeMismatch = previousResponseScopeMismatch(body);
|
|
1809
1962
|
if (scopeMismatch) {
|
|
@@ -2254,7 +2407,8 @@ async function handleResponsesInner(
|
|
|
2254
2407
|
let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
2255
2408
|
(body as { input?: unknown } | undefined)?.input,
|
|
2256
2409
|
);
|
|
2257
|
-
const inboundClientThreadId = req.headers
|
|
2410
|
+
const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
|
|
2411
|
+
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
2258
2412
|
const originalBody = body;
|
|
2259
2413
|
if (options.comboReplaySnapshot) {
|
|
2260
2414
|
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
@@ -2325,6 +2479,7 @@ async function handleResponsesInner(
|
|
|
2325
2479
|
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
2326
2480
|
}
|
|
2327
2481
|
}
|
|
2482
|
+
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
2328
2483
|
} catch (err) {
|
|
2329
2484
|
if (isTranslatorBudgetExceededError(err)) {
|
|
2330
2485
|
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
@@ -2447,7 +2602,12 @@ async function handleResponsesInner(
|
|
|
2447
2602
|
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
2448
2603
|
// provider with native continuation support below.
|
|
2449
2604
|
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
2450
|
-
const
|
|
2605
|
+
const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
|
|
2606
|
+
? resolveSubagentFallbackChain(parsed, config)
|
|
2607
|
+
: null;
|
|
2608
|
+
const previewSelectionAdmission = threadSpawn
|
|
2609
|
+
&& !options.comboAttempt
|
|
2610
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2451
2611
|
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
2452
2612
|
: undefined;
|
|
2453
2613
|
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
@@ -2459,7 +2619,8 @@ async function handleResponsesInner(
|
|
|
2459
2619
|
};
|
|
2460
2620
|
let selectedForwardHeaders = req.headers;
|
|
2461
2621
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
2462
|
-
let
|
|
2622
|
+
let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
|
|
2623
|
+
let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
|
|
2463
2624
|
let subagentQuotaFailureModel = parsed.modelId;
|
|
2464
2625
|
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
2465
2626
|
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
@@ -2477,28 +2638,47 @@ async function handleResponsesInner(
|
|
|
2477
2638
|
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
2478
2639
|
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
2479
2640
|
// tokens — auth is resolved only after the final route is selected.
|
|
2480
|
-
if (
|
|
2641
|
+
if (
|
|
2642
|
+
threadSpawn
|
|
2643
|
+
&& !options.comboAttempt
|
|
2644
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2645
|
+
) {
|
|
2481
2646
|
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
2482
2647
|
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
2483
2648
|
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
2484
2649
|
// model scope, making the preview diverge from the account that actually authenticates.
|
|
2485
|
-
const
|
|
2650
|
+
const fallbackChain = initialSubagentFallbackChain;
|
|
2651
|
+
subagentFallbackModelEligibleAccountIdsForModel = await resolveSubagentFallbackModelEligibility({
|
|
2652
|
+
config,
|
|
2653
|
+
fallbackChain,
|
|
2654
|
+
nativeMainReadsForbidden,
|
|
2655
|
+
resolver: options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements,
|
|
2656
|
+
});
|
|
2657
|
+
const fallbackNow = Date.now();
|
|
2658
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2486
2659
|
poolAffinityKey,
|
|
2487
2660
|
config,
|
|
2488
|
-
|
|
2489
|
-
codexQuotaScopeForModel(
|
|
2490
|
-
previewSelectionOptions,
|
|
2661
|
+
previewNow,
|
|
2662
|
+
codexQuotaScopeForModel(modelId),
|
|
2663
|
+
{ ...previewSelectionOptions, modelEligibleAccountIds },
|
|
2664
|
+
);
|
|
2665
|
+
const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
|
|
2666
|
+
route.modelId,
|
|
2667
|
+
fallbackNow,
|
|
2668
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(route.modelId),
|
|
2491
2669
|
);
|
|
2492
|
-
subagentFallbackPreviewAccountId = previewAccountId;
|
|
2493
2670
|
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
2494
2671
|
const fallback = applySubagentModelFallback(
|
|
2495
2672
|
parsed,
|
|
2496
2673
|
req.headers,
|
|
2497
2674
|
config,
|
|
2498
2675
|
previewAccountId,
|
|
2499
|
-
|
|
2676
|
+
fallbackNow,
|
|
2500
2677
|
unreadableEncryptedAgentTask,
|
|
2501
2678
|
previewSelectionOptions,
|
|
2679
|
+
subagentFallbackAccountPreview,
|
|
2680
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2681
|
+
fallbackChain,
|
|
2502
2682
|
);
|
|
2503
2683
|
if (fallback) {
|
|
2504
2684
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -2565,6 +2745,7 @@ async function handleResponsesInner(
|
|
|
2565
2745
|
"_cursorConversationId",
|
|
2566
2746
|
"_clientThreadId",
|
|
2567
2747
|
"_promptCacheKeyIsSharedCohort",
|
|
2748
|
+
"_cursorClientThreadId",
|
|
2568
2749
|
"_reasoningReplayScope",
|
|
2569
2750
|
"_cursorIsolateConversation",
|
|
2570
2751
|
];
|
|
@@ -2582,15 +2763,47 @@ async function handleResponsesInner(
|
|
|
2582
2763
|
// The ciphertext-only pass intentionally excludes routed candidates. Once recovery
|
|
2583
2764
|
// makes the assignment readable, run selection again with the full configured chain
|
|
2584
2765
|
// and keep the route in sync with any newly selected fallback.
|
|
2585
|
-
const
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2766
|
+
const recoverySelectionAdmission = codexAccountSelectionForTurn(options.turnAdmissionLease)?.();
|
|
2767
|
+
const fallback = (() => {
|
|
2768
|
+
try {
|
|
2769
|
+
const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
|
|
2770
|
+
const recoverySelectionOptions = {
|
|
2771
|
+
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
2772
|
+
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
2773
|
+
};
|
|
2774
|
+
const recoveryNow = Date.now();
|
|
2775
|
+
// Carry the entitlement filter through recovery too (#2509/#2623). The scope was
|
|
2776
|
+
// already re-previewed per candidate here; the ELIGIBLE-ACCOUNT set was not, so a
|
|
2777
|
+
// recovered assignment could select an account that is not entitled to the model
|
|
2778
|
+
// and then fail closed at final auth — the same class of stale-selection bug as
|
|
2779
|
+
// the quota scope, one layer over.
|
|
2780
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2781
|
+
poolAffinityKey,
|
|
2782
|
+
config,
|
|
2783
|
+
previewNow,
|
|
2784
|
+
codexQuotaScopeForModel(modelId),
|
|
2785
|
+
{ ...recoverySelectionOptions, modelEligibleAccountIds },
|
|
2786
|
+
);
|
|
2787
|
+
const recoveryPreviewAccountId = subagentFallbackAccountPreview(
|
|
2788
|
+
parsed.modelId,
|
|
2789
|
+
recoveryNow,
|
|
2790
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(parsed.modelId),
|
|
2791
|
+
);
|
|
2792
|
+
return applySubagentModelFallback(
|
|
2793
|
+
parsed,
|
|
2794
|
+
req.headers,
|
|
2795
|
+
config,
|
|
2796
|
+
recoveryPreviewAccountId,
|
|
2797
|
+
recoveryNow,
|
|
2798
|
+
false,
|
|
2799
|
+
recoverySelectionOptions,
|
|
2800
|
+
subagentFallbackAccountPreview,
|
|
2801
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2802
|
+
);
|
|
2803
|
+
} finally {
|
|
2804
|
+
recoverySelectionAdmission?.release();
|
|
2805
|
+
}
|
|
2806
|
+
})();
|
|
2594
2807
|
if (fallback) {
|
|
2595
2808
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
2596
2809
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -2713,7 +2926,7 @@ async function handleResponsesInner(
|
|
|
2713
2926
|
}
|
|
2714
2927
|
|
|
2715
2928
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
2716
|
-
applyCodexAccountGatedWireNormalization(parsed, route);
|
|
2929
|
+
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
2717
2930
|
logCtx.provider = route.codexAccountNamespace
|
|
2718
2931
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
2719
2932
|
: formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
@@ -2753,6 +2966,42 @@ async function handleResponsesInner(
|
|
|
2753
2966
|
: undefined;
|
|
2754
2967
|
let cursorPoolAccountId: string | null = null;
|
|
2755
2968
|
let cursorPoolFailovers = 0;
|
|
2969
|
+
// Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
|
|
2970
|
+
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
2971
|
+
let genericFailoverAccountId: string | null = null;
|
|
2972
|
+
let genericFailovers = 0;
|
|
2973
|
+
/**
|
|
2974
|
+
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
2975
|
+
*
|
|
2976
|
+
* One helper for all three rotation sites on purpose. Each site used to inline the same four
|
|
2977
|
+
* lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
|
|
2978
|
+
* metadata paired with it stayed behind.
|
|
2979
|
+
*
|
|
2980
|
+
* Returns false when the snapshot cannot be used safely, and the caller must then abandon the
|
|
2981
|
+
* rotation rather than send a half-applied identity:
|
|
2982
|
+
*
|
|
2983
|
+
* - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
|
|
2984
|
+
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
|
|
2985
|
+
* - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
|
|
2986
|
+
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
2987
|
+
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
2988
|
+
*/
|
|
2989
|
+
const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
|
|
2990
|
+
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
2991
|
+
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
2992
|
+
if (route.providerName === "github-copilot") {
|
|
2993
|
+
rotatedProvider = resolveProviderTransport(
|
|
2994
|
+
route.providerName,
|
|
2995
|
+
rotatedProvider,
|
|
2996
|
+
parsed.options.promptCacheKey,
|
|
2997
|
+
snapshot.apiBaseUrl,
|
|
2998
|
+
) as OcxProviderConfig;
|
|
2999
|
+
}
|
|
3000
|
+
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
3001
|
+
route.provider = rotatedProvider;
|
|
3002
|
+
if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
|
|
3003
|
+
return true;
|
|
3004
|
+
};
|
|
2756
3005
|
const oauthSessionKeyParts = {
|
|
2757
3006
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2758
3007
|
threadIdHeader: req.headers.get("thread-id"),
|
|
@@ -2794,6 +3043,9 @@ async function handleResponsesInner(
|
|
|
2794
3043
|
promoteAnthropicActiveAccount(selection.accountId);
|
|
2795
3044
|
route.provider = { ...route.provider, apiKey: accessToken };
|
|
2796
3045
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
|
|
3046
|
+
if (selection.accountId) {
|
|
3047
|
+
logCtx.accountLogLabel = selection.accountId;
|
|
3048
|
+
}
|
|
2797
3049
|
} else if (
|
|
2798
3050
|
route.providerName === "cursor"
|
|
2799
3051
|
&& route.provider.authMode === "oauth"
|
|
@@ -2824,6 +3076,9 @@ async function handleResponsesInner(
|
|
|
2824
3076
|
};
|
|
2825
3077
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
2826
3078
|
logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
|
|
3079
|
+
if (selection.accountId) {
|
|
3080
|
+
logCtx.accountLogLabel = selection.accountId;
|
|
3081
|
+
}
|
|
2827
3082
|
} else {
|
|
2828
3083
|
let resolved: OAuthAccessSnapshot;
|
|
2829
3084
|
if (isAntigravityOAuth) {
|
|
@@ -2853,6 +3108,14 @@ async function handleResponsesInner(
|
|
|
2853
3108
|
};
|
|
2854
3109
|
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
2855
3110
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
3111
|
+
if (resolved.accountId) {
|
|
3112
|
+
logCtx.accountLogLabel = resolved.accountId;
|
|
3113
|
+
}
|
|
3114
|
+
// Remember which account actually served this request so a 429 cools THAT one, not
|
|
3115
|
+
// whichever account is active by the time the response comes back (#2568).
|
|
3116
|
+
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
3117
|
+
genericFailoverAccountId = resolved.accountId;
|
|
3118
|
+
}
|
|
2856
3119
|
if (route.providerName === "kiro") {
|
|
2857
3120
|
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
2858
3121
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
@@ -2964,6 +3227,7 @@ async function handleResponsesInner(
|
|
|
2964
3227
|
(logCtx.attempts ??= []).push(attempt);
|
|
2965
3228
|
}
|
|
2966
3229
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
3230
|
+
let runTurnAdapter = adapter;
|
|
2967
3231
|
if (adapter.runTurn) {
|
|
2968
3232
|
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
2969
3233
|
}
|
|
@@ -3109,8 +3373,9 @@ async function handleResponsesInner(
|
|
|
3109
3373
|
delete parsed.options.parallelToolCalls;
|
|
3110
3374
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
3111
3375
|
// would force schema-constrained JSON into the synthetic compaction item. The flag and
|
|
3112
|
-
// the raw `text`
|
|
3113
|
-
//
|
|
3376
|
+
// the raw `text` control go too: the key-mode openai-responses adapter builds from
|
|
3377
|
+
// _rawBody, so a surviving format there would still reach the upstream. (The Kiro
|
|
3378
|
+
// guard no longer reads _rawBody.text; it refuses structured output only.)
|
|
3114
3379
|
delete parsed.options.textFormat;
|
|
3115
3380
|
delete parsed._structuredOutput;
|
|
3116
3381
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
@@ -3182,7 +3447,9 @@ async function handleResponsesInner(
|
|
|
3182
3447
|
// the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
|
|
3183
3448
|
// it here escaped every catch up to the Bun handler, so the same request produced an
|
|
3184
3449
|
// unstructured 500 — and no request log — depending only on whether a rotation ran first.
|
|
3185
|
-
|
|
3450
|
+
// Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the
|
|
3451
|
+
// catalog had to drop, so the selector naming it is a client input error, not a 500.
|
|
3452
|
+
if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) {
|
|
3186
3453
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
3187
3454
|
}
|
|
3188
3455
|
throw error;
|
|
@@ -3303,10 +3570,16 @@ async function handleResponsesInner(
|
|
|
3303
3570
|
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
3304
3571
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
3305
3572
|
if (inspectionSawUndeclaredTool) return;
|
|
3573
|
+
const restoredResponse = restoreRoutedCustomCalls(
|
|
3574
|
+
response,
|
|
3575
|
+
routedCustomToolNames,
|
|
3576
|
+
routedCustomToolRepairNames,
|
|
3577
|
+
declaredWireToolNames,
|
|
3578
|
+
).value as { id?: unknown; output?: unknown; status?: unknown };
|
|
3306
3579
|
if (
|
|
3307
3580
|
undeclaredToolGuardActive
|
|
3308
3581
|
&& undeclaredToolCallNameInResponse(
|
|
3309
|
-
|
|
3582
|
+
restoredResponse,
|
|
3310
3583
|
declaredWireToolNames,
|
|
3311
3584
|
declaredNamelessClientCallTypes,
|
|
3312
3585
|
providerExecutedCallTypes,
|
|
@@ -3314,7 +3587,7 @@ async function handleResponsesInner(
|
|
|
3314
3587
|
) {
|
|
3315
3588
|
return;
|
|
3316
3589
|
}
|
|
3317
|
-
rememberPassthroughResponse(
|
|
3590
|
+
rememberPassthroughResponse(restoredResponse);
|
|
3318
3591
|
}
|
|
3319
3592
|
: undefined;
|
|
3320
3593
|
recordAdapterReasoning(logCtx, request);
|
|
@@ -3788,7 +4061,7 @@ async function handleResponsesInner(
|
|
|
3788
4061
|
}
|
|
3789
4062
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
3790
4063
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
3791
|
-
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
4064
|
+
if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
3792
4065
|
if (isUsageDebugEnabled()) {
|
|
3793
4066
|
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
3794
4067
|
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
@@ -3829,18 +4102,15 @@ async function handleResponsesInner(
|
|
|
3829
4102
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
3830
4103
|
terminalRecorder(status, httpStatusOverride);
|
|
3831
4104
|
if (status === "failed") {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|| logCtx.terminalHttpStatus === 402
|
|
3835
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
3836
|
-
: undefined;
|
|
3837
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
3838
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4105
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4106
|
+
recordSubagentSpawnFailureForTerminal(
|
|
3839
4107
|
req.headers,
|
|
3840
4108
|
subagentQuotaFailureModel,
|
|
3841
|
-
|
|
4109
|
+
status,
|
|
3842
4110
|
config,
|
|
3843
4111
|
subagentFallbackAccountId,
|
|
4112
|
+
httpStatusOverride,
|
|
4113
|
+
logCtx,
|
|
3844
4114
|
);
|
|
3845
4115
|
}
|
|
3846
4116
|
}
|
|
@@ -3963,6 +4233,7 @@ async function handleResponsesInner(
|
|
|
3963
4233
|
routedCustomToolNames,
|
|
3964
4234
|
translatorBudget,
|
|
3965
4235
|
routedCustomToolRepairNames,
|
|
4236
|
+
declaredWireToolNames,
|
|
3966
4237
|
)
|
|
3967
4238
|
: undefined,
|
|
3968
4239
|
routedToolSearchNames.size > 0
|
|
@@ -4012,19 +4283,17 @@ async function handleResponsesInner(
|
|
|
4012
4283
|
const reportNativeTerminal = recordTerminalOutcomes
|
|
4013
4284
|
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
4014
4285
|
terminalRecorder?.(status, httpStatusOverride);
|
|
4286
|
+
if (status === "failed") interceptRuntimeFailure(new Error(logCtx.upstreamError ?? "upstream response stream failed"), { provider: route.providerName, model: route.modelId, config: config.autonomousRemediation });
|
|
4015
4287
|
if (status === "failed") {
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|| logCtx.terminalHttpStatus === 402
|
|
4019
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
4020
|
-
: undefined;
|
|
4021
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
4022
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4288
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4289
|
+
recordSubagentSpawnFailureForTerminal(
|
|
4023
4290
|
req.headers,
|
|
4024
4291
|
subagentQuotaFailureModel,
|
|
4025
|
-
|
|
4292
|
+
status,
|
|
4026
4293
|
config,
|
|
4027
4294
|
subagentFallbackAccountId,
|
|
4295
|
+
httpStatusOverride,
|
|
4296
|
+
logCtx,
|
|
4028
4297
|
);
|
|
4029
4298
|
}
|
|
4030
4299
|
}
|
|
@@ -4095,19 +4364,17 @@ async function handleResponsesInner(
|
|
|
4095
4364
|
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
4096
4365
|
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
4097
4366
|
terminalRecorder?.(status, httpStatusOverride);
|
|
4367
|
+
if (status === "failed") interceptRuntimeFailure(new Error(logCtx.upstreamError ?? "upstream response stream failed"), { provider: route.providerName, model: route.modelId, config: config.autonomousRemediation });
|
|
4098
4368
|
if (status === "failed") {
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|| logCtx.terminalHttpStatus === 402
|
|
4102
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
4103
|
-
: undefined;
|
|
4104
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
4105
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4369
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4370
|
+
recordSubagentSpawnFailureForTerminal(
|
|
4106
4371
|
req.headers,
|
|
4107
4372
|
subagentQuotaFailureModel,
|
|
4108
|
-
|
|
4373
|
+
status,
|
|
4109
4374
|
config,
|
|
4110
4375
|
subagentFallbackAccountId,
|
|
4376
|
+
httpStatusOverride,
|
|
4377
|
+
logCtx,
|
|
4111
4378
|
);
|
|
4112
4379
|
}
|
|
4113
4380
|
}
|
|
@@ -4173,6 +4440,7 @@ async function handleResponsesInner(
|
|
|
4173
4440
|
restoredNamespace,
|
|
4174
4441
|
routedCustomToolNames,
|
|
4175
4442
|
routedCustomToolRepairNames,
|
|
4443
|
+
declaredWireToolNames,
|
|
4176
4444
|
);
|
|
4177
4445
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
4178
4446
|
restored,
|
|
@@ -4339,6 +4607,49 @@ async function handleResponsesInner(
|
|
|
4339
4607
|
: undefined;
|
|
4340
4608
|
if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
|
|
4341
4609
|
const canRunWebSearch = webSearchWinsMedia && !ccaInTurnGrounding;
|
|
4610
|
+
const rotateSidecarProviderOn429 = async (retryAfter: string | null): Promise<ProviderAdapter | null> => {
|
|
4611
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4612
|
+
retryAfter,
|
|
4613
|
+
now: Date.now(),
|
|
4614
|
+
attemptedKey: route.provider.apiKey,
|
|
4615
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
4616
|
+
});
|
|
4617
|
+
if (rotated) {
|
|
4618
|
+
route.provider = rotated;
|
|
4619
|
+
} else {
|
|
4620
|
+
if (
|
|
4621
|
+
!genericFailoverAccountId
|
|
4622
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4623
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4624
|
+
) return null;
|
|
4625
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4626
|
+
config,
|
|
4627
|
+
route.providerName,
|
|
4628
|
+
genericFailoverAccountId,
|
|
4629
|
+
retryAfter,
|
|
4630
|
+
);
|
|
4631
|
+
if (!nextAccountId) return null;
|
|
4632
|
+
try {
|
|
4633
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4634
|
+
genericFailoverAccountId = nextAccountId;
|
|
4635
|
+
genericFailovers += 1;
|
|
4636
|
+
if (!applyFailoverSnapshot(snapshot)) return null;
|
|
4637
|
+
} catch {
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
const rotatedAdapter = resolveAdapter(
|
|
4642
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4643
|
+
config.cacheRetention,
|
|
4644
|
+
);
|
|
4645
|
+
bindRouteReasoningReplayScope({
|
|
4646
|
+
parsed,
|
|
4647
|
+
providerName: route.providerName,
|
|
4648
|
+
provider: route.provider,
|
|
4649
|
+
adapterName: rotatedAdapter.name,
|
|
4650
|
+
});
|
|
4651
|
+
return rotatedAdapter;
|
|
4652
|
+
};
|
|
4342
4653
|
if (hasMediaPlan && webSearchWinsMedia) {
|
|
4343
4654
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
4344
4655
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
@@ -4432,27 +4743,7 @@ async function handleResponsesInner(
|
|
|
4432
4743
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
4433
4744
|
}
|
|
4434
4745
|
},
|
|
4435
|
-
on429:
|
|
4436
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4437
|
-
retryAfter,
|
|
4438
|
-
now: Date.now(),
|
|
4439
|
-
attemptedKey: route.provider.apiKey,
|
|
4440
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4441
|
-
});
|
|
4442
|
-
if (!rotated) return null;
|
|
4443
|
-
route.provider = rotated;
|
|
4444
|
-
const rotatedAdapter = resolveAdapter(
|
|
4445
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4446
|
-
config.cacheRetention,
|
|
4447
|
-
);
|
|
4448
|
-
bindRouteReasoningReplayScope({
|
|
4449
|
-
parsed,
|
|
4450
|
-
providerName: route.providerName,
|
|
4451
|
-
provider: route.provider,
|
|
4452
|
-
adapterName: rotatedAdapter.name,
|
|
4453
|
-
});
|
|
4454
|
-
return rotatedAdapter;
|
|
4455
|
-
},
|
|
4746
|
+
on429: rotateSidecarProviderOn429,
|
|
4456
4747
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4457
4748
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
4458
4749
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
@@ -4519,31 +4810,11 @@ async function handleResponsesInner(
|
|
|
4519
4810
|
}
|
|
4520
4811
|
},
|
|
4521
4812
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
4522
|
-
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
4813
|
+
connectTimeoutMs: config.connectTimeoutMs ?? Math.max(200_000, wsPlan.routedModelStallTimeoutMs),
|
|
4523
4814
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
4524
4815
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
4525
4816
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
4526
|
-
on429:
|
|
4527
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4528
|
-
retryAfter,
|
|
4529
|
-
now: Date.now(),
|
|
4530
|
-
attemptedKey: route.provider.apiKey,
|
|
4531
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4532
|
-
});
|
|
4533
|
-
if (!rotated) return null;
|
|
4534
|
-
route.provider = rotated;
|
|
4535
|
-
const rotatedAdapter = resolveAdapter(
|
|
4536
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4537
|
-
config.cacheRetention,
|
|
4538
|
-
);
|
|
4539
|
-
bindRouteReasoningReplayScope({
|
|
4540
|
-
parsed,
|
|
4541
|
-
providerName: route.providerName,
|
|
4542
|
-
provider: route.provider,
|
|
4543
|
-
adapterName: rotatedAdapter.name,
|
|
4544
|
-
});
|
|
4545
|
-
return rotatedAdapter;
|
|
4546
|
-
},
|
|
4817
|
+
on429: rotateSidecarProviderOn429,
|
|
4547
4818
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4548
4819
|
onCompletedResponse: commitReasoningReplayServingRoute,
|
|
4549
4820
|
});
|
|
@@ -4619,7 +4890,7 @@ async function handleResponsesInner(
|
|
|
4619
4890
|
pacingSlotAcquired: true,
|
|
4620
4891
|
},
|
|
4621
4892
|
);
|
|
4622
|
-
await
|
|
4893
|
+
await runTurnAdapter.runTurn?.(
|
|
4623
4894
|
parsed,
|
|
4624
4895
|
{
|
|
4625
4896
|
headers: selectedForwardHeaders,
|
|
@@ -4652,6 +4923,77 @@ async function handleResponsesInner(
|
|
|
4652
4923
|
}
|
|
4653
4924
|
};
|
|
4654
4925
|
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
|
|
4926
|
+
const rotateRunTurnAdapterOnPreflight429 = async (
|
|
4927
|
+
error: Extract<AdapterEvent, { type: "error" }>,
|
|
4928
|
+
): Promise<boolean> => {
|
|
4929
|
+
const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
|
|
4930
|
+
if (
|
|
4931
|
+
status !== 429
|
|
4932
|
+
|| !genericFailoverAccountId
|
|
4933
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4934
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4935
|
+
) return false;
|
|
4936
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4937
|
+
config,
|
|
4938
|
+
route.providerName,
|
|
4939
|
+
genericFailoverAccountId,
|
|
4940
|
+
null,
|
|
4941
|
+
);
|
|
4942
|
+
if (!nextAccountId) return false;
|
|
4943
|
+
try {
|
|
4944
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4945
|
+
genericFailoverAccountId = nextAccountId;
|
|
4946
|
+
genericFailovers += 1;
|
|
4947
|
+
if (!applyFailoverSnapshot(snapshot)) return false;
|
|
4948
|
+
// A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
|
|
4949
|
+
// client-visible bytes, so replay is safe, but carrying its account identity into the next
|
|
4950
|
+
// account would not be. Let the rotated adapter derive a fresh identity and conversation.
|
|
4951
|
+
parsed._cursorIdentityScope = undefined;
|
|
4952
|
+
parsed._cursorConversationId = undefined;
|
|
4953
|
+
if (parsed._providerContinuation?.cursor) {
|
|
4954
|
+
const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
|
|
4955
|
+
parsed._providerContinuation = otherProviderState;
|
|
4956
|
+
}
|
|
4957
|
+
const rotatedProvider = resolveWireProtocolOverride(
|
|
4958
|
+
route.providerName,
|
|
4959
|
+
route.modelId,
|
|
4960
|
+
route.provider,
|
|
4961
|
+
inboundWire,
|
|
4962
|
+
);
|
|
4963
|
+
const rotatedAdapter = resolveAdapter(rotatedProvider, config.cacheRetention);
|
|
4964
|
+
if (!rotatedAdapter.runTurn) return false;
|
|
4965
|
+
runTurnAdapter = rotatedAdapter;
|
|
4966
|
+
bindRouteReasoningReplayScope({
|
|
4967
|
+
parsed,
|
|
4968
|
+
providerName: route.providerName,
|
|
4969
|
+
provider: rotatedProvider,
|
|
4970
|
+
adapterName: rotatedAdapter.name,
|
|
4971
|
+
oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
|
|
4972
|
+
codexAuthContext: authCtx,
|
|
4973
|
+
forwardHeaders: selectedForwardHeaders,
|
|
4974
|
+
});
|
|
4975
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
4976
|
+
return true;
|
|
4977
|
+
} catch {
|
|
4978
|
+
return false;
|
|
4979
|
+
}
|
|
4980
|
+
};
|
|
4981
|
+
const preflightRunTurnFailover = async (
|
|
4982
|
+
firstSource: AsyncIterable<AdapterEvent>,
|
|
4983
|
+
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
4984
|
+
let source = firstSource;
|
|
4985
|
+
while (true) {
|
|
4986
|
+
const preflight = await preflightAdapterEvents(source);
|
|
4987
|
+
if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
4988
|
+
return preflight.stream;
|
|
4989
|
+
}
|
|
4990
|
+
const retryQueue = createAdapterEventQueue({
|
|
4991
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
4992
|
+
});
|
|
4993
|
+
void runTurnAttempt(retryQueue, "oauth-account-429");
|
|
4994
|
+
source = retryQueue.stream();
|
|
4995
|
+
}
|
|
4996
|
+
};
|
|
4655
4997
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
4656
4998
|
// first queue is closed once its attempt settles, and pushing into it after
|
|
4657
4999
|
// close is a silent no-op.
|
|
@@ -4669,6 +5011,11 @@ async function handleResponsesInner(
|
|
|
4669
5011
|
let eventSource: AsyncIterable<AdapterEvent> = diagnoseAdapterEvents(
|
|
4670
5012
|
queue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState,
|
|
4671
5013
|
);
|
|
5014
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
5015
|
+
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
5016
|
+
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
5017
|
+
eventSource = await preflightRunTurnFailover(eventSource);
|
|
5018
|
+
}
|
|
4672
5019
|
if (options.comboAttempt) {
|
|
4673
5020
|
const preflight = await preflightAdapterEvents(eventSource);
|
|
4674
5021
|
if (preflight.error || preflight.empty) {
|
|
@@ -4686,7 +5033,16 @@ async function handleResponsesInner(
|
|
|
4686
5033
|
// signal — run the adapter transport again against a fresh queue.
|
|
4687
5034
|
continuation: runTurnRetrySource,
|
|
4688
5035
|
})
|
|
4689
|
-
:
|
|
5036
|
+
// Guard off (the default): leave the stream alone, but record that the turn ended
|
|
5037
|
+
// empty so the user has something to correlate instead of an unexplained blank
|
|
5038
|
+
// result (#2472). Retrying by default would re-send a turn that may already have had
|
|
5039
|
+
// billable side effects, so the honest default is observability, not recovery.
|
|
5040
|
+
: observeEmptyCompletion(eventSource, () => {
|
|
5041
|
+
console.warn(
|
|
5042
|
+
`[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
|
|
5043
|
+
+ "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
|
|
5044
|
+
);
|
|
5045
|
+
});
|
|
4690
5046
|
const sseStream = bridgeToResponsesSSE(
|
|
4691
5047
|
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
4692
5048
|
() => {
|
|
@@ -4740,15 +5096,22 @@ async function handleResponsesInner(
|
|
|
4740
5096
|
|
|
4741
5097
|
await runTurn();
|
|
4742
5098
|
const firstAttemptEvents = await queue.collect();
|
|
5099
|
+
let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
|
|
5100
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
5101
|
+
runTurnEvents = [];
|
|
5102
|
+
for await (const event of await preflightRunTurnFailover(
|
|
5103
|
+
(async function* () { yield* firstAttemptEvents; })(),
|
|
5104
|
+
)) runTurnEvents.push(event);
|
|
5105
|
+
}
|
|
4743
5106
|
let events: AdapterEvent[];
|
|
4744
5107
|
if (emptyCompletionGuardEnabled) {
|
|
4745
5108
|
events = [];
|
|
4746
5109
|
for await (const event of guardEmptyCompletionEventStream({
|
|
4747
|
-
firstEvents: (async function* () { yield*
|
|
5110
|
+
firstEvents: (async function* () { yield* runTurnEvents; })(),
|
|
4748
5111
|
continuation: runTurnRetrySource,
|
|
4749
5112
|
})) events.push(event);
|
|
4750
5113
|
} else {
|
|
4751
|
-
events =
|
|
5114
|
+
events = runTurnEvents;
|
|
4752
5115
|
}
|
|
4753
5116
|
if (options.comboAttempt) {
|
|
4754
5117
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
@@ -5194,6 +5557,7 @@ async function handleResponsesInner(
|
|
|
5194
5557
|
invalidateSameTargetRequest();
|
|
5195
5558
|
promoteAnthropicActiveAccount(nextAccountId);
|
|
5196
5559
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
5560
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5197
5561
|
activeAdapter = resolveAdapter(
|
|
5198
5562
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5199
5563
|
config.cacheRetention,
|
|
@@ -5252,6 +5616,7 @@ async function handleResponsesInner(
|
|
|
5252
5616
|
forwardHeaders: selectedForwardHeaders,
|
|
5253
5617
|
});
|
|
5254
5618
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
5619
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5255
5620
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5256
5621
|
const result = await rebuildAndRefetch("cursor-oauth-auth");
|
|
5257
5622
|
if ("failed" in result) return result.failed;
|
|
@@ -5297,6 +5662,7 @@ async function handleResponsesInner(
|
|
|
5297
5662
|
forwardHeaders: selectedForwardHeaders,
|
|
5298
5663
|
});
|
|
5299
5664
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
5665
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5300
5666
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5301
5667
|
const result = await rebuildAndRefetch("cursor-oauth-429");
|
|
5302
5668
|
if ("failed" in result) return result.failed;
|
|
@@ -5305,6 +5671,49 @@ async function handleResponsesInner(
|
|
|
5305
5671
|
break;
|
|
5306
5672
|
}
|
|
5307
5673
|
}
|
|
5674
|
+
// Generic OAuth account failover (#2568) for providers with no pool of their own.
|
|
5675
|
+
// Presence is consent since #2568d: rotation is ON by default once two or more eligible
|
|
5676
|
+
// accounts are stored for the provider, because a second deliberate login is read as the
|
|
5677
|
+
// operator asking for it. A single-account install is still a strict no-op, and an
|
|
5678
|
+
// explicit `oauthAccountFailover.enabled: false` (global or per provider) still wins --
|
|
5679
|
+
// see isGenericOAuthFailoverEnabled in src/oauth/generic-account-failover.ts. Codex and
|
|
5680
|
+
// Anthropic are excluded by isGenericFailoverProvider: their pools own quota scopes,
|
|
5681
|
+
// probe leases and affinity that this must not reimplement.
|
|
5682
|
+
while (
|
|
5683
|
+
upstreamResponse.status === 429
|
|
5684
|
+
&& genericFailoverAccountId
|
|
5685
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
5686
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
5687
|
+
) {
|
|
5688
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
5689
|
+
config,
|
|
5690
|
+
route.providerName,
|
|
5691
|
+
genericFailoverAccountId,
|
|
5692
|
+
upstreamResponse.headers.get("retry-after"),
|
|
5693
|
+
);
|
|
5694
|
+
if (!nextAccountId) break;
|
|
5695
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
5696
|
+
try {
|
|
5697
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
|
|
5698
|
+
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
5699
|
+
// would mix one account's credential with another's routing data.
|
|
5700
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
5701
|
+
genericFailoverAccountId = nextAccountId;
|
|
5702
|
+
genericFailovers += 1;
|
|
5703
|
+
if (!applyFailoverSnapshot(snapshot)) break;
|
|
5704
|
+
invalidateSameTargetRequest();
|
|
5705
|
+
activeAdapter = resolveAdapter(
|
|
5706
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5707
|
+
config.cacheRetention,
|
|
5708
|
+
);
|
|
5709
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5710
|
+
const result = await rebuildAndRefetch("oauth-account-429");
|
|
5711
|
+
if ("failed" in result) return result.failed;
|
|
5712
|
+
upstreamResponse = result;
|
|
5713
|
+
} catch {
|
|
5714
|
+
break;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5308
5717
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
5309
5718
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
5310
5719
|
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
@@ -5380,29 +5789,41 @@ async function handleResponsesInner(
|
|
|
5380
5789
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
5381
5790
|
// material before it reaches the client-facing error surface.
|
|
5382
5791
|
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
5383
|
-
const
|
|
5384
|
-
|
|
5385
|
-
|
|
5792
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5793
|
+
const message = normalized.cyberPolicy
|
|
5794
|
+
? normalized.message
|
|
5795
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
5796
|
+
: enrichOpenCodeZenRateLimitMessage(
|
|
5797
|
+
`Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
|
|
5798
|
+
{
|
|
5799
|
+
status: upstreamResponse.status,
|
|
5800
|
+
providerName: route.providerName,
|
|
5801
|
+
baseUrl: route.provider.baseUrl,
|
|
5802
|
+
adapter: route.provider.adapter,
|
|
5803
|
+
authMode: route.provider.authMode,
|
|
5804
|
+
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5805
|
+
upstreamRetryAfter,
|
|
5806
|
+
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
5807
|
+
// never reach enrichOpenCodeZenRateLimitMessage here.
|
|
5808
|
+
supportsHttpSameKeyRetry: true,
|
|
5809
|
+
},
|
|
5810
|
+
);
|
|
5811
|
+
const retryAfter = normalized.cyberPolicy
|
|
5812
|
+
? undefined
|
|
5813
|
+
: resolveClientRetryAfter({
|
|
5386
5814
|
status: upstreamResponse.status,
|
|
5387
|
-
|
|
5388
|
-
baseUrl: route.provider.baseUrl,
|
|
5389
|
-
adapter: route.provider.adapter,
|
|
5390
|
-
authMode: route.provider.authMode,
|
|
5391
|
-
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5815
|
+
message,
|
|
5392
5816
|
upstreamRetryAfter,
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5817
|
+
});
|
|
5818
|
+
return formatErrorResponse(
|
|
5819
|
+
upstreamResponse.status,
|
|
5820
|
+
normalized.cyberPolicy ? (normalized.type ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
5821
|
+
message,
|
|
5822
|
+
{
|
|
5823
|
+
...(normalized.cyberPolicy ? { code: CYBER_POLICY_ERROR_CODE } : {}),
|
|
5824
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
5396
5825
|
},
|
|
5397
5826
|
);
|
|
5398
|
-
const retryAfter = resolveClientRetryAfter({
|
|
5399
|
-
status: upstreamResponse.status,
|
|
5400
|
-
message,
|
|
5401
|
-
upstreamRetryAfter,
|
|
5402
|
-
});
|
|
5403
|
-
return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
|
|
5404
|
-
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
5405
|
-
});
|
|
5406
5827
|
}
|
|
5407
5828
|
}
|
|
5408
5829
|
|
|
@@ -5689,6 +6110,7 @@ async function handleResponsesInner(
|
|
|
5689
6110
|
invalidateSameTargetRequest();
|
|
5690
6111
|
promoteAnthropicActiveAccount(nextAccountId);
|
|
5691
6112
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
6113
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5692
6114
|
activeAdapter = resolveAdapter(
|
|
5693
6115
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5694
6116
|
config.cacheRetention,
|
|
@@ -5751,6 +6173,7 @@ async function handleResponsesInner(
|
|
|
5751
6173
|
adapterName: activeAdapter.name,
|
|
5752
6174
|
});
|
|
5753
6175
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
6176
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5754
6177
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5755
6178
|
nextContinuationRecoveryKind = "cursor-oauth-auth";
|
|
5756
6179
|
continue;
|
|
@@ -5800,6 +6223,7 @@ async function handleResponsesInner(
|
|
|
5800
6223
|
adapterName: activeAdapter.name,
|
|
5801
6224
|
});
|
|
5802
6225
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
6226
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5803
6227
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5804
6228
|
nextContinuationRecoveryKind = "cursor-oauth-429";
|
|
5805
6229
|
continue;
|
|
@@ -5825,10 +6249,21 @@ async function handleResponsesInner(
|
|
|
5825
6249
|
|
|
5826
6250
|
if (!response.ok) {
|
|
5827
6251
|
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
6252
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5828
6253
|
yield {
|
|
5829
6254
|
type: "error",
|
|
5830
|
-
status: response.status,
|
|
5831
|
-
message:
|
|
6255
|
+
status: normalized.cyberPolicy ? 400 : response.status,
|
|
6256
|
+
message: normalized.cyberPolicy
|
|
6257
|
+
? normalized.message
|
|
6258
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
6259
|
+
: `Provider continuation error ${response.status}: ${normalized.safeText}`,
|
|
6260
|
+
...(normalized.cyberPolicy
|
|
6261
|
+
? {
|
|
6262
|
+
errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
|
|
6263
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
6264
|
+
retryable: false,
|
|
6265
|
+
}
|
|
6266
|
+
: {}),
|
|
5832
6267
|
};
|
|
5833
6268
|
return;
|
|
5834
6269
|
}
|