@bitkyc08/opencodex 2.33.0 → 2.34.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-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.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 +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 +162 -0
- 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-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- 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/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- 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 +32 -4
- package/src/cli/account-extended.ts +1 -1
- 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/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/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 +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- 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 +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- 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/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- 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/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/types.ts +15 -0
- 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 +65 -7
- 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 +4 -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/index.ts +77 -11
- 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/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- 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 +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- 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-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/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
|
|
31
31
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
32
32
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
33
|
+
import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
|
|
33
34
|
import {
|
|
34
35
|
copyPreviousResponseReplayProvenance,
|
|
35
36
|
expandPreviousResponseInput,
|
|
@@ -71,6 +72,13 @@ import {
|
|
|
71
72
|
targetKey,
|
|
72
73
|
} from "../../combos";
|
|
73
74
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
75
|
+
import {
|
|
76
|
+
CYBER_POLICY_ERROR_CODE,
|
|
77
|
+
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
78
|
+
adapterFailureFromMessage,
|
|
79
|
+
isCyberPolicyCode,
|
|
80
|
+
isCyberPolicyMessage,
|
|
81
|
+
} from "../../lib/errors";
|
|
74
82
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
75
83
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
76
84
|
import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
|
|
@@ -107,6 +115,13 @@ import {
|
|
|
107
115
|
resolveAnthropicAccountForSession,
|
|
108
116
|
rotateAnthropicAccountOn429,
|
|
109
117
|
} from "../../oauth/anthropic-routing";
|
|
118
|
+
import {
|
|
119
|
+
failoverAccountSnapshot,
|
|
120
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
121
|
+
isGenericFailoverProvider,
|
|
122
|
+
isGenericOAuthFailoverEnabled,
|
|
123
|
+
rotateGenericOAuthAccountOn429,
|
|
124
|
+
} from "../../oauth/generic-account-failover";
|
|
110
125
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
111
126
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
112
127
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
@@ -135,6 +150,7 @@ import {
|
|
|
135
150
|
resolveCodexModelEntitlements,
|
|
136
151
|
} from "../../codex/model-entitlements";
|
|
137
152
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
153
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
138
154
|
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
139
155
|
import {
|
|
140
156
|
computeQuotaCooldown,
|
|
@@ -183,7 +199,7 @@ import {
|
|
|
183
199
|
providerModelResponsesUpstreamStreaming,
|
|
184
200
|
type InboundWire,
|
|
185
201
|
} from "../../providers/registry";
|
|
186
|
-
import type { AdapterRequest } from "../../adapters/base";
|
|
202
|
+
import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
|
|
187
203
|
import {
|
|
188
204
|
hasKeyPoolFailover,
|
|
189
205
|
rateLimitRetryDelayMs,
|
|
@@ -203,6 +219,10 @@ import {
|
|
|
203
219
|
applySubagentModelFallback,
|
|
204
220
|
maybePrimeSubagentQuota,
|
|
205
221
|
recordSubagentQuotaFailureForThreadSpawn,
|
|
222
|
+
resolveSubagentFallbackChain,
|
|
223
|
+
subagentFallbackNeedsModelEntitlements,
|
|
224
|
+
type SubagentModelEligibleAccountIds,
|
|
225
|
+
type SubagentPoolAccountPreview,
|
|
206
226
|
} from "../../codex/subagent-model-fallback";
|
|
207
227
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
208
228
|
import {
|
|
@@ -295,7 +315,7 @@ import {
|
|
|
295
315
|
payloadRewriteAsBlockRewrite,
|
|
296
316
|
relaySseWithBlockRewrite,
|
|
297
317
|
} from "../sse-payload-rewrite";
|
|
298
|
-
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
318
|
+
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
299
319
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
300
320
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
301
321
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
@@ -322,6 +342,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
|
|
|
322
342
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
323
343
|
import {
|
|
324
344
|
emptyCompletionRetryEnabled,
|
|
345
|
+
observeEmptyCompletion,
|
|
325
346
|
guardEmptyCompletionEventStream,
|
|
326
347
|
} from "./empty-completion-guard";
|
|
327
348
|
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
@@ -662,6 +683,48 @@ export async function readDisplaySafeErrorText(
|
|
|
662
683
|
}
|
|
663
684
|
}
|
|
664
685
|
|
|
686
|
+
interface NormalizedUpstreamErrorText {
|
|
687
|
+
safeText: string;
|
|
688
|
+
message?: string;
|
|
689
|
+
type?: string;
|
|
690
|
+
code?: string;
|
|
691
|
+
cyberPolicy: boolean;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Extract the structured provider error envelope without making `error.type` authoritative.
|
|
696
|
+
* Policy identity comes from the dedicated code (or the legacy message fallback); a credible
|
|
697
|
+
* upstream type is only carried through so callers do not erase provider diagnostics.
|
|
698
|
+
*/
|
|
699
|
+
function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedUpstreamErrorText {
|
|
700
|
+
const safeText = redactSecretString(text).slice(0, 500).trim() || fallback;
|
|
701
|
+
let message: string | undefined;
|
|
702
|
+
let type: string | undefined;
|
|
703
|
+
let code: string | undefined;
|
|
704
|
+
try {
|
|
705
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
706
|
+
const response = parsed.response && typeof parsed.response === "object" && !Array.isArray(parsed.response)
|
|
707
|
+
? parsed.response as Record<string, unknown>
|
|
708
|
+
: undefined;
|
|
709
|
+
const candidates = [parsed.error, response?.error, response?.last_error, parsed.last_error, parsed];
|
|
710
|
+
const source = candidates.find((candidate): candidate is Record<string, unknown> => {
|
|
711
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return false;
|
|
712
|
+
const record = candidate as Record<string, unknown>;
|
|
713
|
+
return [record.message, record.type, record.code].some(value => typeof value === "string");
|
|
714
|
+
});
|
|
715
|
+
if (!source) return { safeText, cyberPolicy: isCyberPolicyMessage(safeText) };
|
|
716
|
+
if (typeof source.message === "string" && source.message.trim()) {
|
|
717
|
+
message = redactSecretString(source.message.trim()).slice(0, 500);
|
|
718
|
+
}
|
|
719
|
+
if (typeof source.type === "string" && source.type.trim()) type = source.type.trim();
|
|
720
|
+
if (typeof source.code === "string" && source.code.trim()) code = source.code.trim();
|
|
721
|
+
} catch {
|
|
722
|
+
/* non-JSON upstream body — retain the bounded display-safe text */
|
|
723
|
+
}
|
|
724
|
+
const cyberPolicy = isCyberPolicyCode(code) || isCyberPolicyMessage(message ?? safeText);
|
|
725
|
+
return { safeText, message, type, code, cyberPolicy };
|
|
726
|
+
}
|
|
727
|
+
|
|
665
728
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
666
729
|
parsed._stripReasoningEncryptedContent = true;
|
|
667
730
|
}
|
|
@@ -873,11 +936,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
|
|
|
873
936
|
return undefined;
|
|
874
937
|
}
|
|
875
938
|
|
|
876
|
-
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
|
|
939
|
+
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
|
|
877
940
|
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
878
941
|
const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
|
|
879
942
|
if (!wireModel) return;
|
|
880
943
|
|
|
944
|
+
if (logCtx) {
|
|
945
|
+
logCtx.preserveResolvedModelFromRoute = true;
|
|
946
|
+
delete logCtx.resolvedModel;
|
|
947
|
+
}
|
|
881
948
|
parsed.modelId = wireModel;
|
|
882
949
|
if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
|
|
883
950
|
const raw = parsed._rawBody as Record<string, unknown>;
|
|
@@ -1229,6 +1296,8 @@ export interface HandleResponsesOptions {
|
|
|
1229
1296
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
1230
1297
|
onFirstOutput?: () => void;
|
|
1231
1298
|
onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
|
|
1299
|
+
/** Internal deterministic seam for account-gated native fallback tests. */
|
|
1300
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
1232
1301
|
recordTerminalOutcomes?: boolean;
|
|
1233
1302
|
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
1234
1303
|
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -1312,27 +1381,30 @@ export async function consumeComboFailure(
|
|
|
1312
1381
|
let classificationText = fallback;
|
|
1313
1382
|
let usage: OcxUsage | undefined;
|
|
1314
1383
|
let upstreamCode: string | undefined;
|
|
1384
|
+
let upstreamMessage: string | undefined;
|
|
1385
|
+
let upstreamType: string | undefined;
|
|
1315
1386
|
try {
|
|
1316
1387
|
const body = await readBoundedResponseBody(response, { signal });
|
|
1317
1388
|
usage = usageFromComboFailureText(body.text);
|
|
1318
1389
|
if (body.displaySafe) {
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
|
|
1325
|
-
} catch {
|
|
1326
|
-
/* non-JSON upstream body — message-only classification */
|
|
1327
|
-
}
|
|
1390
|
+
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1391
|
+
classificationText = normalized.safeText;
|
|
1392
|
+
upstreamCode = normalized.code;
|
|
1393
|
+
upstreamMessage = normalized.message;
|
|
1394
|
+
upstreamType = normalized.type;
|
|
1328
1395
|
}
|
|
1329
1396
|
} catch (error) {
|
|
1330
1397
|
if (signal?.aborted) throw error;
|
|
1331
1398
|
classificationText = fallback;
|
|
1332
1399
|
}
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1400
|
+
const cyberFailure = isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(classificationText);
|
|
1401
|
+
const normalizedUpstreamCode = cyberFailure ? CYBER_POLICY_ERROR_CODE : upstreamCode;
|
|
1402
|
+
const message = cyberFailure
|
|
1403
|
+
? upstreamMessage
|
|
1404
|
+
?? (isCyberPolicyCode(upstreamCode) ? CYBER_POLICY_FALLBACK_MESSAGE : classificationText)
|
|
1405
|
+
: classificationText === fallback
|
|
1406
|
+
? fallback
|
|
1407
|
+
: `${fallback}: ${classificationText}`;
|
|
1336
1408
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1337
1409
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1338
1410
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
@@ -1350,13 +1422,18 @@ export async function consumeComboFailure(
|
|
|
1350
1422
|
includeDefault: false,
|
|
1351
1423
|
});
|
|
1352
1424
|
return {
|
|
1353
|
-
response: formatErrorResponse(
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1425
|
+
response: formatErrorResponse(
|
|
1426
|
+
response.status,
|
|
1427
|
+
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
1428
|
+
message,
|
|
1429
|
+
{
|
|
1430
|
+
...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
|
|
1431
|
+
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
1432
|
+
},
|
|
1433
|
+
),
|
|
1357
1434
|
classificationText,
|
|
1358
|
-
...(
|
|
1359
|
-
...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1435
|
+
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1436
|
+
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1360
1437
|
...(usage ? { usage } : {}),
|
|
1361
1438
|
};
|
|
1362
1439
|
}
|
|
@@ -1520,6 +1597,7 @@ async function resolveResponsesCodexAuth(
|
|
|
1520
1597
|
modelId: route.modelId,
|
|
1521
1598
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
1522
1599
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1600
|
+
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
1523
1601
|
});
|
|
1524
1602
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
1525
1603
|
} else {
|
|
@@ -1558,6 +1636,25 @@ async function resolveResponsesCodexAuth(
|
|
|
1558
1636
|
}
|
|
1559
1637
|
}
|
|
1560
1638
|
|
|
1639
|
+
async function resolveSubagentFallbackModelEligibility(args: {
|
|
1640
|
+
config: OcxConfig;
|
|
1641
|
+
fallbackChain: readonly string[] | null;
|
|
1642
|
+
nativeMainReadsForbidden: boolean;
|
|
1643
|
+
resolver: typeof resolveCodexModelEntitlements;
|
|
1644
|
+
}): Promise<SubagentModelEligibleAccountIds | undefined> {
|
|
1645
|
+
if (!subagentFallbackNeedsModelEntitlements(args.fallbackChain, args.config)) return undefined;
|
|
1646
|
+
const excludeAccountIds = args.nativeMainReadsForbidden
|
|
1647
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
1648
|
+
: undefined;
|
|
1649
|
+
const snapshot = await args.resolver(args.config, { excludeAccountIds });
|
|
1650
|
+
return (modelId) => {
|
|
1651
|
+
const entitledAccountIds = entitledCodexAccountIdsForModel(snapshot, modelId);
|
|
1652
|
+
return entitledAccountIds
|
|
1653
|
+
? new Set([...entitledAccountIds].filter(accountId => !excludeAccountIds?.has(accountId)))
|
|
1654
|
+
: undefined;
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1561
1658
|
/**
|
|
1562
1659
|
* Apply every route-dependent request mutation against the final selected route.
|
|
1563
1660
|
* Must run only after subagent fallback has settled the model/provider.
|
|
@@ -1602,6 +1699,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1602
1699
|
logCtx.provider = route.providerName;
|
|
1603
1700
|
logCtx.providerAdapter = route.provider.adapter;
|
|
1604
1701
|
logCtx.routeDecision = route.routeDecision;
|
|
1702
|
+
if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
|
|
1605
1703
|
|
|
1606
1704
|
if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
|
|
1607
1705
|
parsed.stream = false;
|
|
@@ -1644,7 +1742,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1644
1742
|
);
|
|
1645
1743
|
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
1646
1744
|
const callerTier = parsed.options.serviceTier;
|
|
1647
|
-
|
|
1745
|
+
// The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
|
|
1746
|
+
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
|
|
1747
|
+
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
|
|
1748
|
+
parsed.options.tierObservation = tierObservationContext(
|
|
1749
|
+
fastPolicy,
|
|
1750
|
+
config.fastMode,
|
|
1751
|
+
callerTier,
|
|
1752
|
+
isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
|
|
1753
|
+
);
|
|
1648
1754
|
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
1649
1755
|
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
1650
1756
|
if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
|
|
@@ -2184,6 +2290,7 @@ async function handleResponsesInner(
|
|
|
2184
2290
|
(body as { input?: unknown } | undefined)?.input,
|
|
2185
2291
|
);
|
|
2186
2292
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
2293
|
+
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
2187
2294
|
const originalBody = body;
|
|
2188
2295
|
if (options.comboReplaySnapshot) {
|
|
2189
2296
|
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
@@ -2253,6 +2360,7 @@ async function handleResponsesInner(
|
|
|
2253
2360
|
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
2254
2361
|
}
|
|
2255
2362
|
}
|
|
2363
|
+
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
2256
2364
|
} catch (err) {
|
|
2257
2365
|
if (isTranslatorBudgetExceededError(err)) {
|
|
2258
2366
|
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
@@ -2354,7 +2462,12 @@ async function handleResponsesInner(
|
|
|
2354
2462
|
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
2355
2463
|
// provider with native continuation support below.
|
|
2356
2464
|
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
2357
|
-
const
|
|
2465
|
+
const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
|
|
2466
|
+
? resolveSubagentFallbackChain(parsed, config)
|
|
2467
|
+
: null;
|
|
2468
|
+
const previewSelectionAdmission = threadSpawn
|
|
2469
|
+
&& !options.comboAttempt
|
|
2470
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2358
2471
|
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
2359
2472
|
: undefined;
|
|
2360
2473
|
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
@@ -2366,7 +2479,8 @@ async function handleResponsesInner(
|
|
|
2366
2479
|
};
|
|
2367
2480
|
let selectedForwardHeaders = req.headers;
|
|
2368
2481
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
2369
|
-
let
|
|
2482
|
+
let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
|
|
2483
|
+
let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
|
|
2370
2484
|
let subagentQuotaFailureModel = parsed.modelId;
|
|
2371
2485
|
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
2372
2486
|
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
@@ -2384,28 +2498,47 @@ async function handleResponsesInner(
|
|
|
2384
2498
|
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
2385
2499
|
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
2386
2500
|
// tokens — auth is resolved only after the final route is selected.
|
|
2387
|
-
if (
|
|
2501
|
+
if (
|
|
2502
|
+
threadSpawn
|
|
2503
|
+
&& !options.comboAttempt
|
|
2504
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2505
|
+
) {
|
|
2388
2506
|
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
2389
2507
|
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
2390
2508
|
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
2391
2509
|
// model scope, making the preview diverge from the account that actually authenticates.
|
|
2392
|
-
const
|
|
2510
|
+
const fallbackChain = initialSubagentFallbackChain;
|
|
2511
|
+
subagentFallbackModelEligibleAccountIdsForModel = await resolveSubagentFallbackModelEligibility({
|
|
2512
|
+
config,
|
|
2513
|
+
fallbackChain,
|
|
2514
|
+
nativeMainReadsForbidden,
|
|
2515
|
+
resolver: options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements,
|
|
2516
|
+
});
|
|
2517
|
+
const fallbackNow = Date.now();
|
|
2518
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2393
2519
|
poolAffinityKey,
|
|
2394
2520
|
config,
|
|
2395
|
-
|
|
2396
|
-
codexQuotaScopeForModel(
|
|
2397
|
-
previewSelectionOptions,
|
|
2521
|
+
previewNow,
|
|
2522
|
+
codexQuotaScopeForModel(modelId),
|
|
2523
|
+
{ ...previewSelectionOptions, modelEligibleAccountIds },
|
|
2524
|
+
);
|
|
2525
|
+
const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
|
|
2526
|
+
route.modelId,
|
|
2527
|
+
fallbackNow,
|
|
2528
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(route.modelId),
|
|
2398
2529
|
);
|
|
2399
|
-
subagentFallbackPreviewAccountId = previewAccountId;
|
|
2400
2530
|
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
2401
2531
|
const fallback = applySubagentModelFallback(
|
|
2402
2532
|
parsed,
|
|
2403
2533
|
req.headers,
|
|
2404
2534
|
config,
|
|
2405
2535
|
previewAccountId,
|
|
2406
|
-
|
|
2536
|
+
fallbackNow,
|
|
2407
2537
|
unreadableEncryptedAgentTask,
|
|
2408
2538
|
previewSelectionOptions,
|
|
2539
|
+
subagentFallbackAccountPreview,
|
|
2540
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2541
|
+
fallbackChain,
|
|
2409
2542
|
);
|
|
2410
2543
|
if (fallback) {
|
|
2411
2544
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -2471,6 +2604,7 @@ async function handleResponsesInner(
|
|
|
2471
2604
|
"_providerContinuationOwner",
|
|
2472
2605
|
"_cursorConversationId",
|
|
2473
2606
|
"_clientThreadId",
|
|
2607
|
+
"_cursorClientThreadId",
|
|
2474
2608
|
"_reasoningReplayScope",
|
|
2475
2609
|
"_cursorIsolateConversation",
|
|
2476
2610
|
];
|
|
@@ -2488,15 +2622,47 @@ async function handleResponsesInner(
|
|
|
2488
2622
|
// The ciphertext-only pass intentionally excludes routed candidates. Once recovery
|
|
2489
2623
|
// makes the assignment readable, run selection again with the full configured chain
|
|
2490
2624
|
// and keep the route in sync with any newly selected fallback.
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2625
|
+
const recoverySelectionAdmission = codexAccountSelectionForTurn(options.turnAdmissionLease)?.();
|
|
2626
|
+
const fallback = (() => {
|
|
2627
|
+
try {
|
|
2628
|
+
const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
|
|
2629
|
+
const recoverySelectionOptions = {
|
|
2630
|
+
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
2631
|
+
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
2632
|
+
};
|
|
2633
|
+
const recoveryNow = Date.now();
|
|
2634
|
+
// Carry the entitlement filter through recovery too (#2509/#2623). The scope was
|
|
2635
|
+
// already re-previewed per candidate here; the ELIGIBLE-ACCOUNT set was not, so a
|
|
2636
|
+
// recovered assignment could select an account that is not entitled to the model
|
|
2637
|
+
// and then fail closed at final auth — the same class of stale-selection bug as
|
|
2638
|
+
// the quota scope, one layer over.
|
|
2639
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2640
|
+
poolAffinityKey,
|
|
2641
|
+
config,
|
|
2642
|
+
previewNow,
|
|
2643
|
+
codexQuotaScopeForModel(modelId),
|
|
2644
|
+
{ ...recoverySelectionOptions, modelEligibleAccountIds },
|
|
2645
|
+
);
|
|
2646
|
+
const recoveryPreviewAccountId = subagentFallbackAccountPreview(
|
|
2647
|
+
parsed.modelId,
|
|
2648
|
+
recoveryNow,
|
|
2649
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(parsed.modelId),
|
|
2650
|
+
);
|
|
2651
|
+
return applySubagentModelFallback(
|
|
2652
|
+
parsed,
|
|
2653
|
+
req.headers,
|
|
2654
|
+
config,
|
|
2655
|
+
recoveryPreviewAccountId,
|
|
2656
|
+
recoveryNow,
|
|
2657
|
+
false,
|
|
2658
|
+
recoverySelectionOptions,
|
|
2659
|
+
subagentFallbackAccountPreview,
|
|
2660
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2661
|
+
);
|
|
2662
|
+
} finally {
|
|
2663
|
+
recoverySelectionAdmission?.release();
|
|
2664
|
+
}
|
|
2665
|
+
})();
|
|
2500
2666
|
if (fallback) {
|
|
2501
2667
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
2502
2668
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -2619,7 +2785,7 @@ async function handleResponsesInner(
|
|
|
2619
2785
|
}
|
|
2620
2786
|
|
|
2621
2787
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
2622
|
-
applyCodexAccountGatedWireNormalization(parsed, route);
|
|
2788
|
+
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
2623
2789
|
logCtx.provider = route.codexAccountNamespace
|
|
2624
2790
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
2625
2791
|
: formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
@@ -2641,6 +2807,42 @@ async function handleResponsesInner(
|
|
|
2641
2807
|
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
2642
2808
|
let anthropicPoolAccountId: string | null = null;
|
|
2643
2809
|
let anthropicPoolFailovers = 0;
|
|
2810
|
+
// Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
|
|
2811
|
+
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
2812
|
+
let genericFailoverAccountId: string | null = null;
|
|
2813
|
+
let genericFailovers = 0;
|
|
2814
|
+
/**
|
|
2815
|
+
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
2816
|
+
*
|
|
2817
|
+
* One helper for all three rotation sites on purpose. Each site used to inline the same four
|
|
2818
|
+
* lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
|
|
2819
|
+
* metadata paired with it stayed behind.
|
|
2820
|
+
*
|
|
2821
|
+
* Returns false when the snapshot cannot be used safely, and the caller must then abandon the
|
|
2822
|
+
* rotation rather than send a half-applied identity:
|
|
2823
|
+
*
|
|
2824
|
+
* - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
|
|
2825
|
+
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
|
|
2826
|
+
* - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
|
|
2827
|
+
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
2828
|
+
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
2829
|
+
*/
|
|
2830
|
+
const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
|
|
2831
|
+
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
2832
|
+
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
2833
|
+
if (route.providerName === "github-copilot") {
|
|
2834
|
+
rotatedProvider = resolveProviderTransport(
|
|
2835
|
+
route.providerName,
|
|
2836
|
+
rotatedProvider,
|
|
2837
|
+
parsed.options.promptCacheKey,
|
|
2838
|
+
snapshot.apiBaseUrl,
|
|
2839
|
+
) as OcxProviderConfig;
|
|
2840
|
+
}
|
|
2841
|
+
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
2842
|
+
route.provider = rotatedProvider;
|
|
2843
|
+
if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
|
|
2844
|
+
return true;
|
|
2845
|
+
};
|
|
2644
2846
|
const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
|
|
2645
2847
|
? anthropicSessionKeyFromParts({
|
|
2646
2848
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
@@ -2680,6 +2882,11 @@ async function handleResponsesInner(
|
|
|
2680
2882
|
};
|
|
2681
2883
|
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
2682
2884
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
2885
|
+
// Remember which account actually served this request so a 429 cools THAT one, not
|
|
2886
|
+
// whichever account is active by the time the response comes back (#2568).
|
|
2887
|
+
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
2888
|
+
genericFailoverAccountId = resolved.accountId;
|
|
2889
|
+
}
|
|
2683
2890
|
if (route.providerName === "kiro") {
|
|
2684
2891
|
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
2685
2892
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
@@ -2758,6 +2965,7 @@ async function handleResponsesInner(
|
|
|
2758
2965
|
(logCtx.attempts ??= []).push(attempt);
|
|
2759
2966
|
}
|
|
2760
2967
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
2968
|
+
let runTurnAdapter = adapter;
|
|
2761
2969
|
if (adapter.runTurn) {
|
|
2762
2970
|
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
2763
2971
|
}
|
|
@@ -2886,8 +3094,9 @@ async function handleResponsesInner(
|
|
|
2886
3094
|
delete parsed.options.parallelToolCalls;
|
|
2887
3095
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
2888
3096
|
// would force schema-constrained JSON into the synthetic compaction item. The flag and
|
|
2889
|
-
// the raw `text`
|
|
2890
|
-
//
|
|
3097
|
+
// the raw `text` control go too: the key-mode openai-responses adapter builds from
|
|
3098
|
+
// _rawBody, so a surviving format there would still reach the upstream. (The Kiro
|
|
3099
|
+
// guard no longer reads _rawBody.text; it refuses structured output only.)
|
|
2891
3100
|
delete parsed.options.textFormat;
|
|
2892
3101
|
delete parsed._structuredOutput;
|
|
2893
3102
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
@@ -2958,7 +3167,9 @@ async function handleResponsesInner(
|
|
|
2958
3167
|
// the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
|
|
2959
3168
|
// it here escaped every catch up to the Bun handler, so the same request produced an
|
|
2960
3169
|
// unstructured 500 — and no request log — depending only on whether a rotation ran first.
|
|
2961
|
-
|
|
3170
|
+
// Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the
|
|
3171
|
+
// catalog had to drop, so the selector naming it is a client input error, not a 500.
|
|
3172
|
+
if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) {
|
|
2962
3173
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
2963
3174
|
}
|
|
2964
3175
|
throw error;
|
|
@@ -3076,10 +3287,16 @@ async function handleResponsesInner(
|
|
|
3076
3287
|
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
3077
3288
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
3078
3289
|
if (inspectionSawUndeclaredTool) return;
|
|
3290
|
+
const restoredResponse = restoreRoutedCustomCalls(
|
|
3291
|
+
response,
|
|
3292
|
+
routedCustomToolNames,
|
|
3293
|
+
routedCustomToolRepairNames,
|
|
3294
|
+
declaredWireToolNames,
|
|
3295
|
+
).value as { id?: unknown; output?: unknown; status?: unknown };
|
|
3079
3296
|
if (
|
|
3080
3297
|
undeclaredToolGuardActive
|
|
3081
3298
|
&& undeclaredToolCallNameInResponse(
|
|
3082
|
-
|
|
3299
|
+
restoredResponse,
|
|
3083
3300
|
declaredWireToolNames,
|
|
3084
3301
|
declaredNamelessClientCallTypes,
|
|
3085
3302
|
providerExecutedCallTypes,
|
|
@@ -3087,7 +3304,7 @@ async function handleResponsesInner(
|
|
|
3087
3304
|
) {
|
|
3088
3305
|
return;
|
|
3089
3306
|
}
|
|
3090
|
-
rememberPassthroughResponse(
|
|
3307
|
+
rememberPassthroughResponse(restoredResponse);
|
|
3091
3308
|
}
|
|
3092
3309
|
: undefined;
|
|
3093
3310
|
recordAdapterReasoning(logCtx, request);
|
|
@@ -3541,7 +3758,7 @@ async function handleResponsesInner(
|
|
|
3541
3758
|
}
|
|
3542
3759
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
3543
3760
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
3544
|
-
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
3761
|
+
if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
3545
3762
|
if (isUsageDebugEnabled()) {
|
|
3546
3763
|
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
3547
3764
|
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
@@ -3716,6 +3933,7 @@ async function handleResponsesInner(
|
|
|
3716
3933
|
routedCustomToolNames,
|
|
3717
3934
|
translatorBudget,
|
|
3718
3935
|
routedCustomToolRepairNames,
|
|
3936
|
+
declaredWireToolNames,
|
|
3719
3937
|
)
|
|
3720
3938
|
: undefined,
|
|
3721
3939
|
routedToolSearchNames.size > 0
|
|
@@ -3926,6 +4144,7 @@ async function handleResponsesInner(
|
|
|
3926
4144
|
restoredNamespace,
|
|
3927
4145
|
routedCustomToolNames,
|
|
3928
4146
|
routedCustomToolRepairNames,
|
|
4147
|
+
declaredWireToolNames,
|
|
3929
4148
|
);
|
|
3930
4149
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
3931
4150
|
restored,
|
|
@@ -4082,6 +4301,49 @@ async function handleResponsesInner(
|
|
|
4082
4301
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
4083
4302
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
4084
4303
|
const canRunWebSearch = !!wsPlan && !adapter.runTurn;
|
|
4304
|
+
const rotateSidecarProviderOn429 = async (retryAfter: string | null): Promise<ProviderAdapter | null> => {
|
|
4305
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4306
|
+
retryAfter,
|
|
4307
|
+
now: Date.now(),
|
|
4308
|
+
attemptedKey: route.provider.apiKey,
|
|
4309
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
4310
|
+
});
|
|
4311
|
+
if (rotated) {
|
|
4312
|
+
route.provider = rotated;
|
|
4313
|
+
} else {
|
|
4314
|
+
if (
|
|
4315
|
+
!genericFailoverAccountId
|
|
4316
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4317
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4318
|
+
) return null;
|
|
4319
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4320
|
+
config,
|
|
4321
|
+
route.providerName,
|
|
4322
|
+
genericFailoverAccountId,
|
|
4323
|
+
retryAfter,
|
|
4324
|
+
);
|
|
4325
|
+
if (!nextAccountId) return null;
|
|
4326
|
+
try {
|
|
4327
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4328
|
+
genericFailoverAccountId = nextAccountId;
|
|
4329
|
+
genericFailovers += 1;
|
|
4330
|
+
if (!applyFailoverSnapshot(snapshot)) return null;
|
|
4331
|
+
} catch {
|
|
4332
|
+
return null;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
const rotatedAdapter = resolveAdapter(
|
|
4336
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4337
|
+
config.cacheRetention,
|
|
4338
|
+
);
|
|
4339
|
+
bindRouteReasoningReplayScope({
|
|
4340
|
+
parsed,
|
|
4341
|
+
providerName: route.providerName,
|
|
4342
|
+
provider: route.provider,
|
|
4343
|
+
adapterName: rotatedAdapter.name,
|
|
4344
|
+
});
|
|
4345
|
+
return rotatedAdapter;
|
|
4346
|
+
};
|
|
4085
4347
|
if ((imgPlan || vidPlan) && canRunWebSearch) {
|
|
4086
4348
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
4087
4349
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
@@ -4172,27 +4434,7 @@ async function handleResponsesInner(
|
|
|
4172
4434
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
4173
4435
|
}
|
|
4174
4436
|
},
|
|
4175
|
-
on429:
|
|
4176
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4177
|
-
retryAfter,
|
|
4178
|
-
now: Date.now(),
|
|
4179
|
-
attemptedKey: route.provider.apiKey,
|
|
4180
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4181
|
-
});
|
|
4182
|
-
if (!rotated) return null;
|
|
4183
|
-
route.provider = rotated;
|
|
4184
|
-
const rotatedAdapter = resolveAdapter(
|
|
4185
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4186
|
-
config.cacheRetention,
|
|
4187
|
-
);
|
|
4188
|
-
bindRouteReasoningReplayScope({
|
|
4189
|
-
parsed,
|
|
4190
|
-
providerName: route.providerName,
|
|
4191
|
-
provider: route.provider,
|
|
4192
|
-
adapterName: rotatedAdapter.name,
|
|
4193
|
-
});
|
|
4194
|
-
return rotatedAdapter;
|
|
4195
|
-
},
|
|
4437
|
+
on429: rotateSidecarProviderOn429,
|
|
4196
4438
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4197
4439
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
4198
4440
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
@@ -4260,27 +4502,7 @@ async function handleResponsesInner(
|
|
|
4260
4502
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
4261
4503
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
4262
4504
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
4263
|
-
on429:
|
|
4264
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4265
|
-
retryAfter,
|
|
4266
|
-
now: Date.now(),
|
|
4267
|
-
attemptedKey: route.provider.apiKey,
|
|
4268
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4269
|
-
});
|
|
4270
|
-
if (!rotated) return null;
|
|
4271
|
-
route.provider = rotated;
|
|
4272
|
-
const rotatedAdapter = resolveAdapter(
|
|
4273
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4274
|
-
config.cacheRetention,
|
|
4275
|
-
);
|
|
4276
|
-
bindRouteReasoningReplayScope({
|
|
4277
|
-
parsed,
|
|
4278
|
-
providerName: route.providerName,
|
|
4279
|
-
provider: route.provider,
|
|
4280
|
-
adapterName: rotatedAdapter.name,
|
|
4281
|
-
});
|
|
4282
|
-
return rotatedAdapter;
|
|
4283
|
-
},
|
|
4505
|
+
on429: rotateSidecarProviderOn429,
|
|
4284
4506
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4285
4507
|
onCompletedResponse: commitReasoningReplayServingRoute,
|
|
4286
4508
|
});
|
|
@@ -4352,7 +4574,7 @@ async function handleResponsesInner(
|
|
|
4352
4574
|
pacingSlotAcquired: true,
|
|
4353
4575
|
},
|
|
4354
4576
|
);
|
|
4355
|
-
await
|
|
4577
|
+
await runTurnAdapter.runTurn?.(
|
|
4356
4578
|
parsed,
|
|
4357
4579
|
{
|
|
4358
4580
|
headers: selectedForwardHeaders,
|
|
@@ -4385,6 +4607,77 @@ async function handleResponsesInner(
|
|
|
4385
4607
|
}
|
|
4386
4608
|
};
|
|
4387
4609
|
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
|
|
4610
|
+
const rotateRunTurnAdapterOnPreflight429 = async (
|
|
4611
|
+
error: Extract<AdapterEvent, { type: "error" }>,
|
|
4612
|
+
): Promise<boolean> => {
|
|
4613
|
+
const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
|
|
4614
|
+
if (
|
|
4615
|
+
status !== 429
|
|
4616
|
+
|| !genericFailoverAccountId
|
|
4617
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4618
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4619
|
+
) return false;
|
|
4620
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4621
|
+
config,
|
|
4622
|
+
route.providerName,
|
|
4623
|
+
genericFailoverAccountId,
|
|
4624
|
+
null,
|
|
4625
|
+
);
|
|
4626
|
+
if (!nextAccountId) return false;
|
|
4627
|
+
try {
|
|
4628
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4629
|
+
genericFailoverAccountId = nextAccountId;
|
|
4630
|
+
genericFailovers += 1;
|
|
4631
|
+
if (!applyFailoverSnapshot(snapshot)) return false;
|
|
4632
|
+
// A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
|
|
4633
|
+
// client-visible bytes, so replay is safe, but carrying its account identity into the next
|
|
4634
|
+
// account would not be. Let the rotated adapter derive a fresh identity and conversation.
|
|
4635
|
+
parsed._cursorIdentityScope = undefined;
|
|
4636
|
+
parsed._cursorConversationId = undefined;
|
|
4637
|
+
if (parsed._providerContinuation?.cursor) {
|
|
4638
|
+
const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
|
|
4639
|
+
parsed._providerContinuation = otherProviderState;
|
|
4640
|
+
}
|
|
4641
|
+
const rotatedProvider = resolveWireProtocolOverride(
|
|
4642
|
+
route.providerName,
|
|
4643
|
+
route.modelId,
|
|
4644
|
+
route.provider,
|
|
4645
|
+
inboundWire,
|
|
4646
|
+
);
|
|
4647
|
+
const rotatedAdapter = resolveAdapter(rotatedProvider, config.cacheRetention);
|
|
4648
|
+
if (!rotatedAdapter.runTurn) return false;
|
|
4649
|
+
runTurnAdapter = rotatedAdapter;
|
|
4650
|
+
bindRouteReasoningReplayScope({
|
|
4651
|
+
parsed,
|
|
4652
|
+
providerName: route.providerName,
|
|
4653
|
+
provider: rotatedProvider,
|
|
4654
|
+
adapterName: rotatedAdapter.name,
|
|
4655
|
+
oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
|
|
4656
|
+
codexAuthContext: authCtx,
|
|
4657
|
+
forwardHeaders: selectedForwardHeaders,
|
|
4658
|
+
});
|
|
4659
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
4660
|
+
return true;
|
|
4661
|
+
} catch {
|
|
4662
|
+
return false;
|
|
4663
|
+
}
|
|
4664
|
+
};
|
|
4665
|
+
const preflightRunTurnFailover = async (
|
|
4666
|
+
firstSource: AsyncIterable<AdapterEvent>,
|
|
4667
|
+
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
4668
|
+
let source = firstSource;
|
|
4669
|
+
while (true) {
|
|
4670
|
+
const preflight = await preflightAdapterEvents(source);
|
|
4671
|
+
if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
4672
|
+
return preflight.stream;
|
|
4673
|
+
}
|
|
4674
|
+
const retryQueue = createAdapterEventQueue({
|
|
4675
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
4676
|
+
});
|
|
4677
|
+
void runTurnAttempt(retryQueue, "oauth-account-429");
|
|
4678
|
+
source = retryQueue.stream();
|
|
4679
|
+
}
|
|
4680
|
+
};
|
|
4388
4681
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
4389
4682
|
// first queue is closed once its attempt settles, and pushing into it after
|
|
4390
4683
|
// close is a silent no-op.
|
|
@@ -4400,6 +4693,11 @@ async function handleResponsesInner(
|
|
|
4400
4693
|
if (parsed.stream) {
|
|
4401
4694
|
void runTurn();
|
|
4402
4695
|
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
4696
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
4697
|
+
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
4698
|
+
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
4699
|
+
eventSource = await preflightRunTurnFailover(eventSource);
|
|
4700
|
+
}
|
|
4403
4701
|
if (options.comboAttempt) {
|
|
4404
4702
|
const preflight = await preflightAdapterEvents(eventSource);
|
|
4405
4703
|
if (preflight.error || preflight.empty) {
|
|
@@ -4417,7 +4715,16 @@ async function handleResponsesInner(
|
|
|
4417
4715
|
// signal — run the adapter transport again against a fresh queue.
|
|
4418
4716
|
continuation: runTurnRetrySource,
|
|
4419
4717
|
})
|
|
4420
|
-
:
|
|
4718
|
+
// Guard off (the default): leave the stream alone, but record that the turn ended
|
|
4719
|
+
// empty so the user has something to correlate instead of an unexplained blank
|
|
4720
|
+
// result (#2472). Retrying by default would re-send a turn that may already have had
|
|
4721
|
+
// billable side effects, so the honest default is observability, not recovery.
|
|
4722
|
+
: observeEmptyCompletion(eventSource, () => {
|
|
4723
|
+
console.warn(
|
|
4724
|
+
`[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
|
|
4725
|
+
+ "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
|
|
4726
|
+
);
|
|
4727
|
+
});
|
|
4421
4728
|
const sseStream = bridgeToResponsesSSE(
|
|
4422
4729
|
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
4423
4730
|
() => {
|
|
@@ -4470,15 +4777,22 @@ async function handleResponsesInner(
|
|
|
4470
4777
|
|
|
4471
4778
|
await runTurn();
|
|
4472
4779
|
const firstAttemptEvents = await queue.collect();
|
|
4780
|
+
let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
|
|
4781
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
4782
|
+
runTurnEvents = [];
|
|
4783
|
+
for await (const event of await preflightRunTurnFailover(
|
|
4784
|
+
(async function* () { yield* firstAttemptEvents; })(),
|
|
4785
|
+
)) runTurnEvents.push(event);
|
|
4786
|
+
}
|
|
4473
4787
|
let events: AdapterEvent[];
|
|
4474
4788
|
if (emptyCompletionGuardEnabled) {
|
|
4475
4789
|
events = [];
|
|
4476
4790
|
for await (const event of guardEmptyCompletionEventStream({
|
|
4477
|
-
firstEvents: (async function* () { yield*
|
|
4791
|
+
firstEvents: (async function* () { yield* runTurnEvents; })(),
|
|
4478
4792
|
continuation: runTurnRetrySource,
|
|
4479
4793
|
})) events.push(event);
|
|
4480
4794
|
} else {
|
|
4481
|
-
events =
|
|
4795
|
+
events = runTurnEvents;
|
|
4482
4796
|
}
|
|
4483
4797
|
if (options.comboAttempt) {
|
|
4484
4798
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
@@ -4877,6 +5191,49 @@ async function handleResponsesInner(
|
|
|
4877
5191
|
break;
|
|
4878
5192
|
}
|
|
4879
5193
|
}
|
|
5194
|
+
// Generic OAuth account failover (#2568) for providers with no pool of their own.
|
|
5195
|
+
// Presence is consent since #2568d: rotation is ON by default once two or more eligible
|
|
5196
|
+
// accounts are stored for the provider, because a second deliberate login is read as the
|
|
5197
|
+
// operator asking for it. A single-account install is still a strict no-op, and an
|
|
5198
|
+
// explicit `oauthAccountFailover.enabled: false` (global or per provider) still wins --
|
|
5199
|
+
// see isGenericOAuthFailoverEnabled in src/oauth/generic-account-failover.ts. Codex and
|
|
5200
|
+
// Anthropic are excluded by isGenericFailoverProvider: their pools own quota scopes,
|
|
5201
|
+
// probe leases and affinity that this must not reimplement.
|
|
5202
|
+
while (
|
|
5203
|
+
upstreamResponse.status === 429
|
|
5204
|
+
&& genericFailoverAccountId
|
|
5205
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
5206
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
5207
|
+
) {
|
|
5208
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
5209
|
+
config,
|
|
5210
|
+
route.providerName,
|
|
5211
|
+
genericFailoverAccountId,
|
|
5212
|
+
upstreamResponse.headers.get("retry-after"),
|
|
5213
|
+
);
|
|
5214
|
+
if (!nextAccountId) break;
|
|
5215
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
5216
|
+
try {
|
|
5217
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
|
|
5218
|
+
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
5219
|
+
// would mix one account's credential with another's routing data.
|
|
5220
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
5221
|
+
genericFailoverAccountId = nextAccountId;
|
|
5222
|
+
genericFailovers += 1;
|
|
5223
|
+
if (!applyFailoverSnapshot(snapshot)) break;
|
|
5224
|
+
invalidateSameTargetRequest();
|
|
5225
|
+
activeAdapter = resolveAdapter(
|
|
5226
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5227
|
+
config.cacheRetention,
|
|
5228
|
+
);
|
|
5229
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5230
|
+
const result = await rebuildAndRefetch("oauth-account-429");
|
|
5231
|
+
if ("failed" in result) return result.failed;
|
|
5232
|
+
upstreamResponse = result;
|
|
5233
|
+
} catch {
|
|
5234
|
+
break;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
4880
5237
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
4881
5238
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
4882
5239
|
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
@@ -4952,29 +5309,41 @@ async function handleResponsesInner(
|
|
|
4952
5309
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
4953
5310
|
// material before it reaches the client-facing error surface.
|
|
4954
5311
|
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
4955
|
-
const
|
|
4956
|
-
|
|
4957
|
-
|
|
5312
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5313
|
+
const message = normalized.cyberPolicy
|
|
5314
|
+
? normalized.message
|
|
5315
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
5316
|
+
: enrichOpenCodeZenRateLimitMessage(
|
|
5317
|
+
`Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
|
|
5318
|
+
{
|
|
5319
|
+
status: upstreamResponse.status,
|
|
5320
|
+
providerName: route.providerName,
|
|
5321
|
+
baseUrl: route.provider.baseUrl,
|
|
5322
|
+
adapter: route.provider.adapter,
|
|
5323
|
+
authMode: route.provider.authMode,
|
|
5324
|
+
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5325
|
+
upstreamRetryAfter,
|
|
5326
|
+
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
5327
|
+
// never reach enrichOpenCodeZenRateLimitMessage here.
|
|
5328
|
+
supportsHttpSameKeyRetry: true,
|
|
5329
|
+
},
|
|
5330
|
+
);
|
|
5331
|
+
const retryAfter = normalized.cyberPolicy
|
|
5332
|
+
? undefined
|
|
5333
|
+
: resolveClientRetryAfter({
|
|
4958
5334
|
status: upstreamResponse.status,
|
|
4959
|
-
|
|
4960
|
-
baseUrl: route.provider.baseUrl,
|
|
4961
|
-
adapter: route.provider.adapter,
|
|
4962
|
-
authMode: route.provider.authMode,
|
|
4963
|
-
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5335
|
+
message,
|
|
4964
5336
|
upstreamRetryAfter,
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
5337
|
+
});
|
|
5338
|
+
return formatErrorResponse(
|
|
5339
|
+
upstreamResponse.status,
|
|
5340
|
+
normalized.cyberPolicy ? (normalized.type ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
5341
|
+
message,
|
|
5342
|
+
{
|
|
5343
|
+
...(normalized.cyberPolicy ? { code: CYBER_POLICY_ERROR_CODE } : {}),
|
|
5344
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
4968
5345
|
},
|
|
4969
5346
|
);
|
|
4970
|
-
const retryAfter = resolveClientRetryAfter({
|
|
4971
|
-
status: upstreamResponse.status,
|
|
4972
|
-
message,
|
|
4973
|
-
upstreamRetryAfter,
|
|
4974
|
-
});
|
|
4975
|
-
return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
|
|
4976
|
-
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
4977
|
-
});
|
|
4978
5347
|
}
|
|
4979
5348
|
}
|
|
4980
5349
|
|
|
@@ -5229,10 +5598,21 @@ async function handleResponsesInner(
|
|
|
5229
5598
|
|
|
5230
5599
|
if (!response.ok) {
|
|
5231
5600
|
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
5601
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5232
5602
|
yield {
|
|
5233
5603
|
type: "error",
|
|
5234
|
-
status: response.status,
|
|
5235
|
-
message:
|
|
5604
|
+
status: normalized.cyberPolicy ? 400 : response.status,
|
|
5605
|
+
message: normalized.cyberPolicy
|
|
5606
|
+
? normalized.message
|
|
5607
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
5608
|
+
: `Provider continuation error ${response.status}: ${normalized.safeText}`,
|
|
5609
|
+
...(normalized.cyberPolicy
|
|
5610
|
+
? {
|
|
5611
|
+
errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
|
|
5612
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
5613
|
+
retryable: false,
|
|
5614
|
+
}
|
|
5615
|
+
: {}),
|
|
5236
5616
|
};
|
|
5237
5617
|
return;
|
|
5238
5618
|
}
|