@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
87
87
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
88
88
|
import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
|
|
89
|
-
import { modelInList, namespacedToolName } from "../../types";
|
|
89
|
+
import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types";
|
|
90
90
|
import type {
|
|
91
91
|
AdapterEvent,
|
|
92
92
|
OcxConfig,
|
|
@@ -100,8 +100,8 @@ import type {
|
|
|
100
100
|
} from "../../types";
|
|
101
101
|
import {
|
|
102
102
|
forceRefreshOAuthAccessSnapshot,
|
|
103
|
-
getOAuthCredentialApiBaseUrl,
|
|
104
103
|
getValidAccessTokenForAccount,
|
|
104
|
+
getValidAccessSnapshotForAccount,
|
|
105
105
|
getValidAccessTokenSnapshot,
|
|
106
106
|
publicOAuthAuthenticationErrorMessage,
|
|
107
107
|
type OAuthAccessSnapshot,
|
|
@@ -122,11 +122,14 @@ import {
|
|
|
122
122
|
import { stampOAuthAccountLabel } from "../../providers/label";
|
|
123
123
|
import {
|
|
124
124
|
failoverAccountSnapshot,
|
|
125
|
+
forgetGenericFailoverRoster,
|
|
125
126
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
126
127
|
isGenericFailoverProvider,
|
|
127
128
|
isGenericOAuthFailoverEnabled,
|
|
129
|
+
preferredInitialAccount,
|
|
128
130
|
rotateGenericOAuthAccountOn429,
|
|
129
131
|
} from "../../oauth/generic-account-failover";
|
|
132
|
+
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
130
133
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
131
134
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
132
135
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
@@ -140,7 +143,7 @@ import {
|
|
|
140
143
|
CodexPoolAuthenticationError,
|
|
141
144
|
CodexThreadAffinityExpiredError,
|
|
142
145
|
headersForCodexAuthContext,
|
|
143
|
-
|
|
146
|
+
materializeCodexUpstreamAuthAsync,
|
|
144
147
|
isCodexAuthContextUsable,
|
|
145
148
|
resolveCodexAuthContext,
|
|
146
149
|
codexProbeLeaseId,
|
|
@@ -155,16 +158,26 @@ import {
|
|
|
155
158
|
resolveCodexModelEntitlements,
|
|
156
159
|
} from "../../codex/model-entitlements";
|
|
157
160
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
158
|
-
import {
|
|
161
|
+
import {
|
|
162
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
163
|
+
forceRefreshMainAccountToken,
|
|
164
|
+
type NativeMainRefreshDependencies,
|
|
165
|
+
} from "../../codex/main-account";
|
|
159
166
|
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
160
167
|
import {
|
|
161
168
|
computeQuotaCooldown,
|
|
162
169
|
codexQuotaScopeForModel,
|
|
163
170
|
formatCodexProviderForLog,
|
|
171
|
+
handOffThreadAffinityGeneration,
|
|
164
172
|
previewCodexAccountForRequest,
|
|
165
173
|
recordCodexUpstreamOutcome,
|
|
166
174
|
type CodexUpstreamOutcome,
|
|
167
175
|
} from "../../codex/routing";
|
|
176
|
+
import {
|
|
177
|
+
TokenRefreshError,
|
|
178
|
+
forceRefreshCodexPoolToken,
|
|
179
|
+
readCodexAccountRecord,
|
|
180
|
+
} from "../../codex/account-store";
|
|
168
181
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
169
182
|
import {
|
|
170
183
|
applyUpstreamRecoveryInit,
|
|
@@ -172,7 +185,11 @@ import {
|
|
|
172
185
|
fetchWithTransientRetry,
|
|
173
186
|
prepareSameTarget429Wait,
|
|
174
187
|
} from "../../lib/upstream-retry";
|
|
175
|
-
import {
|
|
188
|
+
import {
|
|
189
|
+
ForwardAdmissionCredentialError,
|
|
190
|
+
hasForwardableCodexBearer,
|
|
191
|
+
validateForwardAdmissionCredential,
|
|
192
|
+
} from "../auth-cors";
|
|
176
193
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
177
194
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
178
195
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
@@ -210,11 +227,18 @@ import {
|
|
|
210
227
|
rateLimitRetryDelayMs,
|
|
211
228
|
rateLimitRetryPolicyFor,
|
|
212
229
|
rotateProviderTransportOn429,
|
|
230
|
+
transientRetryPolicyFor,
|
|
213
231
|
} from "../../providers/key-failover";
|
|
214
232
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
215
233
|
import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
|
|
216
234
|
import type { WsData } from "../ws-bridge";
|
|
217
|
-
import {
|
|
235
|
+
import {
|
|
236
|
+
codexAccountSelectionForTurn,
|
|
237
|
+
registerTurn,
|
|
238
|
+
trackStreamLifetime,
|
|
239
|
+
tryClaimNativeMainProfileForTurn,
|
|
240
|
+
unregisterTurn,
|
|
241
|
+
} from "../lifecycle";
|
|
218
242
|
import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
|
|
219
243
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
220
244
|
import type { AdmissionLease } from "../../lib/admission";
|
|
@@ -266,6 +290,7 @@ import {
|
|
|
266
290
|
} from "../relay";
|
|
267
291
|
import {
|
|
268
292
|
agentTaskRecoveryConfig,
|
|
293
|
+
discardEncryptedAgentTaskRecovery,
|
|
269
294
|
recoverEncryptedAgentTask,
|
|
270
295
|
} from "./agent-task-recovery";
|
|
271
296
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
@@ -295,9 +320,9 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
|
|
|
295
320
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
296
321
|
|
|
297
322
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
298
|
-
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
323
|
+
import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
|
|
299
324
|
import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
|
|
300
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
325
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier } from "./fetch-helpers";
|
|
301
326
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
302
327
|
import {
|
|
303
328
|
acquireUpstreamHostAdmission,
|
|
@@ -327,6 +352,7 @@ import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-sea
|
|
|
327
352
|
import {
|
|
328
353
|
createRoutedNamespaceCallRestoreRewrite,
|
|
329
354
|
NamespaceToolCollisionError,
|
|
355
|
+
restoreRoutedNamespaceCalls,
|
|
330
356
|
restoreRoutedNamespaceCallsInJson,
|
|
331
357
|
type RoutedNamespaceToolAliases,
|
|
332
358
|
} from "../../responses/namespace-tool-compat";
|
|
@@ -347,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
|
|
|
347
373
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
348
374
|
import {
|
|
349
375
|
emptyCompletionRetryEnabled,
|
|
376
|
+
emptyCompletionNotice,
|
|
350
377
|
observeEmptyCompletion,
|
|
351
378
|
guardEmptyCompletionEventStream,
|
|
352
379
|
} from "./empty-completion-guard";
|
|
@@ -375,6 +402,11 @@ export function sidecarOutcomeRecorder(
|
|
|
375
402
|
probeLeaseId: authCtx.probeLeaseId,
|
|
376
403
|
probeQuotaScope: authCtx.probeQuotaScope,
|
|
377
404
|
writerGeneration: authCtx.writerGeneration,
|
|
405
|
+
// A vision or web-search sidecar can return 401/403, and that is evidence about the exact
|
|
406
|
+
// stored credential it used. Without the generation it becomes an account-wide quarantine
|
|
407
|
+
// that a replacement inherits (#2892 gap 4). `main-pool` has no stored-record generation, so
|
|
408
|
+
// it keeps the unfenced account-wide semantics.
|
|
409
|
+
...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
|
|
378
410
|
})
|
|
379
411
|
: undefined;
|
|
380
412
|
}
|
|
@@ -893,10 +925,20 @@ interface CodexPoolAccountRetryArgs {
|
|
|
893
925
|
codexWsRuntimeIdentity?: BunRuntimeGateInput;
|
|
894
926
|
translatorBudget: TranslatorBudget;
|
|
895
927
|
turnAdmissionLease?: AdmissionLease;
|
|
928
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
896
929
|
};
|
|
897
930
|
firstAuthCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
|
|
898
931
|
firstResponse: Response;
|
|
899
932
|
outcomeStatus: number;
|
|
933
|
+
/**
|
|
934
|
+
* Forbid resolving a DIFFERENT account for this retry.
|
|
935
|
+
*
|
|
936
|
+
* Set when a stored Pool 401 already spent this logical request's account budget on its own
|
|
937
|
+
* refresh and replay. The same-account gated-model retry above stays available, because it
|
|
938
|
+
* sends to the account that was already paying; only the alternate-account resolution below is
|
|
939
|
+
* out of budget.
|
|
940
|
+
*/
|
|
941
|
+
sameAccountOnly?: boolean;
|
|
900
942
|
upstream: AbortController;
|
|
901
943
|
connectMs: number;
|
|
902
944
|
passthroughEstimate?: number;
|
|
@@ -923,6 +965,29 @@ type CodexPoolAccountRetryResult =
|
|
|
923
965
|
authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
|
|
924
966
|
};
|
|
925
967
|
|
|
968
|
+
/** Keep retry-stage entitlement snapshots inside the native-main selection fence. */
|
|
969
|
+
async function resolveCodexRetryModelEntitlements(
|
|
970
|
+
config: OcxConfig,
|
|
971
|
+
resolver: typeof resolveCodexModelEntitlements,
|
|
972
|
+
turnAdmissionLease?: AdmissionLease,
|
|
973
|
+
): Promise<Awaited<ReturnType<typeof resolveCodexModelEntitlements>>> {
|
|
974
|
+
// The initial auth selection has already released its admission before the first
|
|
975
|
+
// response arrives. Re-enter for every refresh so profile switching cannot overlap
|
|
976
|
+
// credential discovery, and omit main entirely when a drain or recovery owns it.
|
|
977
|
+
const selectionAdmission = codexAccountSelectionForTurn(turnAdmissionLease)?.();
|
|
978
|
+
const nativeMainReadsForbidden = isNativeMainTrafficBlocked()
|
|
979
|
+
|| selectionAdmission?.mainProfileDraining === true;
|
|
980
|
+
try {
|
|
981
|
+
return await resolver(config, {
|
|
982
|
+
excludeAccountIds: nativeMainReadsForbidden
|
|
983
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
984
|
+
: undefined,
|
|
985
|
+
});
|
|
986
|
+
} finally {
|
|
987
|
+
selectionAdmission?.release();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
926
991
|
const CODEX_ACCOUNT_GATED_CANONICAL_WIRE_MODELS: ReadonlyMap<string, string> = new Map([
|
|
927
992
|
// The authenticated catalog currently advertises Daybreak Blue, while successful responses
|
|
928
993
|
// identify the serving model as gpt-5.6-sol. Sending the selector itself is shard-dependent:
|
|
@@ -1011,10 +1076,22 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1011
1076
|
outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
|
|
1012
1077
|
} = args;
|
|
1013
1078
|
const inboundWire = options.inboundWire ?? "responses";
|
|
1079
|
+
const entitlementResolver = options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements;
|
|
1014
1080
|
let retryAuthCtx: CodexAuthContext | undefined;
|
|
1015
1081
|
if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
|
|
1016
1082
|
invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
|
|
1017
|
-
|
|
1083
|
+
let refreshed;
|
|
1084
|
+
try {
|
|
1085
|
+
refreshed = await resolveCodexRetryModelEntitlements(
|
|
1086
|
+
config,
|
|
1087
|
+
entitlementResolver,
|
|
1088
|
+
options.turnAdmissionLease,
|
|
1089
|
+
);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
await firstResponse.body?.cancel().catch(() => undefined);
|
|
1092
|
+
releaseCodexAuthContextProbeLease(firstAuthCtx);
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1018
1095
|
if (entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(firstAuthCtx.accountId)) {
|
|
1019
1096
|
// The authenticated roster still grants this exact model. Retry on the same account:
|
|
1020
1097
|
// upstream shards can briefly disagree during a gated-model rollout, but a pre-stream 400
|
|
@@ -1024,7 +1101,9 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1024
1101
|
}
|
|
1025
1102
|
// Exact account selectors may retry the same confirmed account above, but must never resolve
|
|
1026
1103
|
// an alternate. Quota failures and a refreshed entitlement miss remain terminal.
|
|
1027
|
-
if (!retryAuthCtx && firstAuthCtx.fixedAccount
|
|
1104
|
+
if (!retryAuthCtx && (firstAuthCtx.fixedAccount || args.sameAccountOnly === true)) {
|
|
1105
|
+
return { kind: "no-alternate" };
|
|
1106
|
+
}
|
|
1028
1107
|
try {
|
|
1029
1108
|
retryAuthCtx ??= await resolveCodexAuthContext(
|
|
1030
1109
|
req.headers,
|
|
@@ -1033,16 +1112,22 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1033
1112
|
{
|
|
1034
1113
|
excludeAccountId: firstAuthCtx.accountId,
|
|
1035
1114
|
modelId: route.modelId,
|
|
1115
|
+
requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
|
|
1036
1116
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1117
|
+
resolveCodexModelEntitlements: entitlementResolver,
|
|
1037
1118
|
},
|
|
1038
1119
|
);
|
|
1039
1120
|
} catch (error) {
|
|
1040
|
-
|
|
1121
|
+
const unexpectedRetryError =
|
|
1041
1122
|
!(error instanceof CodexPoolAuthenticationError)
|
|
1042
1123
|
&& !(error instanceof CodexAuthContextError)
|
|
1043
1124
|
&& !(error instanceof CodexAccountCooldownError)
|
|
1044
|
-
&& !(error instanceof CodexMainProfileDrainingError)
|
|
1045
|
-
)
|
|
1125
|
+
&& !(error instanceof CodexMainProfileDrainingError);
|
|
1126
|
+
if (unexpectedRetryError) {
|
|
1127
|
+
await firstResponse.body?.cancel().catch(() => undefined);
|
|
1128
|
+
releaseCodexAuthContextProbeLease(firstAuthCtx);
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1046
1131
|
}
|
|
1047
1132
|
if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
|
|
1048
1133
|
return { kind: "no-alternate" };
|
|
@@ -1131,24 +1216,30 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1131
1216
|
try {
|
|
1132
1217
|
while (true) {
|
|
1133
1218
|
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1219
|
+
try {
|
|
1220
|
+
upstreamResponse = await fetchWithHeaderTimeout(
|
|
1221
|
+
request.url,
|
|
1222
|
+
{
|
|
1223
|
+
method: request.method,
|
|
1224
|
+
headers: request.headers,
|
|
1225
|
+
body: request.body,
|
|
1226
|
+
},
|
|
1227
|
+
upstream.signal,
|
|
1228
|
+
connectMs,
|
|
1229
|
+
stream,
|
|
1230
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1231
|
+
providerName: route.providerName,
|
|
1232
|
+
modelId: route.modelId,
|
|
1233
|
+
}),
|
|
1234
|
+
// Credential-bearing forward send: never follow a redirect into a
|
|
1235
|
+
// dead-host rejection after the credential was seen (#914).
|
|
1236
|
+
route.provider.authMode === "forward",
|
|
1237
|
+
);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
// Only the forward send is a transport boundary. Entitlement resolver throws below are
|
|
1240
|
+
// deliberately outside this catch so programming errors retain their original path.
|
|
1241
|
+
return { kind: "transport", error, authCtx: retryAuthCtx };
|
|
1242
|
+
}
|
|
1152
1243
|
retrySendCount += 1;
|
|
1153
1244
|
args.onResponse?.(upstreamResponse, retryAuthCtx, request);
|
|
1154
1245
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
@@ -1158,13 +1249,23 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1158
1249
|
options.abortSignal,
|
|
1159
1250
|
)) break;
|
|
1160
1251
|
invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
|
|
1161
|
-
|
|
1252
|
+
let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
|
|
1253
|
+
try {
|
|
1254
|
+
refreshed = await resolveCodexRetryModelEntitlements(
|
|
1255
|
+
config,
|
|
1256
|
+
entitlementResolver,
|
|
1257
|
+
options.turnAdmissionLease,
|
|
1258
|
+
);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
await upstreamResponse.body?.cancel().catch(() => undefined);
|
|
1261
|
+
await firstResponse.body?.cancel().catch(() => undefined);
|
|
1262
|
+
releaseCodexAuthContextProbeLease(firstAuthCtx);
|
|
1263
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
1264
|
+
throw error;
|
|
1265
|
+
}
|
|
1162
1266
|
if (!entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(retryAuthCtx.accountId)) break;
|
|
1163
1267
|
await upstreamResponse.body?.cancel().catch(() => undefined);
|
|
1164
1268
|
}
|
|
1165
|
-
} catch (error) {
|
|
1166
|
-
// Attribute the transport failure to the alternate account (already selected).
|
|
1167
|
-
return { kind: "transport", error, authCtx: retryAuthCtx };
|
|
1168
1269
|
} finally {
|
|
1169
1270
|
request.releaseBodyObservation?.();
|
|
1170
1271
|
}
|
|
@@ -1235,6 +1336,10 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1235
1336
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
1236
1337
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
1237
1338
|
writerGeneration: authCtx.writerGeneration,
|
|
1339
|
+
// A mid-stream terminal can carry a semantic 401 long after the credential was
|
|
1340
|
+
// replaced. It is never replayed — the client already saw output — but it must
|
|
1341
|
+
// not retire the replacement either (#2887).
|
|
1342
|
+
...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
|
|
1238
1343
|
});
|
|
1239
1344
|
};
|
|
1240
1345
|
}
|
|
@@ -1311,6 +1416,8 @@ export interface HandleResponsesOptions {
|
|
|
1311
1416
|
responsesTerminalRepairScheduler?: ResponsesTerminalRepairScheduler;
|
|
1312
1417
|
/** Internal deterministic runtime-identity seam for Codex upstream WS selection tests. */
|
|
1313
1418
|
codexWsRuntimeIdentity?: BunRuntimeGateInput;
|
|
1419
|
+
/** Test seam for native main refresh without live OAuth traffic. */
|
|
1420
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
1314
1421
|
/**
|
|
1315
1422
|
* When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
|
|
1316
1423
|
* (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
|
|
@@ -1339,11 +1446,14 @@ export interface HandleResponsesOptions {
|
|
|
1339
1446
|
sourceBody: unknown;
|
|
1340
1447
|
previousResponseInputExpanded: boolean;
|
|
1341
1448
|
providerContinuation: OcxProviderContinuationState | undefined;
|
|
1449
|
+
recoveredPlaintext: boolean;
|
|
1342
1450
|
};
|
|
1343
1451
|
/** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
|
|
1344
1452
|
deferCodexResetDerivedCooldown?: boolean;
|
|
1345
1453
|
/** 030-owned handoff when a child consumed the original failure under bounds. */
|
|
1346
1454
|
onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
|
|
1455
|
+
/** A stored Pool credential was refreshed and its one allowed same-account replay was sent. */
|
|
1456
|
+
onStoredPool401ReplayDispatched?: () => void;
|
|
1347
1457
|
/** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
|
|
1348
1458
|
translatorBudget?: TranslatorBudget;
|
|
1349
1459
|
/**
|
|
@@ -1592,6 +1702,9 @@ async function resolveResponsesCodexAuth(
|
|
|
1592
1702
|
// no-ChatGPT-login install keeps working.
|
|
1593
1703
|
const substituteMainCredential = options.admission?.source === "bearer"
|
|
1594
1704
|
&& (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
|
|
1705
|
+
const requestScopedMainCredential = route.codexAccountMode !== undefined
|
|
1706
|
+
&& !substituteMainCredential
|
|
1707
|
+
&& hasForwardableCodexBearer(req.headers, config);
|
|
1595
1708
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
1596
1709
|
validateForwardAdmissionCredential(req.headers, config);
|
|
1597
1710
|
}
|
|
@@ -1601,11 +1714,28 @@ async function resolveResponsesCodexAuth(
|
|
|
1601
1714
|
accountId: route.codexAccountId,
|
|
1602
1715
|
modelId: route.modelId,
|
|
1603
1716
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
1717
|
+
requestScopedMainCredential,
|
|
1604
1718
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1605
1719
|
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
1720
|
+
signal: options.abortSignal,
|
|
1721
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1606
1722
|
});
|
|
1607
1723
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
1608
1724
|
} else {
|
|
1725
|
+
// A custom-named canonical-forward provider has no Codex account mode, but an
|
|
1726
|
+
// admission bearer still substitutes the stored main credential below. Claim the
|
|
1727
|
+
// same physical profile before synthesizing the main context so transport-based
|
|
1728
|
+
// substitution cannot bypass a switch drain.
|
|
1729
|
+
if (
|
|
1730
|
+
substituteMainCredential
|
|
1731
|
+
&& (
|
|
1732
|
+
isNativeMainTrafficBlocked()
|
|
1733
|
+
|| !tryClaimNativeMainProfileForTurn(options.turnAdmissionLease)
|
|
1734
|
+
|| isNativeMainTrafficBlocked()
|
|
1735
|
+
)
|
|
1736
|
+
) {
|
|
1737
|
+
throw new CodexMainProfileDrainingError();
|
|
1738
|
+
}
|
|
1609
1739
|
authCtx = { kind: "main", accountId: null };
|
|
1610
1740
|
options.onCodexAuthContextResolved?.(undefined);
|
|
1611
1741
|
}
|
|
@@ -1619,7 +1749,11 @@ async function resolveResponsesCodexAuth(
|
|
|
1619
1749
|
return {
|
|
1620
1750
|
ok: true,
|
|
1621
1751
|
authCtx,
|
|
1622
|
-
headers:
|
|
1752
|
+
headers: await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
|
|
1753
|
+
substituteMainCredential,
|
|
1754
|
+
signal: options.abortSignal,
|
|
1755
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1756
|
+
}),
|
|
1623
1757
|
substituteMainCredential,
|
|
1624
1758
|
};
|
|
1625
1759
|
} catch (err) {
|
|
@@ -1641,6 +1775,137 @@ async function resolveResponsesCodexAuth(
|
|
|
1641
1775
|
}
|
|
1642
1776
|
}
|
|
1643
1777
|
|
|
1778
|
+
/**
|
|
1779
|
+
* Terminal means the grant itself is dead and no retry can help. Everything else —
|
|
1780
|
+
* an untyped network failure, a token-endpoint 5xx surfacing as `unknown`, an abort,
|
|
1781
|
+
* refresh capacity, lock contention, a superseded flight — is transient, and treating
|
|
1782
|
+
* it as terminal would quarantine a healthy account on an upstream blip, which is the
|
|
1783
|
+
* defect this path exists to fix (#2887).
|
|
1784
|
+
*/
|
|
1785
|
+
function isTerminalPoolRefreshFailure(error: unknown): boolean {
|
|
1786
|
+
return error instanceof TokenRefreshError && (error.reason === "revoked" || error.reason === "expired");
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* One forced refresh and one same-account rebuild for a stored pool credential that
|
|
1791
|
+
* upstream rejected with a pre-stream 401. `quarantine` distinguishes a dead grant,
|
|
1792
|
+
* which must retire the account, from a transient failure, which must not.
|
|
1793
|
+
*/
|
|
1794
|
+
async function refreshPoolForwardAuth(args: {
|
|
1795
|
+
req: Request;
|
|
1796
|
+
route: RouteResult;
|
|
1797
|
+
authCtx: CodexAuthContext & { kind: "pool" };
|
|
1798
|
+
substituteMainCredential: boolean;
|
|
1799
|
+
options: HandleResponsesOptions;
|
|
1800
|
+
}): Promise<
|
|
1801
|
+
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1802
|
+
| { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
|
|
1803
|
+
> {
|
|
1804
|
+
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
1805
|
+
try {
|
|
1806
|
+
const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
|
|
1807
|
+
rejectedGeneration: authCtx.generation,
|
|
1808
|
+
rejectedAccessToken: authCtx.accessToken,
|
|
1809
|
+
signal: options.abortSignal,
|
|
1810
|
+
});
|
|
1811
|
+
if (!refreshed.rotated) {
|
|
1812
|
+
// The store resolved to the same bearer upstream just rejected. Replaying it
|
|
1813
|
+
// would spend another upstream call to earn the identical 401. Upstream can do
|
|
1814
|
+
// this on a SUCCESSFUL response by rotating only the refresh grant, so the
|
|
1815
|
+
// credential generation may already have moved — quarantine has to be fenced on
|
|
1816
|
+
// where the credential actually is, not on the generation we started from.
|
|
1817
|
+
return {
|
|
1818
|
+
ok: false,
|
|
1819
|
+
quarantine: true,
|
|
1820
|
+
quarantineGeneration: refreshed.generation,
|
|
1821
|
+
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
// Only a CAS this request performed itself proves the new credential descends from
|
|
1825
|
+
// the rejected one. Somebody else's replacement may be a different identity, and
|
|
1826
|
+
// its affinity must be retired rather than inherited.
|
|
1827
|
+
if (refreshed.selfRefreshed) {
|
|
1828
|
+
handOffThreadAffinityGeneration(authCtx.accountId, authCtx.generation, refreshed.generation);
|
|
1829
|
+
}
|
|
1830
|
+
const refreshedAuthCtx: CodexAuthContext = {
|
|
1831
|
+
...authCtx,
|
|
1832
|
+
accessToken: refreshed.accessToken,
|
|
1833
|
+
chatgptAccountId: refreshed.chatgptAccountId,
|
|
1834
|
+
generation: refreshed.generation,
|
|
1835
|
+
};
|
|
1836
|
+
const provider = applyCodexAuthContextToProvider(
|
|
1837
|
+
stripCodexRuntimeProviderFields(route.provider),
|
|
1838
|
+
refreshedAuthCtx,
|
|
1839
|
+
route.codexAccountMode,
|
|
1840
|
+
);
|
|
1841
|
+
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
1842
|
+
substituteMainCredential,
|
|
1843
|
+
signal: options.abortSignal,
|
|
1844
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1845
|
+
});
|
|
1846
|
+
return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
if (isTerminalPoolRefreshFailure(error)) {
|
|
1849
|
+
return {
|
|
1850
|
+
ok: false,
|
|
1851
|
+
quarantine: true,
|
|
1852
|
+
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
const response = formatErrorResponse(
|
|
1856
|
+
503,
|
|
1857
|
+
"server_busy",
|
|
1858
|
+
"Codex credential refresh did not complete; retry this request",
|
|
1859
|
+
);
|
|
1860
|
+
const headers = new Headers(response.headers);
|
|
1861
|
+
headers.set("Retry-After", "1");
|
|
1862
|
+
return { ok: false, quarantine: false, response: new Response(response.body, { status: response.status, headers }) };
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
async function refreshNativeMainForwardAuth(args: {
|
|
1867
|
+
req: Request;
|
|
1868
|
+
route: RouteResult;
|
|
1869
|
+
authCtx: CodexAuthContext;
|
|
1870
|
+
substituteMainCredential: boolean;
|
|
1871
|
+
options: HandleResponsesOptions;
|
|
1872
|
+
}): Promise<
|
|
1873
|
+
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
1874
|
+
| { ok: false; response: Response }
|
|
1875
|
+
> {
|
|
1876
|
+
const { req, route, authCtx, substituteMainCredential, options } = args;
|
|
1877
|
+
if (authCtx.kind !== "main-pool") {
|
|
1878
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
|
|
1879
|
+
}
|
|
1880
|
+
try {
|
|
1881
|
+
const refreshed = await forceRefreshMainAccountToken(authCtx.accessToken, {
|
|
1882
|
+
signal: options.abortSignal,
|
|
1883
|
+
...(options.nativeMainRefreshDependencies ?? {}),
|
|
1884
|
+
});
|
|
1885
|
+
if (!refreshed) {
|
|
1886
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication") };
|
|
1887
|
+
}
|
|
1888
|
+
const refreshedAuthCtx: CodexAuthContext = {
|
|
1889
|
+
...authCtx,
|
|
1890
|
+
accessToken: refreshed.accessToken,
|
|
1891
|
+
chatgptAccountId: refreshed.chatgptAccountId,
|
|
1892
|
+
};
|
|
1893
|
+
const provider = applyCodexAuthContextToProvider(
|
|
1894
|
+
stripCodexRuntimeProviderFields(route.provider),
|
|
1895
|
+
refreshedAuthCtx,
|
|
1896
|
+
route.codexAccountMode,
|
|
1897
|
+
);
|
|
1898
|
+
const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
1899
|
+
substituteMainCredential,
|
|
1900
|
+
signal: options.abortSignal,
|
|
1901
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
1902
|
+
});
|
|
1903
|
+
return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
return { ok: false, response: nativeMainRefreshFailureResponse(error) };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1644
1909
|
async function resolveSubagentFallbackModelEligibility(args: {
|
|
1645
1910
|
config: OcxConfig;
|
|
1646
1911
|
fallbackChain: readonly string[] | null;
|
|
@@ -1894,6 +2159,7 @@ export async function handleComboResponses(
|
|
|
1894
2159
|
providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
|
|
1895
2160
|
? previousResponseProviderState(requestedPreviousId)
|
|
1896
2161
|
: undefined,
|
|
2162
|
+
recoveredPlaintext: false,
|
|
1897
2163
|
};
|
|
1898
2164
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
1899
2165
|
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
@@ -1923,18 +2189,74 @@ export async function handleComboResponses(
|
|
|
1923
2189
|
return false;
|
|
1924
2190
|
}
|
|
1925
2191
|
};
|
|
2192
|
+
let comboPayloadReadable = false;
|
|
1926
2193
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
1927
|
-
!unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2194
|
+
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2195
|
+
const initialNow = Date.now();
|
|
2196
|
+
let pick: ReturnType<typeof pickComboTarget> = null;
|
|
1928
2197
|
|
|
1929
2198
|
if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
|
|
1930
|
-
|
|
2199
|
+
const recovery = agentTaskRecoveryConfig(config);
|
|
2200
|
+
if (
|
|
2201
|
+
(options.inboundWire ?? "responses") !== "responses"
|
|
2202
|
+
|| !isThreadSpawnRequest(req.headers)
|
|
2203
|
+
|| !recovery
|
|
2204
|
+
|| options.comboAttempt
|
|
2205
|
+
) {
|
|
2206
|
+
discardEncryptedAgentTaskRecovery(
|
|
2207
|
+
req,
|
|
2208
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
2209
|
+
config,
|
|
2210
|
+
{ parentThreadId: inboundClientThreadId },
|
|
2211
|
+
);
|
|
2212
|
+
return unreadableEncryptedAgentTaskResponse();
|
|
2213
|
+
}
|
|
2214
|
+
pick = pickComboTarget(config, comboId, {
|
|
2215
|
+
eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
|
|
2216
|
+
});
|
|
2217
|
+
if (!pick) {
|
|
2218
|
+
discardEncryptedAgentTaskRecovery(
|
|
2219
|
+
req,
|
|
2220
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
2221
|
+
config,
|
|
2222
|
+
{ parentThreadId: inboundClientThreadId },
|
|
2223
|
+
);
|
|
2224
|
+
return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
|
|
2225
|
+
}
|
|
2226
|
+
let recovered = false;
|
|
2227
|
+
try {
|
|
2228
|
+
recovered = await recoverEncryptedAgentTask(
|
|
2229
|
+
req,
|
|
2230
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
2231
|
+
recovery,
|
|
2232
|
+
config,
|
|
2233
|
+
{ parentThreadId: inboundClientThreadId, abortSignal: options.abortSignal },
|
|
2234
|
+
);
|
|
2235
|
+
} catch {
|
|
2236
|
+
recovered = false;
|
|
2237
|
+
}
|
|
2238
|
+
// Recovery has the same in-place input mutation contract as the direct routed path.
|
|
2239
|
+
if (
|
|
2240
|
+
!recovered
|
|
2241
|
+
|| hasUnreadableEncryptedAgentTask((body as { input?: unknown } | undefined)?.input)
|
|
2242
|
+
) {
|
|
2243
|
+
discardEncryptedAgentTaskRecovery(
|
|
2244
|
+
req,
|
|
2245
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
2246
|
+
config,
|
|
2247
|
+
{ parentThreadId: inboundClientThreadId },
|
|
2248
|
+
);
|
|
2249
|
+
return unreadableEncryptedAgentTaskResponse();
|
|
2250
|
+
}
|
|
2251
|
+
comboPayloadReadable = true;
|
|
2252
|
+
comboReplaySnapshot.recoveredPlaintext = true;
|
|
2253
|
+
} else {
|
|
2254
|
+
pick = pickComboTarget(config, comboId, {
|
|
2255
|
+
eligible: target => payloadEligible(target)
|
|
2256
|
+
&& !isComboTargetInCooldown(comboId, target, initialNow),
|
|
2257
|
+
});
|
|
1931
2258
|
}
|
|
1932
2259
|
|
|
1933
|
-
const initialNow = Date.now();
|
|
1934
|
-
let pick = pickComboTarget(config, comboId, {
|
|
1935
|
-
eligible: target => payloadEligible(target)
|
|
1936
|
-
&& !isComboTargetInCooldown(comboId, target, initialNow),
|
|
1937
|
-
});
|
|
1938
2260
|
if (!pick) {
|
|
1939
2261
|
return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
|
|
1940
2262
|
}
|
|
@@ -1988,6 +2310,7 @@ export async function handleComboResponses(
|
|
|
1988
2310
|
attemptRetained = true;
|
|
1989
2311
|
};
|
|
1990
2312
|
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
2313
|
+
let storedPool401ReplayDispatched = false;
|
|
1991
2314
|
const callbackGate = createChildPassthroughCallbackGate(options);
|
|
1992
2315
|
let response: Response;
|
|
1993
2316
|
try {
|
|
@@ -2014,6 +2337,7 @@ export async function handleComboResponses(
|
|
|
2014
2337
|
onCodexAuthContextResolved: value => { resolvedAuth = value; },
|
|
2015
2338
|
setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
|
|
2016
2339
|
onConsumedComboFailure: value => { consumedChildFailure = value; },
|
|
2340
|
+
onStoredPool401ReplayDispatched: () => { storedPool401ReplayDispatched = true; },
|
|
2017
2341
|
onNativePassthroughTerminal: callbackGate.onTerminal,
|
|
2018
2342
|
onNativePassthroughCancel: callbackGate.onCancel,
|
|
2019
2343
|
});
|
|
@@ -2119,6 +2443,10 @@ export async function handleComboResponses(
|
|
|
2119
2443
|
(logCtx.attempts ??= []).push(attempt);
|
|
2120
2444
|
attemptRetained = true;
|
|
2121
2445
|
lastFailure = failure.response;
|
|
2446
|
+
if (storedPool401ReplayDispatched) {
|
|
2447
|
+
adoptFailedChildLog(childLog);
|
|
2448
|
+
return lastFailure;
|
|
2449
|
+
}
|
|
2122
2450
|
if (comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
2123
2451
|
code: failure.upstreamCode,
|
|
2124
2452
|
}) === "stop") {
|
|
@@ -2335,6 +2663,9 @@ async function handleResponsesInner(
|
|
|
2335
2663
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
2336
2664
|
try {
|
|
2337
2665
|
parsed = parseRequest(body);
|
|
2666
|
+
if (options.comboReplaySnapshot?.recoveredPlaintext) {
|
|
2667
|
+
markBodyNonPersistable(parsed._rawBody);
|
|
2668
|
+
}
|
|
2338
2669
|
toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
|
|
2339
2670
|
if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
|
|
2340
2671
|
const providerContinuationCandidate = options.comboReplaySnapshot
|
|
@@ -2417,41 +2748,42 @@ async function handleResponsesInner(
|
|
|
2417
2748
|
logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
|
|
2418
2749
|
logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
|
|
2419
2750
|
|
|
2420
|
-
// Shadow call intercept: rewrite Codex 0.145.0+ helper calls (gpt-5.6-luna).
|
|
2421
|
-
// Ancient clients using gpt-5.4-mini remain configurable via sourceModels.
|
|
2422
|
-
const _sci = config.shadowCallIntercept;
|
|
2423
|
-
if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
|
|
2424
|
-
parsed.modelId,
|
|
2425
|
-
_sci.sourceModels,
|
|
2426
|
-
)) {
|
|
2427
|
-
const _sciOriginal = parsed.modelId;
|
|
2428
|
-
parsed.modelId = _sci.model;
|
|
2429
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
2430
|
-
(parsed._rawBody as { model?: string }).model = _sci.model;
|
|
2431
|
-
}
|
|
2432
|
-
// Force effort to low for shadow/helper calls (matching upstream behavior)
|
|
2433
|
-
parsed.options.reasoning = "low";
|
|
2434
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
2435
|
-
(parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
|
|
2436
|
-
}
|
|
2437
|
-
// Record the operator-configured prefix that matched, NOT the caller's raw model string.
|
|
2438
|
-
// Matching is by prefix, so a caller can append arbitrary text and still intercept; that
|
|
2439
|
-
// raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
|
|
2440
|
-
// that does not recognize every credential family. The prefix is a value the operator
|
|
2441
|
-
// configured, so no caller-controlled string is persisted.
|
|
2442
|
-
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
|
|
2443
|
-
shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
|
|
2444
|
-
);
|
|
2445
|
-
// Helpers must not resume/append into the parent thread's Cursor conversation.
|
|
2446
|
-
parsed._cursorIsolateConversation = true;
|
|
2447
|
-
}
|
|
2448
|
-
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
2449
|
-
|
|
2450
2751
|
let route: RouteResult;
|
|
2451
2752
|
try {
|
|
2452
|
-
|
|
2453
|
-
? routeConcreteModel(config,
|
|
2454
|
-
: routeModel(config,
|
|
2753
|
+
const resolveRoute = (modelId: string) => options.comboAttempt
|
|
2754
|
+
? routeConcreteModel(config, modelId)
|
|
2755
|
+
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
|
|
2756
|
+
const _sci = config.shadowCallIntercept;
|
|
2757
|
+
let shadowRoute: RouteResult | undefined;
|
|
2758
|
+
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
2759
|
+
const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
|
|
2760
|
+
let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
|
|
2761
|
+
try {
|
|
2762
|
+
const resolvedSource = routeConcreteModel(config, parsed.modelId);
|
|
2763
|
+
sourceIdentity = { providerName: resolvedSource.providerName, modelId: sourcePrefix };
|
|
2764
|
+
} catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
|
|
2765
|
+
const targetRoute = resolveRoute(_sci.model);
|
|
2766
|
+
if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
|
|
2767
|
+
const _sciOriginal = parsed.modelId;
|
|
2768
|
+
parsed.modelId = _sci.model;
|
|
2769
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
2770
|
+
(parsed._rawBody as { model?: string }).model = _sci.model;
|
|
2771
|
+
}
|
|
2772
|
+
// Record the operator-configured prefix that matched, NOT the caller's raw model string.
|
|
2773
|
+
// Matching is by prefix, so a caller can append arbitrary text and still intercept; that
|
|
2774
|
+
// raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
|
|
2775
|
+
// that does not recognize every credential family. The prefix is a value the operator
|
|
2776
|
+
// configured, so no caller-controlled string is persisted.
|
|
2777
|
+
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
|
|
2778
|
+
shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
|
|
2779
|
+
);
|
|
2780
|
+
// Helpers must not resume/append into the parent thread's Cursor conversation.
|
|
2781
|
+
parsed._cursorIsolateConversation = true;
|
|
2782
|
+
shadowRoute = targetRoute;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
2786
|
+
route = shadowRoute ?? resolveRoute(parsed.modelId);
|
|
2455
2787
|
logCtx.routeDecision = route.routeDecision;
|
|
2456
2788
|
} catch (err) {
|
|
2457
2789
|
if (err instanceof NoAvailableComboTargetsError) {
|
|
@@ -2530,6 +2862,7 @@ async function handleResponsesInner(
|
|
|
2530
2862
|
previewNow,
|
|
2531
2863
|
codexQuotaScopeForModel(modelId),
|
|
2532
2864
|
{ ...previewSelectionOptions, modelEligibleAccountIds },
|
|
2865
|
+
modelId,
|
|
2533
2866
|
);
|
|
2534
2867
|
const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
|
|
2535
2868
|
route.modelId,
|
|
@@ -2651,6 +2984,7 @@ async function handleResponsesInner(
|
|
|
2651
2984
|
previewNow,
|
|
2652
2985
|
codexQuotaScopeForModel(modelId),
|
|
2653
2986
|
{ ...recoverySelectionOptions, modelEligibleAccountIds },
|
|
2987
|
+
modelId,
|
|
2654
2988
|
);
|
|
2655
2989
|
const recoveryPreviewAccountId = subagentFallbackAccountPreview(
|
|
2656
2990
|
parsed.modelId,
|
|
@@ -2831,7 +3165,12 @@ async function handleResponsesInner(
|
|
|
2831
3165
|
* rotation rather than send a half-applied identity:
|
|
2832
3166
|
*
|
|
2833
3167
|
* - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
|
|
2834
|
-
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
|
|
3168
|
+
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host. The
|
|
3169
|
+
* snapshot value is RESOLVED first: `rotatedProvider` is a clone of the FAILED account's
|
|
3170
|
+
* provider, so passing a bare `undefined` origin let the transport resolver fall through its
|
|
3171
|
+
* own `?? validateCopilotApiBaseUrl(provider.baseUrl)` step to the previous account's host —
|
|
3172
|
+
* pairing B's bearer with A's accepted origin. Login and refresh always persist a resolved
|
|
3173
|
+
* origin, so this fallback protects malformed or manually seeded credentials.
|
|
2835
3174
|
* - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
|
|
2836
3175
|
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
2837
3176
|
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
@@ -2844,7 +3183,7 @@ async function handleResponsesInner(
|
|
|
2844
3183
|
route.providerName,
|
|
2845
3184
|
rotatedProvider,
|
|
2846
3185
|
parsed.options.promptCacheKey,
|
|
2847
|
-
snapshot.apiBaseUrl,
|
|
3186
|
+
resolveCopilotApiBaseUrl(snapshot.apiBaseUrl),
|
|
2848
3187
|
) as OcxProviderConfig;
|
|
2849
3188
|
}
|
|
2850
3189
|
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
@@ -2888,7 +3227,53 @@ async function handleResponsesInner(
|
|
|
2888
3227
|
route.provider = { ...route.provider, apiKey: accessToken };
|
|
2889
3228
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
|
|
2890
3229
|
} else {
|
|
2891
|
-
|
|
3230
|
+
// Prefer the account with known headroom BEFORE the first attempt. Rotation alone
|
|
3231
|
+
// only reacts to a 429, so a turn could open on an account a previous probe already
|
|
3232
|
+
// measured as spent. A null answer means "use the active account", so every provider
|
|
3233
|
+
// without quota evidence keeps the resolution it has today.
|
|
3234
|
+
const preferredAccountId = isGenericFailoverProvider(route.providerName, route.provider)
|
|
3235
|
+
? preferredInitialAccount(config, route.providerName)
|
|
3236
|
+
: null;
|
|
3237
|
+
// Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
|
|
3238
|
+
// rotation site, and rotation sites must apply their credential through
|
|
3239
|
+
// applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
|
|
3240
|
+
// already pairs the snapshot's Kiro metadata, Copilot origin and Antigravity project
|
|
3241
|
+
// with this same bearer, exactly as it does for the active account.
|
|
3242
|
+
let usedPreferredAccount = preferredAccountId !== null;
|
|
3243
|
+
let resolved: OAuthAccessSnapshot;
|
|
3244
|
+
if (preferredAccountId) {
|
|
3245
|
+
try {
|
|
3246
|
+
// `requireUsableAccount` makes a removed OR reauth-flagged account throw from
|
|
3247
|
+
// inside the resolver's own store read. Without it a revoked account resolves
|
|
3248
|
+
// successfully — its credential is still readable — and the request would
|
|
3249
|
+
// dispatch on an account already known to need a fresh login.
|
|
3250
|
+
resolved = await getValidAccessSnapshotForAccount(
|
|
3251
|
+
route.providerName,
|
|
3252
|
+
preferredAccountId,
|
|
3253
|
+
{ requireUsableAccount: true },
|
|
3254
|
+
);
|
|
3255
|
+
} catch {
|
|
3256
|
+
// The roster is read behind a short TTL, so a preferred account can be removed
|
|
3257
|
+
// or flagged for reauth in the window after it was cached. Resolving it then
|
|
3258
|
+
// throws, and a PREFERENCE that turns a healthy request into a 401 is worse
|
|
3259
|
+
// than no preference at all — the active account is still perfectly usable.
|
|
3260
|
+
// Drop the stale roster so the next request re-reads it, and carry on.
|
|
3261
|
+
forgetGenericFailoverRoster(route.providerName);
|
|
3262
|
+
usedPreferredAccount = false;
|
|
3263
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
3264
|
+
}
|
|
3265
|
+
} else {
|
|
3266
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
3267
|
+
}
|
|
3268
|
+
// A Cloud Code Assist account needs its own project. Antigravity's refresh path
|
|
3269
|
+
// tolerates project discovery failing, so a stored account can legitimately have
|
|
3270
|
+
// none — and a PREFERENCE must never turn a working request into an error. Fall
|
|
3271
|
+
// back to the ordinary active-account resolution instead, which is exactly what
|
|
3272
|
+
// would have happened had the preference never existed.
|
|
3273
|
+
if (usedPreferredAccount && route.provider.googleMode === "cloud-code-assist" && !resolved.projectId) {
|
|
3274
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
3275
|
+
usedPreferredAccount = false;
|
|
3276
|
+
}
|
|
2892
3277
|
replayOAuthCredentialSnapshot = {
|
|
2893
3278
|
accountId: resolved.accountId,
|
|
2894
3279
|
generation: resolved.generation,
|
|
@@ -2912,9 +3297,19 @@ async function handleResponsesInner(
|
|
|
2912
3297
|
// Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
|
|
2913
3298
|
// CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
|
|
2914
3299
|
// a fresh token with project metadata re-read from a different credential generation.
|
|
2915
|
-
if (route.provider.googleMode === "cloud-code-assist"
|
|
2916
|
-
|
|
2917
|
-
|
|
3300
|
+
if (route.provider.googleMode === "cloud-code-assist") {
|
|
3301
|
+
// When pre-dispatch chose a DIFFERENT account, the configured project belongs to
|
|
3302
|
+
// the account we did not use, and `!route.provider.project` would skip right past
|
|
3303
|
+
// it — installing B's bearer alongside A's project. That is the #2841 pairing bug
|
|
3304
|
+
// in its original shape, so the preferred-account path replaces the project
|
|
3305
|
+
// unconditionally and refuses to dispatch at all if the chosen account has none.
|
|
3306
|
+
// A project-less preferred account already fell back above, so by here the
|
|
3307
|
+
// preferred path always has one.
|
|
3308
|
+
if (usedPreferredAccount && resolved.projectId) {
|
|
3309
|
+
route.provider = { ...route.provider, project: resolved.projectId };
|
|
3310
|
+
} else if (!route.provider.project && resolved.projectId) {
|
|
3311
|
+
route.provider = { ...route.provider, project: resolved.projectId };
|
|
3312
|
+
}
|
|
2918
3313
|
}
|
|
2919
3314
|
}
|
|
2920
3315
|
} catch (err) {
|
|
@@ -2933,7 +3328,9 @@ async function handleResponsesInner(
|
|
|
2933
3328
|
route.providerName,
|
|
2934
3329
|
route.provider,
|
|
2935
3330
|
parsed.options.promptCacheKey,
|
|
2936
|
-
route.providerName === "github-copilot"
|
|
3331
|
+
route.providerName === "github-copilot" && route.provider.authMode === "oauth"
|
|
3332
|
+
? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
|
|
3333
|
+
: undefined,
|
|
2937
3334
|
);
|
|
2938
3335
|
let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2939
3336
|
const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
@@ -3172,9 +3569,10 @@ async function handleResponsesInner(
|
|
|
3172
3569
|
);
|
|
3173
3570
|
// Hosted calls the PROVIDER runs itself. Gated on the destination actually being xAI, so a
|
|
3174
3571
|
// declaration alone cannot buy the exemption on some other upstream that never serves it.
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3572
|
+
// Provider-executed declarations are authorized from the actual outbound body, after the
|
|
3573
|
+
// adapter has applied destination-specific injection and normalization. Client-executed tool
|
|
3574
|
+
// authority remains bounded to the caller-owned catalog above.
|
|
3575
|
+
const providerExecutedCallTypes = new Set<ProviderExecutedCallType>();
|
|
3178
3576
|
let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
|
|
3179
3577
|
try {
|
|
3180
3578
|
request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
@@ -3239,9 +3637,41 @@ async function handleResponsesInner(
|
|
|
3239
3637
|
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
3240
3638
|
const declaredWireToolNames = new Set<string>();
|
|
3241
3639
|
const declaredNamelessClientCallTypes = new Set<string>();
|
|
3640
|
+
// `buildToolBridgeMaps` creates a bare alias only when the caller selected exactly one
|
|
3641
|
+
// namespaced tool through a bare tool_choice. Restore that request-bounded identity before
|
|
3642
|
+
// authorization checks instead of admitting the bare name into the declared set: for `exec`,
|
|
3643
|
+
// the latter would also authorize the unrelated code-mode helper names.
|
|
3644
|
+
const authorizedBareNamespaceToolAliases: RoutedNamespaceToolAliases = new Map(
|
|
3645
|
+
[...toolBridgeMaps.toolNsMap].flatMap(([alias, identity]) =>
|
|
3646
|
+
alias === identity.name
|
|
3647
|
+
? [[alias, {
|
|
3648
|
+
namespace: identity.namespace,
|
|
3649
|
+
name: identity.name,
|
|
3650
|
+
kind: identity.freeform ? "custom" as const : "function" as const,
|
|
3651
|
+
}] as const]
|
|
3652
|
+
: []
|
|
3653
|
+
),
|
|
3654
|
+
);
|
|
3655
|
+
const restoreAuthorizedBareNamespaceToolCalls = (value: unknown): unknown =>
|
|
3656
|
+
restoreRoutedNamespaceCalls(value, authorizedBareNamespaceToolAliases).value;
|
|
3242
3657
|
let undeclaredToolGuardActive = false;
|
|
3243
3658
|
const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
|
|
3244
3659
|
outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
|
|
3660
|
+
providerExecutedCallTypes.clear();
|
|
3661
|
+
if (isXaiResponsesDestination(route.provider)) {
|
|
3662
|
+
// Preserve the caller-declared authorization recognized by the original classifier, then
|
|
3663
|
+
// add adapter-injected declarations from the actual current-turn outbound catalog.
|
|
3664
|
+
for (const callType of collectProviderExecutedCallTypes(clientToolAuthorizationBody)) {
|
|
3665
|
+
providerExecutedCallTypes.add(callType);
|
|
3666
|
+
}
|
|
3667
|
+
const currentOutboundCatalog = currentTurnWireToolCatalogBody(
|
|
3668
|
+
outboundRequestBody,
|
|
3669
|
+
replayedInputPrefixLength,
|
|
3670
|
+
);
|
|
3671
|
+
for (const callType of collectProviderExecutedCallTypes(currentOutboundCatalog)) {
|
|
3672
|
+
providerExecutedCallTypes.add(callType);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3245
3675
|
declaredWireToolNames.clear();
|
|
3246
3676
|
// With no replay prefix the full outbound body belongs to this turn and its normalized
|
|
3247
3677
|
// aliases are authoritative. A continuation's outbound body still contains historical
|
|
@@ -3267,7 +3697,19 @@ async function handleResponsesInner(
|
|
|
3267
3697
|
// however, the parsed maps also contain historical catalog entries, so only the bounded
|
|
3268
3698
|
// current-turn wire snapshot above may authorize a call.
|
|
3269
3699
|
if (replayedInputPrefixLength === 0) {
|
|
3270
|
-
for (const name of toolBridgeMaps.declaredToolNames)
|
|
3700
|
+
for (const name of toolBridgeMaps.declaredToolNames) {
|
|
3701
|
+
// `buildToolBridgeMaps` also aliases a namespaced tool under its bare name when the
|
|
3702
|
+
// caller's `tool_choice` selected it unambiguously, which the bridge needs to route the
|
|
3703
|
+
// call back. For `exec` alone that alias would also switch on nested-helper
|
|
3704
|
+
// normalization and re-authorize `exec_command`/`shell_command`/`apply_patch`, so it is
|
|
3705
|
+
// admitted here only when the caller's own catalog declared a bare `exec`. Selecting an
|
|
3706
|
+
// MCP `exec` is not a declaration of the code-mode shell tool.
|
|
3707
|
+
if (
|
|
3708
|
+
name === CODE_MODE_EXEC_TOOL_NAME
|
|
3709
|
+
&& !clientDeclaredWireToolNames.has(CODE_MODE_EXEC_TOOL_NAME)
|
|
3710
|
+
) continue;
|
|
3711
|
+
declaredWireToolNames.add(name);
|
|
3712
|
+
}
|
|
3271
3713
|
}
|
|
3272
3714
|
undeclaredToolGuardActive = (
|
|
3273
3715
|
declaredWireToolNames.size > 0
|
|
@@ -3293,7 +3735,7 @@ async function handleResponsesInner(
|
|
|
3293
3735
|
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
3294
3736
|
if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
|
|
3295
3737
|
if (undeclaredToolCallName(
|
|
3296
|
-
payload,
|
|
3738
|
+
restoreAuthorizedBareNamespaceToolCalls(payload),
|
|
3297
3739
|
declaredWireToolNames,
|
|
3298
3740
|
declaredNamelessClientCallTypes,
|
|
3299
3741
|
providerExecutedCallTypes,
|
|
@@ -3305,7 +3747,7 @@ async function handleResponsesInner(
|
|
|
3305
3747
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
3306
3748
|
if (inspectionSawUndeclaredTool) return;
|
|
3307
3749
|
const restoredResponse = restoreRoutedCustomCalls(
|
|
3308
|
-
response,
|
|
3750
|
+
restoreAuthorizedBareNamespaceToolCalls(response),
|
|
3309
3751
|
routedCustomToolNames,
|
|
3310
3752
|
routedCustomToolRepairNames,
|
|
3311
3753
|
declaredWireToolNames,
|
|
@@ -3453,6 +3895,7 @@ async function handleResponsesInner(
|
|
|
3453
3895
|
|
|
3454
3896
|
const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
|
|
3455
3897
|
let oauth401ReplayAttempted = false;
|
|
3898
|
+
let codex401ReplayKind: "main" | "stored" | null = null;
|
|
3456
3899
|
const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
|
|
3457
3900
|
let rateLimitRetries = 0;
|
|
3458
3901
|
const rebuildAndRefetch = async (
|
|
@@ -3523,6 +3966,102 @@ async function handleResponsesInner(
|
|
|
3523
3966
|
// Keep recovery kinds in sync with the generic `recovery:` loop below.
|
|
3524
3967
|
passthroughRecovery: for (;;) {
|
|
3525
3968
|
|
|
3969
|
+
if (
|
|
3970
|
+
upstreamResponse.status === 401
|
|
3971
|
+
&& (authCtx.kind === "main-pool" || authCtx.kind === "pool")
|
|
3972
|
+
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
3973
|
+
&& codex401ReplayKind === null
|
|
3974
|
+
) {
|
|
3975
|
+
codex401ReplayKind = authCtx.kind === "pool" ? "stored" : "main";
|
|
3976
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
|
|
3977
|
+
const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
|
|
3978
|
+
const poolReplay = poolAuthCtx
|
|
3979
|
+
? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
|
|
3980
|
+
: undefined;
|
|
3981
|
+
const replay = poolReplay
|
|
3982
|
+
?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
|
|
3983
|
+
if (!replay.ok) {
|
|
3984
|
+
// Compact already records this; core historically returned without recording,
|
|
3985
|
+
// so a dead grant stayed selectable and every request repeated the same doomed
|
|
3986
|
+
// refresh. Fenced by the generation the 401 belongs to (#2887).
|
|
3987
|
+
if (poolAuthCtx && poolReplay && !poolReplay.ok && poolReplay.quarantine) {
|
|
3988
|
+
recordCodexUpstreamOutcome(config, poolAuthCtx.accountId, 401, {
|
|
3989
|
+
threadId: poolAuthCtx.affinityKey,
|
|
3990
|
+
fixedAccount: poolAuthCtx.fixedAccount,
|
|
3991
|
+
modelId: route.modelId,
|
|
3992
|
+
writerGeneration: poolAuthCtx.writerGeneration,
|
|
3993
|
+
credentialGeneration: poolReplay.quarantineGeneration ?? poolAuthCtx.generation,
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
upstream.abort();
|
|
3997
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
3998
|
+
return replay.response;
|
|
3999
|
+
}
|
|
4000
|
+
authCtx = replay.authCtx;
|
|
4001
|
+
route.provider = replay.provider;
|
|
4002
|
+
selectedForwardHeaders = replay.headers;
|
|
4003
|
+
const replayAdapter = resolveAdapter(
|
|
4004
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
|
|
4005
|
+
config.cacheRetention,
|
|
4006
|
+
);
|
|
4007
|
+
if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
|
|
4008
|
+
upstream.abort();
|
|
4009
|
+
return formatErrorResponse(502, "upstream_error", "Native main refresh changed the provider wire unexpectedly");
|
|
4010
|
+
}
|
|
4011
|
+
bindRouteReasoningReplayScope({
|
|
4012
|
+
parsed,
|
|
4013
|
+
providerName: route.providerName,
|
|
4014
|
+
provider: replay.provider,
|
|
4015
|
+
adapterName: replayAdapter.name,
|
|
4016
|
+
codexAuthContext: authCtx,
|
|
4017
|
+
forwardHeaders: selectedForwardHeaders,
|
|
4018
|
+
});
|
|
4019
|
+
logCtx.providerAdapter = replayAdapter.name;
|
|
4020
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel);
|
|
4021
|
+
try {
|
|
4022
|
+
request = await replayAdapter.buildRequest(parsed, {
|
|
4023
|
+
headers: selectedForwardHeaders,
|
|
4024
|
+
translatorBudget,
|
|
4025
|
+
});
|
|
4026
|
+
refreshRoutedNamespaceToolAliases(request);
|
|
4027
|
+
recordAdapterReasoning(logCtx, request);
|
|
4028
|
+
recordAdapterTier(logCtx, request);
|
|
4029
|
+
refreshUndeclaredToolGuard(request);
|
|
4030
|
+
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, "oauth-401");
|
|
4031
|
+
upstreamResponse = await fetchWithHeaderTimeout(
|
|
4032
|
+
request.url,
|
|
4033
|
+
{ method: request.method, headers: request.headers, body: request.body },
|
|
4034
|
+
upstream.signal,
|
|
4035
|
+
connectMs,
|
|
4036
|
+
parsed.stream,
|
|
4037
|
+
// The replay-dispatched signal is what bounds the rest of this logical request, so it
|
|
4038
|
+
// has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
|
|
4039
|
+
// admission BEFORE calling the executor, so signalling at the call site would spend the
|
|
4040
|
+
// budget even when a rejected pacing wait means nothing reaches the network. Wrapping
|
|
4041
|
+
// the executor moves the signal to the last moment before the send, where a throw from
|
|
4042
|
+
// here on is a genuine transport attempt.
|
|
4043
|
+
storedPoolReplayDispatchNotifier(
|
|
4044
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4045
|
+
providerName: route.providerName,
|
|
4046
|
+
modelId: route.modelId,
|
|
4047
|
+
}),
|
|
4048
|
+
codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
|
|
4049
|
+
),
|
|
4050
|
+
route.provider.authMode === "forward",
|
|
4051
|
+
).then(response => {
|
|
4052
|
+
settleObservedHostResponse();
|
|
4053
|
+
return response;
|
|
4054
|
+
});
|
|
4055
|
+
} catch (err) {
|
|
4056
|
+
return transportFailureResponse(err);
|
|
4057
|
+
} finally {
|
|
4058
|
+
request.releaseBodyObservation?.();
|
|
4059
|
+
}
|
|
4060
|
+
continue passthroughRecovery;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
|
|
4064
|
+
|
|
3526
4065
|
// Native Responses providers return before the generic adapter recovery loop below. Keep
|
|
3527
4066
|
// their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
|
|
3528
4067
|
// rebuilt replay. xAI's current subscription models use this branch now that their official
|
|
@@ -3555,7 +4094,9 @@ async function handleResponsesInner(
|
|
|
3555
4094
|
route.providerName,
|
|
3556
4095
|
{ ...route.provider, apiKey: refreshed.accessToken },
|
|
3557
4096
|
parsed.options.promptCacheKey,
|
|
3558
|
-
route.providerName === "github-copilot"
|
|
4097
|
+
route.providerName === "github-copilot"
|
|
4098
|
+
? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
|
|
4099
|
+
: undefined,
|
|
3559
4100
|
);
|
|
3560
4101
|
route.provider = refreshedProvider;
|
|
3561
4102
|
const refreshedAdapter = resolveAdapter(
|
|
@@ -3720,6 +4261,14 @@ async function handleResponsesInner(
|
|
|
3720
4261
|
}
|
|
3721
4262
|
|
|
3722
4263
|
if (poolRetryOutcome !== undefined) {
|
|
4264
|
+
// A stored Pool 401 spent this request's account budget on its own refresh and replay, so
|
|
4265
|
+
// nothing afterwards may be paid for out of a DIFFERENT account. One flag carries that,
|
|
4266
|
+
// rather than a status check here as well: a quota failure has no same-account move, so
|
|
4267
|
+
// `sameAccountOnly` makes it terminal by refusing the alternate; the gated-model 400
|
|
4268
|
+
// ladder does have one — retrying the account the refreshed roster still grants — and
|
|
4269
|
+
// keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
|
|
4270
|
+
// justify because this flag already produced the identical result.
|
|
4271
|
+
const storedReplaySpent = codex401ReplayKind === "stored";
|
|
3723
4272
|
const retry = await retryCodexPoolOnAlternateAccount({
|
|
3724
4273
|
req,
|
|
3725
4274
|
config,
|
|
@@ -3730,6 +4279,7 @@ async function handleResponsesInner(
|
|
|
3730
4279
|
firstAuthCtx: authCtx,
|
|
3731
4280
|
firstResponse: upstreamResponse,
|
|
3732
4281
|
outcomeStatus: poolRetryOutcome,
|
|
4282
|
+
sameAccountOnly: storedReplaySpent,
|
|
3733
4283
|
upstream,
|
|
3734
4284
|
connectMs,
|
|
3735
4285
|
passthroughEstimate,
|
|
@@ -3845,6 +4395,9 @@ async function handleResponsesInner(
|
|
|
3845
4395
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
3846
4396
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
3847
4397
|
writerGeneration: authCtx.writerGeneration,
|
|
4398
|
+
// Includes a replay's second 401, which is the case that actually retires the
|
|
4399
|
+
// account — fence it on the credential the request was holding.
|
|
4400
|
+
...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
|
|
3848
4401
|
});
|
|
3849
4402
|
}
|
|
3850
4403
|
}
|
|
@@ -3928,6 +4481,9 @@ async function handleResponsesInner(
|
|
|
3928
4481
|
routedNamespaceToolAliases.size > 0
|
|
3929
4482
|
? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
|
|
3930
4483
|
: undefined,
|
|
4484
|
+
authorizedBareNamespaceToolAliases.size > 0
|
|
4485
|
+
? createRoutedNamespaceCallRestoreRewrite(authorizedBareNamespaceToolAliases)
|
|
4486
|
+
: undefined,
|
|
3931
4487
|
hasResponsesItemIdRepair(repairConfig)
|
|
3932
4488
|
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
|
|
3933
4489
|
: undefined,
|
|
@@ -4157,8 +4713,12 @@ async function handleResponsesInner(
|
|
|
4157
4713
|
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
4158
4714
|
routedNamespaceToolAliases,
|
|
4159
4715
|
);
|
|
4160
|
-
const
|
|
4716
|
+
const restoredAuthorizedBareNamespace = restoreRoutedNamespaceCallsInJson(
|
|
4161
4717
|
restoredNamespace,
|
|
4718
|
+
authorizedBareNamespaceToolAliases,
|
|
4719
|
+
);
|
|
4720
|
+
const restored = restoreRoutedCustomCallsInJson(
|
|
4721
|
+
restoredAuthorizedBareNamespace,
|
|
4162
4722
|
routedCustomToolNames,
|
|
4163
4723
|
routedCustomToolRepairNames,
|
|
4164
4724
|
declaredWireToolNames,
|
|
@@ -4484,9 +5044,21 @@ async function handleResponsesInner(
|
|
|
4484
5044
|
// through web-search instead of being swallowed. runTurn adapters never enter this branch.
|
|
4485
5045
|
if (canRunWebSearch && wsPlan) {
|
|
4486
5046
|
parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
5047
|
+
// Resolve the mutable route at send time: a 429 rotation replaces route.provider, so retaining
|
|
5048
|
+
// one pre-rotation providerFetch would keep the old credential and transport pin.
|
|
5049
|
+
const routedProviderFetch = ((input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
|
|
5050
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5051
|
+
providerName: route.providerName,
|
|
5052
|
+
modelId: route.modelId,
|
|
5053
|
+
})(input, init)) as typeof globalThis.fetch;
|
|
4487
5054
|
const wsResponse = await runWithWebSearch({
|
|
4488
5055
|
parsed, adapter,
|
|
4489
|
-
incomingMeta: {
|
|
5056
|
+
incomingMeta: {
|
|
5057
|
+
headers: selectedForwardHeaders,
|
|
5058
|
+
abortSignal: options.abortSignal,
|
|
5059
|
+
translatorBudget,
|
|
5060
|
+
providerFetch: routedProviderFetch,
|
|
5061
|
+
},
|
|
4490
5062
|
backend: wsPlan.backend,
|
|
4491
5063
|
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
4492
5064
|
anthropicSidecar: wsPlan.anthropicSidecar,
|
|
@@ -4738,10 +5310,7 @@ async function handleResponsesInner(
|
|
|
4738
5310
|
// result (#2472). Retrying by default would re-send a turn that may already have had
|
|
4739
5311
|
// billable side effects, so the honest default is observability, not recovery.
|
|
4740
5312
|
: observeEmptyCompletion(eventSource, () => {
|
|
4741
|
-
console.warn(
|
|
4742
|
-
`[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
|
|
4743
|
-
+ "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
|
|
4744
|
-
);
|
|
5313
|
+
console.warn(emptyCompletionNotice(route.providerName, route.modelId));
|
|
4745
5314
|
});
|
|
4746
5315
|
const sseStream = bridgeToResponsesSSE(
|
|
4747
5316
|
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
@@ -4936,6 +5505,13 @@ async function handleResponsesInner(
|
|
|
4936
5505
|
{ headers: { "Content-Type": "application/json" } },
|
|
4937
5506
|
);
|
|
4938
5507
|
}
|
|
5508
|
+
// One request-scoped transient-retry budget owner, declared here so BOTH the initial send
|
|
5509
|
+
// and the later recovery refetches (429, key/account rotation, OAuth replay) share it. A
|
|
5510
|
+
// per-leg budget would let a request that recovers several times multiply upstream load.
|
|
5511
|
+
let transientSendsUsed = 0;
|
|
5512
|
+
const noteTransientSends = (used: number): void => { transientSendsUsed += Math.max(0, used); };
|
|
5513
|
+
const remainingTransientSendBudget = (budget: number): number =>
|
|
5514
|
+
Math.max(1, budget - transientSendsUsed);
|
|
4939
5515
|
try {
|
|
4940
5516
|
initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
4941
5517
|
refreshRoutedNamespaceToolAliases(initialRequest);
|
|
@@ -4990,7 +5566,13 @@ async function handleResponsesInner(
|
|
|
4990
5566
|
// direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
|
|
4991
5567
|
// adapters keep reset-only retry so combo failover still hops on the first 5xx
|
|
4992
5568
|
// instead of burning ~1.2s of same-target retries per hop.
|
|
4993
|
-
|
|
5569
|
+
// #2643: an opted-in key-auth openai-chat provider also gets transient-5xx retry. The
|
|
5570
|
+
// legacy direct-Google exception is preserved exactly; every other adapter still keeps
|
|
5571
|
+
// reset-only semantics so combo failover hops on the first 5xx.
|
|
5572
|
+
const transientPolicy = transientRetryPolicyFor(route.provider);
|
|
5573
|
+
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
5574
|
+
? fetchWithTransientRetry
|
|
5575
|
+
: fetchWithResetRetry;
|
|
4994
5576
|
upstreamResponse = await fetchWithRetryPolicy(
|
|
4995
5577
|
recovery => {
|
|
4996
5578
|
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
|
|
@@ -5004,7 +5586,13 @@ async function handleResponsesInner(
|
|
|
5004
5586
|
modelId: route.modelId,
|
|
5005
5587
|
}));
|
|
5006
5588
|
},
|
|
5007
|
-
{
|
|
5589
|
+
{
|
|
5590
|
+
abortSignal: upstream.signal,
|
|
5591
|
+
label: safeHostLabel(builtInitialRequest.url),
|
|
5592
|
+
...(transientPolicy
|
|
5593
|
+
? { attempts: transientPolicy.attempts, onSendsConsumed: noteTransientSends }
|
|
5594
|
+
: {}),
|
|
5595
|
+
},
|
|
5008
5596
|
);
|
|
5009
5597
|
}
|
|
5010
5598
|
} catch (err) {
|
|
@@ -5093,13 +5681,36 @@ async function handleResponsesInner(
|
|
|
5093
5681
|
}),
|
|
5094
5682
|
});
|
|
5095
5683
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5684
|
+
// #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
|
|
5685
|
+
// opted-in provider's transient-5xx policy applied to the initial send and to
|
|
5686
|
+
// native chat but was silently bypassed here — a 429 that recovered into a
|
|
5687
|
+
// retryable 503 got no retry on the Responses path. Route it through the same
|
|
5688
|
+
// selection, and pass what is LEFT of the request-scoped budget rather than a
|
|
5689
|
+
// fresh one, so a recovery loop cannot multiply total upstream sends.
|
|
5690
|
+
const refetchTransientPolicy = transientRetryPolicyFor(route.provider);
|
|
5691
|
+
const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
|
|
5692
|
+
? fetchWithTransientRetry
|
|
5693
|
+
: fetchWithResetRetry;
|
|
5694
|
+
return await refetchWithPolicy(
|
|
5695
|
+
recoveryKind => fetchWithHeaderTimeout(retryRequest.url,
|
|
5696
|
+
applyUpstreamRecoveryInit({
|
|
5697
|
+
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
5698
|
+
}, recoveryKind), upstream.signal, connectMs, parsed.stream,
|
|
5699
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5700
|
+
providerName: route.providerName,
|
|
5701
|
+
modelId: route.modelId,
|
|
5702
|
+
})),
|
|
5703
|
+
{
|
|
5704
|
+
abortSignal: upstream.signal,
|
|
5705
|
+
label: safeHostLabel(retryRequest.url),
|
|
5706
|
+
...(refetchTransientPolicy
|
|
5707
|
+
? {
|
|
5708
|
+
attempts: remainingTransientSendBudget(refetchTransientPolicy.attempts),
|
|
5709
|
+
onSendsConsumed: noteTransientSends,
|
|
5710
|
+
}
|
|
5711
|
+
: {}),
|
|
5712
|
+
},
|
|
5713
|
+
);
|
|
5103
5714
|
} finally {
|
|
5104
5715
|
retryRequest.releaseBodyObservation?.();
|
|
5105
5716
|
}
|
|
@@ -5142,7 +5753,9 @@ async function handleResponsesInner(
|
|
|
5142
5753
|
route.providerName,
|
|
5143
5754
|
{ ...route.provider, apiKey: refreshed.accessToken },
|
|
5144
5755
|
parsed.options.promptCacheKey,
|
|
5145
|
-
route.providerName === "github-copilot"
|
|
5756
|
+
route.providerName === "github-copilot"
|
|
5757
|
+
? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
|
|
5758
|
+
: undefined,
|
|
5146
5759
|
);
|
|
5147
5760
|
route.provider = refreshedProvider;
|
|
5148
5761
|
invalidateSameTargetRequest();
|
|
@@ -5505,7 +6118,10 @@ async function handleResponsesInner(
|
|
|
5505
6118
|
}
|
|
5506
6119
|
// Same #1851 scope guard as the initial send: transient-5xx retry only for direct
|
|
5507
6120
|
// Google AI Studio; every other adapter keeps reset-only semantics here.
|
|
5508
|
-
const
|
|
6121
|
+
const continuationTransientPolicy = transientRetryPolicyFor(route.provider);
|
|
6122
|
+
const fetchContinuationWithRetryPolicy = (route.provider.adapter === "google" || continuationTransientPolicy)
|
|
6123
|
+
? fetchWithTransientRetry
|
|
6124
|
+
: fetchWithResetRetry;
|
|
5509
6125
|
return await fetchContinuationWithRetryPolicy(
|
|
5510
6126
|
recovery => {
|
|
5511
6127
|
noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
|
|
@@ -5525,7 +6141,19 @@ async function handleResponsesInner(
|
|
|
5525
6141
|
}),
|
|
5526
6142
|
);
|
|
5527
6143
|
},
|
|
5528
|
-
{
|
|
6144
|
+
{
|
|
6145
|
+
abortSignal: upstream.signal,
|
|
6146
|
+
label: safeHostLabel(builtContinuationRequest.url),
|
|
6147
|
+
// Same request-scoped budget as the initial send and the 429/rotation refetches:
|
|
6148
|
+
// a terminal-guard continuation is another leg of ONE request, so handing it a
|
|
6149
|
+
// fresh `attempts` would let one request exceed the configured total-send ceiling.
|
|
6150
|
+
...(continuationTransientPolicy
|
|
6151
|
+
? {
|
|
6152
|
+
attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
|
|
6153
|
+
onSendsConsumed: noteTransientSends,
|
|
6154
|
+
}
|
|
6155
|
+
: {}),
|
|
6156
|
+
},
|
|
5529
6157
|
);
|
|
5530
6158
|
} finally {
|
|
5531
6159
|
builtContinuationRequest.releaseBodyObservation?.();
|