@yansigit/opencodex 2.33.1 → 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 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- 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 +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- 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 +156 -65
- 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 +334 -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 +14 -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 +5 -0
- 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 -37
- 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 +229 -153
- 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 +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- 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 +28 -0
- 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 +73 -14
- package/src/providers/registry.ts +160 -68
- 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 +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -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 +152 -80
- 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 +1 -1
- package/src/server/management/model-routes.ts +266 -0
- 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/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- 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/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 +15 -4
- 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 +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -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 {
|
|
@@ -324,7 +346,7 @@ import {
|
|
|
324
346
|
payloadRewriteAsBlockRewrite,
|
|
325
347
|
relaySseWithBlockRewrite,
|
|
326
348
|
} from "../sse-payload-rewrite";
|
|
327
|
-
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
349
|
+
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
328
350
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
329
351
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
330
352
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
@@ -351,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
|
|
|
351
373
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
352
374
|
import {
|
|
353
375
|
emptyCompletionRetryEnabled,
|
|
376
|
+
observeEmptyCompletion,
|
|
354
377
|
guardEmptyCompletionEventStream,
|
|
355
378
|
} from "./empty-completion-guard";
|
|
356
379
|
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
@@ -719,6 +742,48 @@ export async function readDisplaySafeErrorText(
|
|
|
719
742
|
}
|
|
720
743
|
}
|
|
721
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
|
+
|
|
722
787
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
723
788
|
parsed._stripReasoningEncryptedContent = true;
|
|
724
789
|
}
|
|
@@ -930,11 +995,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
|
|
|
930
995
|
return undefined;
|
|
931
996
|
}
|
|
932
997
|
|
|
933
|
-
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
|
|
998
|
+
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
|
|
934
999
|
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
935
1000
|
const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
|
|
936
1001
|
if (!wireModel) return;
|
|
937
1002
|
|
|
1003
|
+
if (logCtx) {
|
|
1004
|
+
logCtx.preserveResolvedModelFromRoute = true;
|
|
1005
|
+
delete logCtx.resolvedModel;
|
|
1006
|
+
}
|
|
938
1007
|
parsed.modelId = wireModel;
|
|
939
1008
|
if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
|
|
940
1009
|
const raw = parsed._rawBody as Record<string, unknown>;
|
|
@@ -1224,6 +1293,34 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1224
1293
|
};
|
|
1225
1294
|
}
|
|
1226
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
|
+
|
|
1227
1324
|
|
|
1228
1325
|
|
|
1229
1326
|
export function decodeRequestErrorResponse(err: unknown, label: string): Response {
|
|
@@ -1288,6 +1385,10 @@ export interface HandleResponsesOptions {
|
|
|
1288
1385
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
1289
1386
|
onFirstOutput?: () => void;
|
|
1290
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;
|
|
1291
1392
|
recordTerminalOutcomes?: boolean;
|
|
1292
1393
|
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
1293
1394
|
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -1371,27 +1472,30 @@ export async function consumeComboFailure(
|
|
|
1371
1472
|
let classificationText = fallback;
|
|
1372
1473
|
let usage: OcxUsage | undefined;
|
|
1373
1474
|
let upstreamCode: string | undefined;
|
|
1475
|
+
let upstreamMessage: string | undefined;
|
|
1476
|
+
let upstreamType: string | undefined;
|
|
1374
1477
|
try {
|
|
1375
1478
|
const body = await readBoundedResponseBody(response, { signal });
|
|
1376
1479
|
usage = usageFromComboFailureText(body.text);
|
|
1377
1480
|
if (body.displaySafe) {
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
|
|
1384
|
-
} catch {
|
|
1385
|
-
/* non-JSON upstream body — message-only classification */
|
|
1386
|
-
}
|
|
1481
|
+
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1482
|
+
classificationText = normalized.safeText;
|
|
1483
|
+
upstreamCode = normalized.code;
|
|
1484
|
+
upstreamMessage = normalized.message;
|
|
1485
|
+
upstreamType = normalized.type;
|
|
1387
1486
|
}
|
|
1388
1487
|
} catch (error) {
|
|
1389
1488
|
if (signal?.aborted) throw error;
|
|
1390
1489
|
classificationText = fallback;
|
|
1391
1490
|
}
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
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}`;
|
|
1395
1499
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1396
1500
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1397
1501
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
@@ -1409,13 +1513,18 @@ export async function consumeComboFailure(
|
|
|
1409
1513
|
includeDefault: false,
|
|
1410
1514
|
});
|
|
1411
1515
|
return {
|
|
1412
|
-
response: formatErrorResponse(
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
+
),
|
|
1416
1525
|
classificationText,
|
|
1417
|
-
...(
|
|
1418
|
-
...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1526
|
+
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1527
|
+
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1419
1528
|
...(usage ? { usage } : {}),
|
|
1420
1529
|
};
|
|
1421
1530
|
}
|
|
@@ -1579,6 +1688,8 @@ async function resolveResponsesCodexAuth(
|
|
|
1579
1688
|
modelId: route.modelId,
|
|
1580
1689
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
1581
1690
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1691
|
+
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
1692
|
+
isDirectCallerEntitledToCodexModel: options.isDirectCallerEntitledToCodexModel,
|
|
1582
1693
|
});
|
|
1583
1694
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
1584
1695
|
} else {
|
|
@@ -1617,6 +1728,25 @@ async function resolveResponsesCodexAuth(
|
|
|
1617
1728
|
}
|
|
1618
1729
|
}
|
|
1619
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
|
+
|
|
1620
1750
|
/**
|
|
1621
1751
|
* Apply every route-dependent request mutation against the final selected route.
|
|
1622
1752
|
* Must run only after subagent fallback has settled the model/provider.
|
|
@@ -1661,6 +1791,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1661
1791
|
logCtx.provider = route.providerName;
|
|
1662
1792
|
logCtx.providerAdapter = route.provider.adapter;
|
|
1663
1793
|
logCtx.routeDecision = route.routeDecision;
|
|
1794
|
+
if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
|
|
1664
1795
|
|
|
1665
1796
|
if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
|
|
1666
1797
|
parsed.stream = false;
|
|
@@ -1703,7 +1834,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1703
1834
|
);
|
|
1704
1835
|
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
1705
1836
|
const callerTier = parsed.options.serviceTier;
|
|
1706
|
-
|
|
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
|
+
);
|
|
1707
1846
|
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
1708
1847
|
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
1709
1848
|
if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
|
|
@@ -1760,6 +1899,19 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1760
1899
|
}
|
|
1761
1900
|
}
|
|
1762
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
|
+
|
|
1763
1915
|
{
|
|
1764
1916
|
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
|
|
1765
1917
|
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
|
|
@@ -2256,6 +2408,7 @@ async function handleResponsesInner(
|
|
|
2256
2408
|
(body as { input?: unknown } | undefined)?.input,
|
|
2257
2409
|
);
|
|
2258
2410
|
const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
|
|
2411
|
+
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
2259
2412
|
const originalBody = body;
|
|
2260
2413
|
if (options.comboReplaySnapshot) {
|
|
2261
2414
|
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
@@ -2326,6 +2479,7 @@ async function handleResponsesInner(
|
|
|
2326
2479
|
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
2327
2480
|
}
|
|
2328
2481
|
}
|
|
2482
|
+
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
2329
2483
|
} catch (err) {
|
|
2330
2484
|
if (isTranslatorBudgetExceededError(err)) {
|
|
2331
2485
|
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
@@ -2448,7 +2602,12 @@ async function handleResponsesInner(
|
|
|
2448
2602
|
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
2449
2603
|
// provider with native continuation support below.
|
|
2450
2604
|
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
2451
|
-
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)
|
|
2452
2611
|
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
2453
2612
|
: undefined;
|
|
2454
2613
|
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
@@ -2460,7 +2619,8 @@ async function handleResponsesInner(
|
|
|
2460
2619
|
};
|
|
2461
2620
|
let selectedForwardHeaders = req.headers;
|
|
2462
2621
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
2463
|
-
let
|
|
2622
|
+
let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
|
|
2623
|
+
let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
|
|
2464
2624
|
let subagentQuotaFailureModel = parsed.modelId;
|
|
2465
2625
|
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
2466
2626
|
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
@@ -2478,28 +2638,47 @@ async function handleResponsesInner(
|
|
|
2478
2638
|
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
2479
2639
|
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
2480
2640
|
// tokens — auth is resolved only after the final route is selected.
|
|
2481
|
-
if (
|
|
2641
|
+
if (
|
|
2642
|
+
threadSpawn
|
|
2643
|
+
&& !options.comboAttempt
|
|
2644
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2645
|
+
) {
|
|
2482
2646
|
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
2483
2647
|
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
2484
2648
|
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
2485
2649
|
// model scope, making the preview diverge from the account that actually authenticates.
|
|
2486
|
-
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(
|
|
2487
2659
|
poolAffinityKey,
|
|
2488
2660
|
config,
|
|
2489
|
-
|
|
2490
|
-
codexQuotaScopeForModel(
|
|
2491
|
-
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),
|
|
2492
2669
|
);
|
|
2493
|
-
subagentFallbackPreviewAccountId = previewAccountId;
|
|
2494
2670
|
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
2495
2671
|
const fallback = applySubagentModelFallback(
|
|
2496
2672
|
parsed,
|
|
2497
2673
|
req.headers,
|
|
2498
2674
|
config,
|
|
2499
2675
|
previewAccountId,
|
|
2500
|
-
|
|
2676
|
+
fallbackNow,
|
|
2501
2677
|
unreadableEncryptedAgentTask,
|
|
2502
2678
|
previewSelectionOptions,
|
|
2679
|
+
subagentFallbackAccountPreview,
|
|
2680
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2681
|
+
fallbackChain,
|
|
2503
2682
|
);
|
|
2504
2683
|
if (fallback) {
|
|
2505
2684
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -2566,6 +2745,7 @@ async function handleResponsesInner(
|
|
|
2566
2745
|
"_cursorConversationId",
|
|
2567
2746
|
"_clientThreadId",
|
|
2568
2747
|
"_promptCacheKeyIsSharedCohort",
|
|
2748
|
+
"_cursorClientThreadId",
|
|
2569
2749
|
"_reasoningReplayScope",
|
|
2570
2750
|
"_cursorIsolateConversation",
|
|
2571
2751
|
];
|
|
@@ -2583,15 +2763,47 @@ async function handleResponsesInner(
|
|
|
2583
2763
|
// The ciphertext-only pass intentionally excludes routed candidates. Once recovery
|
|
2584
2764
|
// makes the assignment readable, run selection again with the full configured chain
|
|
2585
2765
|
// and keep the route in sync with any newly selected fallback.
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
+
})();
|
|
2595
2807
|
if (fallback) {
|
|
2596
2808
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
2597
2809
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -2714,7 +2926,7 @@ async function handleResponsesInner(
|
|
|
2714
2926
|
}
|
|
2715
2927
|
|
|
2716
2928
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
2717
|
-
applyCodexAccountGatedWireNormalization(parsed, route);
|
|
2929
|
+
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
2718
2930
|
logCtx.provider = route.codexAccountNamespace
|
|
2719
2931
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
2720
2932
|
: formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
@@ -2754,6 +2966,42 @@ async function handleResponsesInner(
|
|
|
2754
2966
|
: undefined;
|
|
2755
2967
|
let cursorPoolAccountId: string | null = null;
|
|
2756
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
|
+
};
|
|
2757
3005
|
const oauthSessionKeyParts = {
|
|
2758
3006
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2759
3007
|
threadIdHeader: req.headers.get("thread-id"),
|
|
@@ -2795,6 +3043,9 @@ async function handleResponsesInner(
|
|
|
2795
3043
|
promoteAnthropicActiveAccount(selection.accountId);
|
|
2796
3044
|
route.provider = { ...route.provider, apiKey: accessToken };
|
|
2797
3045
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
|
|
3046
|
+
if (selection.accountId) {
|
|
3047
|
+
logCtx.accountLogLabel = selection.accountId;
|
|
3048
|
+
}
|
|
2798
3049
|
} else if (
|
|
2799
3050
|
route.providerName === "cursor"
|
|
2800
3051
|
&& route.provider.authMode === "oauth"
|
|
@@ -2825,6 +3076,9 @@ async function handleResponsesInner(
|
|
|
2825
3076
|
};
|
|
2826
3077
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
2827
3078
|
logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
|
|
3079
|
+
if (selection.accountId) {
|
|
3080
|
+
logCtx.accountLogLabel = selection.accountId;
|
|
3081
|
+
}
|
|
2828
3082
|
} else {
|
|
2829
3083
|
let resolved: OAuthAccessSnapshot;
|
|
2830
3084
|
if (isAntigravityOAuth) {
|
|
@@ -2854,6 +3108,14 @@ async function handleResponsesInner(
|
|
|
2854
3108
|
};
|
|
2855
3109
|
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
2856
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
|
+
}
|
|
2857
3119
|
if (route.providerName === "kiro") {
|
|
2858
3120
|
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
2859
3121
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
@@ -2965,6 +3227,7 @@ async function handleResponsesInner(
|
|
|
2965
3227
|
(logCtx.attempts ??= []).push(attempt);
|
|
2966
3228
|
}
|
|
2967
3229
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
3230
|
+
let runTurnAdapter = adapter;
|
|
2968
3231
|
if (adapter.runTurn) {
|
|
2969
3232
|
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
2970
3233
|
}
|
|
@@ -3110,8 +3373,9 @@ async function handleResponsesInner(
|
|
|
3110
3373
|
delete parsed.options.parallelToolCalls;
|
|
3111
3374
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
3112
3375
|
// would force schema-constrained JSON into the synthetic compaction item. The flag and
|
|
3113
|
-
// the raw `text`
|
|
3114
|
-
//
|
|
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.)
|
|
3115
3379
|
delete parsed.options.textFormat;
|
|
3116
3380
|
delete parsed._structuredOutput;
|
|
3117
3381
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
@@ -3183,7 +3447,9 @@ async function handleResponsesInner(
|
|
|
3183
3447
|
// the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
|
|
3184
3448
|
// it here escaped every catch up to the Bun handler, so the same request produced an
|
|
3185
3449
|
// unstructured 500 — and no request log — depending only on whether a rotation ran first.
|
|
3186
|
-
|
|
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) {
|
|
3187
3453
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
3188
3454
|
}
|
|
3189
3455
|
throw error;
|
|
@@ -3304,10 +3570,16 @@ async function handleResponsesInner(
|
|
|
3304
3570
|
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
3305
3571
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
3306
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 };
|
|
3307
3579
|
if (
|
|
3308
3580
|
undeclaredToolGuardActive
|
|
3309
3581
|
&& undeclaredToolCallNameInResponse(
|
|
3310
|
-
|
|
3582
|
+
restoredResponse,
|
|
3311
3583
|
declaredWireToolNames,
|
|
3312
3584
|
declaredNamelessClientCallTypes,
|
|
3313
3585
|
providerExecutedCallTypes,
|
|
@@ -3315,7 +3587,7 @@ async function handleResponsesInner(
|
|
|
3315
3587
|
) {
|
|
3316
3588
|
return;
|
|
3317
3589
|
}
|
|
3318
|
-
rememberPassthroughResponse(
|
|
3590
|
+
rememberPassthroughResponse(restoredResponse);
|
|
3319
3591
|
}
|
|
3320
3592
|
: undefined;
|
|
3321
3593
|
recordAdapterReasoning(logCtx, request);
|
|
@@ -3789,7 +4061,7 @@ async function handleResponsesInner(
|
|
|
3789
4061
|
}
|
|
3790
4062
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
3791
4063
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
3792
|
-
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
4064
|
+
if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
3793
4065
|
if (isUsageDebugEnabled()) {
|
|
3794
4066
|
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
3795
4067
|
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
@@ -3830,18 +4102,15 @@ async function handleResponsesInner(
|
|
|
3830
4102
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
3831
4103
|
terminalRecorder(status, httpStatusOverride);
|
|
3832
4104
|
if (status === "failed") {
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|| logCtx.terminalHttpStatus === 402
|
|
3836
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
3837
|
-
: undefined;
|
|
3838
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
3839
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4105
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4106
|
+
recordSubagentSpawnFailureForTerminal(
|
|
3840
4107
|
req.headers,
|
|
3841
4108
|
subagentQuotaFailureModel,
|
|
3842
|
-
|
|
4109
|
+
status,
|
|
3843
4110
|
config,
|
|
3844
4111
|
subagentFallbackAccountId,
|
|
4112
|
+
httpStatusOverride,
|
|
4113
|
+
logCtx,
|
|
3845
4114
|
);
|
|
3846
4115
|
}
|
|
3847
4116
|
}
|
|
@@ -3964,6 +4233,7 @@ async function handleResponsesInner(
|
|
|
3964
4233
|
routedCustomToolNames,
|
|
3965
4234
|
translatorBudget,
|
|
3966
4235
|
routedCustomToolRepairNames,
|
|
4236
|
+
declaredWireToolNames,
|
|
3967
4237
|
)
|
|
3968
4238
|
: undefined,
|
|
3969
4239
|
routedToolSearchNames.size > 0
|
|
@@ -4013,19 +4283,17 @@ async function handleResponsesInner(
|
|
|
4013
4283
|
const reportNativeTerminal = recordTerminalOutcomes
|
|
4014
4284
|
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
4015
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 });
|
|
4016
4287
|
if (status === "failed") {
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|| logCtx.terminalHttpStatus === 402
|
|
4020
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
4021
|
-
: undefined;
|
|
4022
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
4023
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4288
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4289
|
+
recordSubagentSpawnFailureForTerminal(
|
|
4024
4290
|
req.headers,
|
|
4025
4291
|
subagentQuotaFailureModel,
|
|
4026
|
-
|
|
4292
|
+
status,
|
|
4027
4293
|
config,
|
|
4028
4294
|
subagentFallbackAccountId,
|
|
4295
|
+
httpStatusOverride,
|
|
4296
|
+
logCtx,
|
|
4029
4297
|
);
|
|
4030
4298
|
}
|
|
4031
4299
|
}
|
|
@@ -4096,19 +4364,17 @@ async function handleResponsesInner(
|
|
|
4096
4364
|
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
4097
4365
|
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
4098
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 });
|
|
4099
4368
|
if (status === "failed") {
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|| logCtx.terminalHttpStatus === 402
|
|
4103
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
4104
|
-
: undefined;
|
|
4105
|
-
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
4106
|
-
recordSubagentQuotaFailureForThreadSpawn(
|
|
4369
|
+
if (!isFixedCodexAccount(authCtx)) {
|
|
4370
|
+
recordSubagentSpawnFailureForTerminal(
|
|
4107
4371
|
req.headers,
|
|
4108
4372
|
subagentQuotaFailureModel,
|
|
4109
|
-
|
|
4373
|
+
status,
|
|
4110
4374
|
config,
|
|
4111
4375
|
subagentFallbackAccountId,
|
|
4376
|
+
httpStatusOverride,
|
|
4377
|
+
logCtx,
|
|
4112
4378
|
);
|
|
4113
4379
|
}
|
|
4114
4380
|
}
|
|
@@ -4174,6 +4440,7 @@ async function handleResponsesInner(
|
|
|
4174
4440
|
restoredNamespace,
|
|
4175
4441
|
routedCustomToolNames,
|
|
4176
4442
|
routedCustomToolRepairNames,
|
|
4443
|
+
declaredWireToolNames,
|
|
4177
4444
|
);
|
|
4178
4445
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
4179
4446
|
restored,
|
|
@@ -4340,6 +4607,49 @@ async function handleResponsesInner(
|
|
|
4340
4607
|
: undefined;
|
|
4341
4608
|
if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
|
|
4342
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
|
+
};
|
|
4343
4653
|
if (hasMediaPlan && webSearchWinsMedia) {
|
|
4344
4654
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
4345
4655
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
@@ -4433,27 +4743,7 @@ async function handleResponsesInner(
|
|
|
4433
4743
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
4434
4744
|
}
|
|
4435
4745
|
},
|
|
4436
|
-
on429:
|
|
4437
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4438
|
-
retryAfter,
|
|
4439
|
-
now: Date.now(),
|
|
4440
|
-
attemptedKey: route.provider.apiKey,
|
|
4441
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4442
|
-
});
|
|
4443
|
-
if (!rotated) return null;
|
|
4444
|
-
route.provider = rotated;
|
|
4445
|
-
const rotatedAdapter = resolveAdapter(
|
|
4446
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4447
|
-
config.cacheRetention,
|
|
4448
|
-
);
|
|
4449
|
-
bindRouteReasoningReplayScope({
|
|
4450
|
-
parsed,
|
|
4451
|
-
providerName: route.providerName,
|
|
4452
|
-
provider: route.provider,
|
|
4453
|
-
adapterName: rotatedAdapter.name,
|
|
4454
|
-
});
|
|
4455
|
-
return rotatedAdapter;
|
|
4456
|
-
},
|
|
4746
|
+
on429: rotateSidecarProviderOn429,
|
|
4457
4747
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4458
4748
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
4459
4749
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
@@ -4524,27 +4814,7 @@ async function handleResponsesInner(
|
|
|
4524
4814
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
4525
4815
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
4526
4816
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
4527
|
-
on429:
|
|
4528
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4529
|
-
retryAfter,
|
|
4530
|
-
now: Date.now(),
|
|
4531
|
-
attemptedKey: route.provider.apiKey,
|
|
4532
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4533
|
-
});
|
|
4534
|
-
if (!rotated) return null;
|
|
4535
|
-
route.provider = rotated;
|
|
4536
|
-
const rotatedAdapter = resolveAdapter(
|
|
4537
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4538
|
-
config.cacheRetention,
|
|
4539
|
-
);
|
|
4540
|
-
bindRouteReasoningReplayScope({
|
|
4541
|
-
parsed,
|
|
4542
|
-
providerName: route.providerName,
|
|
4543
|
-
provider: route.provider,
|
|
4544
|
-
adapterName: rotatedAdapter.name,
|
|
4545
|
-
});
|
|
4546
|
-
return rotatedAdapter;
|
|
4547
|
-
},
|
|
4817
|
+
on429: rotateSidecarProviderOn429,
|
|
4548
4818
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4549
4819
|
onCompletedResponse: commitReasoningReplayServingRoute,
|
|
4550
4820
|
});
|
|
@@ -4620,7 +4890,7 @@ async function handleResponsesInner(
|
|
|
4620
4890
|
pacingSlotAcquired: true,
|
|
4621
4891
|
},
|
|
4622
4892
|
);
|
|
4623
|
-
await
|
|
4893
|
+
await runTurnAdapter.runTurn?.(
|
|
4624
4894
|
parsed,
|
|
4625
4895
|
{
|
|
4626
4896
|
headers: selectedForwardHeaders,
|
|
@@ -4653,6 +4923,77 @@ async function handleResponsesInner(
|
|
|
4653
4923
|
}
|
|
4654
4924
|
};
|
|
4655
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
|
+
};
|
|
4656
4997
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
4657
4998
|
// first queue is closed once its attempt settles, and pushing into it after
|
|
4658
4999
|
// close is a silent no-op.
|
|
@@ -4670,6 +5011,11 @@ async function handleResponsesInner(
|
|
|
4670
5011
|
let eventSource: AsyncIterable<AdapterEvent> = diagnoseAdapterEvents(
|
|
4671
5012
|
queue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState,
|
|
4672
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
|
+
}
|
|
4673
5019
|
if (options.comboAttempt) {
|
|
4674
5020
|
const preflight = await preflightAdapterEvents(eventSource);
|
|
4675
5021
|
if (preflight.error || preflight.empty) {
|
|
@@ -4687,7 +5033,16 @@ async function handleResponsesInner(
|
|
|
4687
5033
|
// signal — run the adapter transport again against a fresh queue.
|
|
4688
5034
|
continuation: runTurnRetrySource,
|
|
4689
5035
|
})
|
|
4690
|
-
:
|
|
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
|
+
});
|
|
4691
5046
|
const sseStream = bridgeToResponsesSSE(
|
|
4692
5047
|
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
4693
5048
|
() => {
|
|
@@ -4741,15 +5096,22 @@ async function handleResponsesInner(
|
|
|
4741
5096
|
|
|
4742
5097
|
await runTurn();
|
|
4743
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
|
+
}
|
|
4744
5106
|
let events: AdapterEvent[];
|
|
4745
5107
|
if (emptyCompletionGuardEnabled) {
|
|
4746
5108
|
events = [];
|
|
4747
5109
|
for await (const event of guardEmptyCompletionEventStream({
|
|
4748
|
-
firstEvents: (async function* () { yield*
|
|
5110
|
+
firstEvents: (async function* () { yield* runTurnEvents; })(),
|
|
4749
5111
|
continuation: runTurnRetrySource,
|
|
4750
5112
|
})) events.push(event);
|
|
4751
5113
|
} else {
|
|
4752
|
-
events =
|
|
5114
|
+
events = runTurnEvents;
|
|
4753
5115
|
}
|
|
4754
5116
|
if (options.comboAttempt) {
|
|
4755
5117
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
@@ -5195,6 +5557,7 @@ async function handleResponsesInner(
|
|
|
5195
5557
|
invalidateSameTargetRequest();
|
|
5196
5558
|
promoteAnthropicActiveAccount(nextAccountId);
|
|
5197
5559
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
5560
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5198
5561
|
activeAdapter = resolveAdapter(
|
|
5199
5562
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5200
5563
|
config.cacheRetention,
|
|
@@ -5253,6 +5616,7 @@ async function handleResponsesInner(
|
|
|
5253
5616
|
forwardHeaders: selectedForwardHeaders,
|
|
5254
5617
|
});
|
|
5255
5618
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
5619
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5256
5620
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5257
5621
|
const result = await rebuildAndRefetch("cursor-oauth-auth");
|
|
5258
5622
|
if ("failed" in result) return result.failed;
|
|
@@ -5298,6 +5662,7 @@ async function handleResponsesInner(
|
|
|
5298
5662
|
forwardHeaders: selectedForwardHeaders,
|
|
5299
5663
|
});
|
|
5300
5664
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
5665
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5301
5666
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5302
5667
|
const result = await rebuildAndRefetch("cursor-oauth-429");
|
|
5303
5668
|
if ("failed" in result) return result.failed;
|
|
@@ -5306,6 +5671,49 @@ async function handleResponsesInner(
|
|
|
5306
5671
|
break;
|
|
5307
5672
|
}
|
|
5308
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
|
+
}
|
|
5309
5717
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
5310
5718
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
5311
5719
|
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
@@ -5381,29 +5789,41 @@ async function handleResponsesInner(
|
|
|
5381
5789
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
5382
5790
|
// material before it reaches the client-facing error surface.
|
|
5383
5791
|
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
5384
|
-
const
|
|
5385
|
-
|
|
5386
|
-
|
|
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({
|
|
5387
5814
|
status: upstreamResponse.status,
|
|
5388
|
-
|
|
5389
|
-
baseUrl: route.provider.baseUrl,
|
|
5390
|
-
adapter: route.provider.adapter,
|
|
5391
|
-
authMode: route.provider.authMode,
|
|
5392
|
-
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5815
|
+
message,
|
|
5393
5816
|
upstreamRetryAfter,
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
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 } : {}),
|
|
5397
5825
|
},
|
|
5398
5826
|
);
|
|
5399
|
-
const retryAfter = resolveClientRetryAfter({
|
|
5400
|
-
status: upstreamResponse.status,
|
|
5401
|
-
message,
|
|
5402
|
-
upstreamRetryAfter,
|
|
5403
|
-
});
|
|
5404
|
-
return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
|
|
5405
|
-
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
5406
|
-
});
|
|
5407
5827
|
}
|
|
5408
5828
|
}
|
|
5409
5829
|
|
|
@@ -5690,6 +6110,7 @@ async function handleResponsesInner(
|
|
|
5690
6110
|
invalidateSameTargetRequest();
|
|
5691
6111
|
promoteAnthropicActiveAccount(nextAccountId);
|
|
5692
6112
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
6113
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5693
6114
|
activeAdapter = resolveAdapter(
|
|
5694
6115
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5695
6116
|
config.cacheRetention,
|
|
@@ -5752,6 +6173,7 @@ async function handleResponsesInner(
|
|
|
5752
6173
|
adapterName: activeAdapter.name,
|
|
5753
6174
|
});
|
|
5754
6175
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
6176
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5755
6177
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5756
6178
|
nextContinuationRecoveryKind = "cursor-oauth-auth";
|
|
5757
6179
|
continue;
|
|
@@ -5801,6 +6223,7 @@ async function handleResponsesInner(
|
|
|
5801
6223
|
adapterName: activeAdapter.name,
|
|
5802
6224
|
});
|
|
5803
6225
|
logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
|
|
6226
|
+
logCtx.accountLogLabel = nextAccountId;
|
|
5804
6227
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5805
6228
|
nextContinuationRecoveryKind = "cursor-oauth-429";
|
|
5806
6229
|
continue;
|
|
@@ -5826,10 +6249,21 @@ async function handleResponsesInner(
|
|
|
5826
6249
|
|
|
5827
6250
|
if (!response.ok) {
|
|
5828
6251
|
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
6252
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5829
6253
|
yield {
|
|
5830
6254
|
type: "error",
|
|
5831
|
-
status: response.status,
|
|
5832
|
-
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
|
+
: {}),
|
|
5833
6267
|
};
|
|
5834
6268
|
return;
|
|
5835
6269
|
}
|