@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -118,18 +118,19 @@ import {
|
|
|
118
118
|
type OAuthAccessSnapshot,
|
|
119
119
|
UnsupportedOAuthProviderError,
|
|
120
120
|
} from "../../oauth";
|
|
121
|
+
import { captureOAuthAccountSelection, commitOAuthAccountSelection, credentialGeneration, getAccountCredentialWithStatus } from "../../oauth/store";
|
|
121
122
|
import {
|
|
122
123
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
123
124
|
anthropicSessionKeyFromParts,
|
|
124
|
-
|
|
125
|
+
commitAnthropicSelectionRouting,
|
|
125
126
|
formatAnthropicProviderForLog,
|
|
126
|
-
|
|
127
|
+
getAnthropicPoolAccessSnapshot,
|
|
127
128
|
getAnthropicPoolRetryAfterSeconds,
|
|
128
129
|
isAnthropicAccountPoolEnabled,
|
|
129
130
|
hasAnthropicFailoverQuorum,
|
|
130
|
-
promoteAnthropicActiveAccount,
|
|
131
131
|
resolveAnthropicAccountForSession,
|
|
132
132
|
rotateAnthropicAccountOn429,
|
|
133
|
+
type AnthropicAccountSelectionReason,
|
|
133
134
|
} from "../../oauth/anthropic-routing";
|
|
134
135
|
import { stampOAuthAccountLabel } from "../../providers/label";
|
|
135
136
|
import {
|
|
@@ -241,6 +242,7 @@ import {
|
|
|
241
242
|
type InboundWire,
|
|
242
243
|
} from "../../providers/registry";
|
|
243
244
|
import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
|
|
245
|
+
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../../providers/api-key-selection";
|
|
244
246
|
import {
|
|
245
247
|
hasKeyPoolFailover,
|
|
246
248
|
rateLimitRetryDelayMs,
|
|
@@ -263,6 +265,7 @@ import {
|
|
|
263
265
|
import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
|
|
264
266
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
265
267
|
import {
|
|
268
|
+
ENCRYPTED_FUNCTION_OUTPUT_REJECTION,
|
|
266
269
|
isRateLimitOrQuotaFailureMessage,
|
|
267
270
|
upstreamErrorMessageFromPayload,
|
|
268
271
|
} from "../../lib/errors";
|
|
@@ -291,6 +294,7 @@ import {
|
|
|
291
294
|
recordAttemptRequestedEffort,
|
|
292
295
|
requestLogSpeedLabel,
|
|
293
296
|
sealRequestAttemptIdentity,
|
|
297
|
+
recordAttemptCredentialSource,
|
|
294
298
|
usageFromResponsesPayload,
|
|
295
299
|
type RequestLogContext,
|
|
296
300
|
} from "../request-log";
|
|
@@ -318,6 +322,7 @@ import {
|
|
|
318
322
|
agentTaskRecoveryConfig,
|
|
319
323
|
discardEncryptedAgentTaskRecovery,
|
|
320
324
|
recoverEncryptedAgentTask,
|
|
325
|
+
restoreCachedEncryptedAgentTasks,
|
|
321
326
|
} from "./agent-task-recovery";
|
|
322
327
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
323
328
|
import {
|
|
@@ -355,7 +360,7 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
|
|
|
355
360
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
356
361
|
import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
|
|
357
362
|
import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
|
|
358
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier } from "./fetch-helpers";
|
|
363
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier, type ProviderFetchOptions } from "./fetch-helpers";
|
|
359
364
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
360
365
|
import {
|
|
361
366
|
acquireUpstreamHostAdmission,
|
|
@@ -367,6 +372,7 @@ import {
|
|
|
367
372
|
upstreamHostHealthKey,
|
|
368
373
|
type UpstreamHostAdmissionLease,
|
|
369
374
|
} from "../../codex/upstream-host-health";
|
|
375
|
+
import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
|
|
370
376
|
import {
|
|
371
377
|
createResponsesSnapshotBlockRewrite,
|
|
372
378
|
hasResponsesSnapshotRepair,
|
|
@@ -380,6 +386,8 @@ import {
|
|
|
380
386
|
} from "../sse-payload-rewrite";
|
|
381
387
|
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
382
388
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
389
|
+
import { collectFunctionCallRepairSchemas, repairFunctionCallsInJson } from "../../responses/function-call-compat";
|
|
390
|
+
import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
|
|
383
391
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
384
392
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
385
393
|
import {
|
|
@@ -638,28 +646,96 @@ const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
|
|
|
638
646
|
"compaction_summary",
|
|
639
647
|
"context_compaction",
|
|
640
648
|
]);
|
|
649
|
+
const FUNCTION_OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output"]);
|
|
650
|
+
// codex-app subagent results replay as agent_message items whose content parts may carry
|
|
651
|
+
// backend-minted encrypted_content; the ChatGPT backend decrypts them in its function-output
|
|
652
|
+
// path, so a cross-identity replay of those parts produces ENCRYPTED_FUNCTION_OUTPUT_REJECTION.
|
|
653
|
+
const AGENT_MESSAGE_TYPE = "agent_message";
|
|
654
|
+
|
|
655
|
+
function encryptedFunctionOutputParts(output: unknown): boolean {
|
|
656
|
+
return Array.isArray(output) && output.some(part => (
|
|
657
|
+
part !== null
|
|
658
|
+
&& typeof part === "object"
|
|
659
|
+
&& !Array.isArray(part)
|
|
660
|
+
&& (part as { type?: unknown }).type === "encrypted_content"
|
|
661
|
+
&& typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
662
|
+
&& (part as { encrypted_content: string }).encrypted_content.length > 0
|
|
663
|
+
));
|
|
664
|
+
}
|
|
641
665
|
|
|
642
|
-
function
|
|
643
|
-
if (!bodyText) return
|
|
666
|
+
function outboundResponsesInput(bodyText: string | undefined): unknown[] | undefined {
|
|
667
|
+
if (!bodyText) return undefined;
|
|
644
668
|
try {
|
|
645
669
|
const body = JSON.parse(bodyText) as unknown;
|
|
646
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
670
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
|
647
671
|
const input = (body as { input?: unknown }).input;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
672
|
+
return Array.isArray(input) ? input : undefined;
|
|
673
|
+
} catch {
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function outboundResponsesBodyCarriesEncryptedFunctionOutput(bodyText: string | undefined): boolean {
|
|
679
|
+
const input = outboundResponsesInput(bodyText);
|
|
680
|
+
if (!input) return false;
|
|
681
|
+
return input.some(item => {
|
|
682
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) return false;
|
|
683
|
+
const candidate = item as { type?: unknown; output?: unknown; content?: unknown };
|
|
684
|
+
const type = String(candidate.type ?? "");
|
|
685
|
+
if (FUNCTION_OUTPUT_TYPES.has(type) && encryptedFunctionOutputParts(candidate.output)) return true;
|
|
686
|
+
return type === AGENT_MESSAGE_TYPE && encryptedFunctionOutputParts(candidate.content);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
|
|
691
|
+
const input = outboundResponsesInput(bodyText);
|
|
692
|
+
if (!input) return false;
|
|
693
|
+
return input.some(item => {
|
|
694
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
695
|
+
const candidate = item as { type?: unknown; encrypted_content?: unknown; output?: unknown };
|
|
696
|
+
if (
|
|
697
|
+
typeof candidate.type === "string"
|
|
698
|
+
&& OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
|
|
699
|
+
&& typeof candidate.encrypted_content === "string"
|
|
700
|
+
&& candidate.encrypted_content.length > 0
|
|
701
|
+
) return true;
|
|
702
|
+
if (
|
|
703
|
+
typeof candidate.type === "string"
|
|
704
|
+
&& FUNCTION_OUTPUT_TYPES.has(candidate.type)
|
|
705
|
+
&& encryptedFunctionOutputParts(candidate.output)
|
|
706
|
+
) return true;
|
|
707
|
+
return candidate.type === AGENT_MESSAGE_TYPE
|
|
708
|
+
&& encryptedFunctionOutputParts((candidate as { content?: unknown }).content);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
|
|
713
|
+
if (bodyText.trim() === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
714
|
+
try {
|
|
715
|
+
const payload = JSON.parse(bodyText) as unknown;
|
|
716
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
717
|
+
const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
|
|
718
|
+
if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
719
|
+
if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
720
|
+
if (record.error === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
721
|
+
return record.error !== null
|
|
722
|
+
&& typeof record.error === "object"
|
|
723
|
+
&& !Array.isArray(record.error)
|
|
724
|
+
&& (record.error as { message?: unknown }).message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
657
725
|
} catch {
|
|
658
726
|
return false;
|
|
659
727
|
}
|
|
660
728
|
}
|
|
661
729
|
|
|
662
730
|
function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
|
|
731
|
+
if (isEncryptedFunctionOutputRejection(bodyText)) return true;
|
|
732
|
+
try {
|
|
733
|
+
if (upstreamErrorMessageFromPayload(JSON.parse(bodyText) as unknown) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
/* invalid JSON bodies fall through to the exact nested envelope checks */
|
|
738
|
+
}
|
|
663
739
|
try {
|
|
664
740
|
const payload = JSON.parse(bodyText) as unknown;
|
|
665
741
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
@@ -704,8 +780,13 @@ export function shouldAttemptOpaqueBlobRecovery(args: {
|
|
|
704
780
|
errorBody: string;
|
|
705
781
|
alreadyAttempted: boolean;
|
|
706
782
|
}): boolean {
|
|
707
|
-
|
|
708
|
-
|
|
783
|
+
const acceptedStatus = (args.status >= 400 && args.status < 500)
|
|
784
|
+
|| (
|
|
785
|
+
args.status === 502
|
|
786
|
+
&& outboundResponsesBodyCarriesEncryptedFunctionOutput(args.outboundBody)
|
|
787
|
+
&& isEncryptedFunctionOutputRejection(args.errorBody)
|
|
788
|
+
);
|
|
789
|
+
return acceptedStatus
|
|
709
790
|
&& args.adapterName === "openai-responses"
|
|
710
791
|
&& !args.alreadyAttempted
|
|
711
792
|
&& outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
|
|
@@ -721,7 +802,7 @@ async function opaqueBlobRejectionBodyForRecovery(
|
|
|
721
802
|
): Promise<string | undefined> {
|
|
722
803
|
if (
|
|
723
804
|
response.status < 400
|
|
724
|
-
|| response.status >= 500
|
|
805
|
+
|| (response.status >= 500 && response.status !== 502)
|
|
725
806
|
|| adapterName !== "openai-responses"
|
|
726
807
|
|| alreadyAttempted
|
|
727
808
|
|| !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
|
|
@@ -798,6 +879,50 @@ function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedU
|
|
|
798
879
|
|
|
799
880
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
800
881
|
parsed._stripReasoningEncryptedContent = true;
|
|
882
|
+
const rawBody = parsed._rawBody;
|
|
883
|
+
if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return;
|
|
884
|
+
const input = (rawBody as { input?: unknown }).input;
|
|
885
|
+
if (!Array.isArray(input)) return;
|
|
886
|
+
const stripEncryptedParts = (parts: unknown[]): unknown[] => {
|
|
887
|
+
let changed = false;
|
|
888
|
+
const stripped = parts.map(part => {
|
|
889
|
+
if (
|
|
890
|
+
part !== null
|
|
891
|
+
&& typeof part === "object"
|
|
892
|
+
&& !Array.isArray(part)
|
|
893
|
+
&& (part as { type?: unknown }).type === "encrypted_content"
|
|
894
|
+
&& typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
895
|
+
&& (part as { encrypted_content: string }).encrypted_content.length > 0
|
|
896
|
+
) {
|
|
897
|
+
changed = true;
|
|
898
|
+
return { type: "input_text", text: "[encrypted content omitted]" };
|
|
899
|
+
}
|
|
900
|
+
return part;
|
|
901
|
+
});
|
|
902
|
+
return changed ? stripped : parts;
|
|
903
|
+
};
|
|
904
|
+
const strippedInput = input.map(item => {
|
|
905
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
906
|
+
const record = item as Record<string, unknown>;
|
|
907
|
+
const type = String(record.type ?? "");
|
|
908
|
+
if (FUNCTION_OUTPUT_TYPES.has(type) && Array.isArray(record.output)) {
|
|
909
|
+
const output = stripEncryptedParts(record.output);
|
|
910
|
+
return output !== record.output ? { ...record, output } : item;
|
|
911
|
+
}
|
|
912
|
+
if (type === AGENT_MESSAGE_TYPE && Array.isArray(record.content)) {
|
|
913
|
+
const content = stripEncryptedParts(record.content);
|
|
914
|
+
return content !== record.content ? { ...record, content } : item;
|
|
915
|
+
}
|
|
916
|
+
return item;
|
|
917
|
+
});
|
|
918
|
+
Object.assign(rawBody, { input: strippedInput });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function resetStreamedOpaqueBlobLogContext(logCtx: RequestLogContext): void {
|
|
922
|
+
delete logCtx.upstreamError;
|
|
923
|
+
delete logCtx.terminalHttpStatus;
|
|
924
|
+
delete logCtx.terminalErrorCode;
|
|
925
|
+
delete logCtx.terminalIncompleteReason;
|
|
801
926
|
}
|
|
802
927
|
|
|
803
928
|
type OpaqueBlobRecoveryGuard = { attempted: boolean };
|
|
@@ -1300,6 +1425,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1300
1425
|
retryAdapter.name,
|
|
1301
1426
|
logCtx.accountLogLabel,
|
|
1302
1427
|
);
|
|
1428
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
|
|
1303
1429
|
|
|
1304
1430
|
const retrySameConfirmedAccount = outcomeStatus === 400
|
|
1305
1431
|
&& ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)
|
|
@@ -2403,19 +2529,11 @@ export async function handleComboResponses(
|
|
|
2403
2529
|
let comboPayloadReadable = false;
|
|
2404
2530
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
2405
2531
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2406
|
-
|
|
2407
|
-
let
|
|
2408
|
-
const
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
now?: number;
|
|
2412
|
-
}) => pickComboTargetWithWait(config, comboId, {
|
|
2413
|
-
...pickOptions,
|
|
2414
|
-
waitForCooldownMs: combo.waitForCooldownMs,
|
|
2415
|
-
abortSignal: options.abortSignal,
|
|
2416
|
-
});
|
|
2417
|
-
|
|
2418
|
-
if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
|
|
2532
|
+
let encryptedTaskRecoveryAttempted = false;
|
|
2533
|
+
let storedPool401ReplayDispatched = false;
|
|
2534
|
+
const recoverUnreadableEncryptedTask = async (): Promise<boolean> => {
|
|
2535
|
+
if (encryptedTaskRecoveryAttempted) return false;
|
|
2536
|
+
encryptedTaskRecoveryAttempted = true;
|
|
2419
2537
|
const recovery = agentTaskRecoveryConfig(config);
|
|
2420
2538
|
if (
|
|
2421
2539
|
(options.inboundWire ?? "responses") !== "responses"
|
|
@@ -2429,19 +2547,7 @@ export async function handleComboResponses(
|
|
|
2429
2547
|
config,
|
|
2430
2548
|
{ parentThreadId: inboundClientThreadId },
|
|
2431
2549
|
);
|
|
2432
|
-
return
|
|
2433
|
-
}
|
|
2434
|
-
pick = await pickWithWait({ now: initialNow });
|
|
2435
|
-
if (!pick) {
|
|
2436
|
-
discardEncryptedAgentTaskRecovery(
|
|
2437
|
-
req,
|
|
2438
|
-
(body as { input?: unknown } | undefined)?.input,
|
|
2439
|
-
config,
|
|
2440
|
-
{ parentThreadId: inboundClientThreadId },
|
|
2441
|
-
);
|
|
2442
|
-
return options.abortSignal?.aborted
|
|
2443
|
-
? clientCancelledResponse()
|
|
2444
|
-
: comboUnavailable(comboId);
|
|
2550
|
+
return false;
|
|
2445
2551
|
}
|
|
2446
2552
|
let recovered = false;
|
|
2447
2553
|
try {
|
|
@@ -2466,15 +2572,45 @@ export async function handleComboResponses(
|
|
|
2466
2572
|
config,
|
|
2467
2573
|
{ parentThreadId: inboundClientThreadId },
|
|
2468
2574
|
);
|
|
2469
|
-
return
|
|
2575
|
+
return false;
|
|
2470
2576
|
}
|
|
2471
2577
|
comboPayloadReadable = true;
|
|
2472
2578
|
comboReplaySnapshot.recoveredPlaintext = true;
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2579
|
+
return true;
|
|
2580
|
+
};
|
|
2581
|
+
const initialNow = Date.now();
|
|
2582
|
+
const pickWithWait = (pickOptions: {
|
|
2583
|
+
exclude?: Iterable<string>;
|
|
2584
|
+
eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
|
|
2585
|
+
now?: number;
|
|
2586
|
+
}) => pickComboTargetWithWait(config, comboId, {
|
|
2587
|
+
...pickOptions,
|
|
2588
|
+
waitForCooldownMs: combo.waitForCooldownMs,
|
|
2589
|
+
abortSignal: options.abortSignal,
|
|
2590
|
+
});
|
|
2591
|
+
let pick = await pickWithWait({
|
|
2592
|
+
eligible: payloadEligible,
|
|
2593
|
+
now: initialNow,
|
|
2594
|
+
});
|
|
2595
|
+
|
|
2596
|
+
if (unreadableEncryptedAgentTask && !pick) {
|
|
2597
|
+
pick = await pickWithWait({ now: initialNow });
|
|
2598
|
+
if (!pick) {
|
|
2599
|
+
discardEncryptedAgentTaskRecovery(
|
|
2600
|
+
req,
|
|
2601
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
2602
|
+
config,
|
|
2603
|
+
{ parentThreadId: inboundClientThreadId },
|
|
2604
|
+
);
|
|
2605
|
+
return options.abortSignal?.aborted
|
|
2606
|
+
? clientCancelledResponse()
|
|
2607
|
+
: comboUnavailable(comboId);
|
|
2608
|
+
}
|
|
2609
|
+
if (!(await recoverUnreadableEncryptedTask())) {
|
|
2610
|
+
return options.abortSignal?.aborted
|
|
2611
|
+
? clientCancelledResponse()
|
|
2612
|
+
: unreadableEncryptedAgentTaskResponse();
|
|
2613
|
+
}
|
|
2478
2614
|
}
|
|
2479
2615
|
|
|
2480
2616
|
if (!pick) {
|
|
@@ -2532,7 +2668,6 @@ export async function handleComboResponses(
|
|
|
2532
2668
|
attemptRetained = true;
|
|
2533
2669
|
};
|
|
2534
2670
|
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
2535
|
-
let storedPool401ReplayDispatched = false;
|
|
2536
2671
|
const callbackGate = createChildPassthroughCallbackGate(options);
|
|
2537
2672
|
let response: Response;
|
|
2538
2673
|
try {
|
|
@@ -2665,13 +2800,36 @@ export async function handleComboResponses(
|
|
|
2665
2800
|
(logCtx.attempts ??= []).push(attempt);
|
|
2666
2801
|
attemptRetained = true;
|
|
2667
2802
|
lastFailure = failure.response;
|
|
2803
|
+
const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
2804
|
+
code: failure.upstreamCode,
|
|
2805
|
+
});
|
|
2668
2806
|
if (storedPool401ReplayDispatched) {
|
|
2807
|
+
if (failureDecision === "hop" && unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
2808
|
+
const recoveredTarget = await pickWithWait({
|
|
2809
|
+
exclude: pick.attempted,
|
|
2810
|
+
eligible: target => {
|
|
2811
|
+
try {
|
|
2812
|
+
const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
|
|
2813
|
+
return route.codexAccountMode === undefined
|
|
2814
|
+
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
2815
|
+
} catch {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
},
|
|
2819
|
+
});
|
|
2820
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2821
|
+
if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
|
|
2822
|
+
pick = recoveredTarget;
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2826
|
+
}
|
|
2827
|
+
// Keep the spent Pool budget sticky even after a recovered routed child:
|
|
2828
|
+
// no later failure may reopen ordinary combo/native account hopping.
|
|
2669
2829
|
adoptFailedChildLog(childLog);
|
|
2670
2830
|
return lastFailure;
|
|
2671
2831
|
}
|
|
2672
|
-
if (
|
|
2673
|
-
code: failure.upstreamCode,
|
|
2674
|
-
}) === "stop") {
|
|
2832
|
+
if (failureDecision === "stop") {
|
|
2675
2833
|
adoptFailedChildLog(childLog);
|
|
2676
2834
|
if (
|
|
2677
2835
|
failure.response.status === 413
|
|
@@ -2685,6 +2843,7 @@ export async function handleComboResponses(
|
|
|
2685
2843
|
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
2686
2844
|
);
|
|
2687
2845
|
const failureNow = Date.now();
|
|
2846
|
+
const attemptedTargets = pick.attempted;
|
|
2688
2847
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
2689
2848
|
retryAfter: failure.retryAfter,
|
|
2690
2849
|
resetAt: failure.resetAt,
|
|
@@ -2708,6 +2867,18 @@ export async function handleComboResponses(
|
|
|
2708
2867
|
});
|
|
2709
2868
|
}
|
|
2710
2869
|
if (!pick) {
|
|
2870
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2871
|
+
if (unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
2872
|
+
const recoveredTarget = await pickWithWait({
|
|
2873
|
+
exclude: attemptedTargets,
|
|
2874
|
+
now: failureNow,
|
|
2875
|
+
});
|
|
2876
|
+
if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
|
|
2877
|
+
pick = recoveredTarget;
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
// Waiting or recovery may have observed cancellation after the check above.
|
|
2711
2882
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2712
2883
|
adoptFailedChildLog(childLog);
|
|
2713
2884
|
}
|
|
@@ -2952,6 +3123,7 @@ async function handleResponsesInner(
|
|
|
2952
3123
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
2953
3124
|
try {
|
|
2954
3125
|
parsed = parseRequest(body);
|
|
3126
|
+
parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
|
|
2955
3127
|
// Captured before any parser mutates it, so both grammars see the client's id.
|
|
2956
3128
|
const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
|
|
2957
3129
|
if (fastRow) {
|
|
@@ -3234,14 +3406,18 @@ async function handleResponsesInner(
|
|
|
3234
3406
|
inboundWire === "responses"
|
|
3235
3407
|
&&
|
|
3236
3408
|
threadSpawn
|
|
3237
|
-
&& unreadableEncryptedAgentTask
|
|
3238
3409
|
&& agentTaskRecovery
|
|
3239
3410
|
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
3240
3411
|
&& !options.comboAttempt
|
|
3241
3412
|
&& !canPassThroughEncryptedV2AgentTask(route, inboundWire)
|
|
3242
3413
|
) {
|
|
3243
|
-
let recovered =
|
|
3244
|
-
|
|
3414
|
+
let recovered = restoreCachedEncryptedAgentTasks(
|
|
3415
|
+
req, (body as { input?: unknown } | undefined)?.input, config, { parentThreadId },
|
|
3416
|
+
) > 0;
|
|
3417
|
+
unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
3418
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
3419
|
+
);
|
|
3420
|
+
if (unreadableEncryptedAgentTask) try {
|
|
3245
3421
|
recovered = await recoverEncryptedAgentTask(
|
|
3246
3422
|
req,
|
|
3247
3423
|
(body as { input?: unknown } | undefined)?.input,
|
|
@@ -3266,6 +3442,7 @@ async function handleResponsesInner(
|
|
|
3266
3442
|
"_providerContinuationOwner",
|
|
3267
3443
|
"_cursorConversationId",
|
|
3268
3444
|
"_clientThreadId",
|
|
3445
|
+
"_promptCacheKeyIsSharedCohort",
|
|
3269
3446
|
"_cursorClientThreadId",
|
|
3270
3447
|
"_reasoningReplayScope",
|
|
3271
3448
|
"_cursorIsolateConversation",
|
|
@@ -3275,6 +3452,7 @@ async function handleResponsesInner(
|
|
|
3275
3452
|
(reparsed as unknown as Record<string, unknown>)[key] = parsed[key];
|
|
3276
3453
|
}
|
|
3277
3454
|
}
|
|
3455
|
+
bindTurnTerminationScope(reparsed, resolvedConversationId);
|
|
3278
3456
|
parsed = reparsed;
|
|
3279
3457
|
// The recovery mutated `body.input` in place, so `_rawBody` now carries decrypted task
|
|
3280
3458
|
// text. Bar it from the continuation cache before any recording path can reach it —
|
|
@@ -3483,8 +3661,12 @@ async function handleResponsesInner(
|
|
|
3483
3661
|
|
|
3484
3662
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
3485
3663
|
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
3486
|
-
const isOAuth401ReplayProvider = (
|
|
3487
|
-
|
|
3664
|
+
const isOAuth401ReplayProvider = (
|
|
3665
|
+
route.providerName === "xai"
|
|
3666
|
+
|| route.providerName === "github-copilot"
|
|
3667
|
+
|| route.providerName === "kiro"
|
|
3668
|
+
|| route.providerName === "google-antigravity"
|
|
3669
|
+
) && route.provider.authMode === "oauth";
|
|
3488
3670
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
3489
3671
|
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
3490
3672
|
let anthropicPoolAccountId: string | null = null;
|
|
@@ -3493,6 +3675,82 @@ async function handleResponsesInner(
|
|
|
3493
3675
|
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
3494
3676
|
let genericFailoverAccountId: string | null = null;
|
|
3495
3677
|
let genericFailovers = 0;
|
|
3678
|
+
let oauthSelection = route.provider.authMode === "oauth"
|
|
3679
|
+
? captureOAuthAccountSelection(route.providerName) : null;
|
|
3680
|
+
let servingOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
3681
|
+
// These owners also serve early passthrough and sidecar sends. A dispatch-time
|
|
3682
|
+
// rebuild must update every later builder, without entering a later block's TDZ.
|
|
3683
|
+
let adapter: ProviderAdapter;
|
|
3684
|
+
let activeAdapter: ProviderAdapter;
|
|
3685
|
+
let runTurnAdapter: ProviderAdapter;
|
|
3686
|
+
let sameTargetRequest: AdapterRequest | undefined;
|
|
3687
|
+
let sameTargetParsed: OcxParsedRequest | undefined;
|
|
3688
|
+
let sameTargetToken = 0;
|
|
3689
|
+
let transportToken = 0;
|
|
3690
|
+
let imageTierBias = 0;
|
|
3691
|
+
const invalidateSameTargetRequest = (): void => { transportToken += 1; };
|
|
3692
|
+
type DispatchBinding =
|
|
3693
|
+
| { kind: "oauth"; selection: NonNullable<typeof oauthSelection>; snapshot: OAuthAccessSnapshot }
|
|
3694
|
+
| { kind: "api-key"; provider: OcxProviderConfig };
|
|
3695
|
+
const requestBindings = new WeakMap<AdapterRequest, DispatchBinding>();
|
|
3696
|
+
const adapterBindings = new WeakMap<ProviderAdapter, DispatchBinding>();
|
|
3697
|
+
const rawRunTurns = new WeakMap<ProviderAdapter, NonNullable<ProviderAdapter["runTurn"]>>();
|
|
3698
|
+
const commitResolvedOAuthSelection = async (
|
|
3699
|
+
candidate: OAuthAccessSnapshot,
|
|
3700
|
+
proactive = false,
|
|
3701
|
+
anthropicReason?: AnthropicAccountSelectionReason,
|
|
3702
|
+
): Promise<OAuthAccessSnapshot | null> => {
|
|
3703
|
+
const maxSelectionAttempts = 3;
|
|
3704
|
+
for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
|
|
3705
|
+
if (!oauthSelection) return null;
|
|
3706
|
+
const proactiveEnabled = route.providerName === "anthropic"
|
|
3707
|
+
? isAnthropicAccountPoolEnabled(config)
|
|
3708
|
+
: (config.providers[route.providerName]?.oauthAccountFailover?.enabled
|
|
3709
|
+
?? config.oauthAccountFailover?.enabled) === true;
|
|
3710
|
+
if (proactive && candidate.accountId !== oauthSelection.accountId && !proactiveEnabled) {
|
|
3711
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
3712
|
+
if (!oauthSelection) return null;
|
|
3713
|
+
candidate = route.providerName === "anthropic"
|
|
3714
|
+
? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
|
|
3715
|
+
: await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
|
|
3716
|
+
}
|
|
3717
|
+
const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
|
|
3718
|
+
expectedSelection: oauthSelection,
|
|
3719
|
+
expectedCredentialGeneration: candidate.generation,
|
|
3720
|
+
requireUsableAccount: true,
|
|
3721
|
+
});
|
|
3722
|
+
if (committed) {
|
|
3723
|
+
if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
|
|
3724
|
+
candidate.accountId, oauthSelection, committed,
|
|
3725
|
+
{ config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation },
|
|
3726
|
+
)) return null;
|
|
3727
|
+
oauthSelection = committed;
|
|
3728
|
+
servingOAuthSnapshot = candidate;
|
|
3729
|
+
forgetGenericFailoverRoster(route.providerName);
|
|
3730
|
+
return candidate;
|
|
3731
|
+
}
|
|
3732
|
+
// A newer manual choice wins over this request's old proposal, including A→B→A.
|
|
3733
|
+
// Resolve that choice, not the rejected candidate, before trying admission again.
|
|
3734
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
3735
|
+
if (!oauthSelection) return null;
|
|
3736
|
+
candidate = route.providerName === "anthropic"
|
|
3737
|
+
? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
|
|
3738
|
+
: await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
|
|
3739
|
+
if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
|
|
3740
|
+
}
|
|
3741
|
+
return null;
|
|
3742
|
+
};
|
|
3743
|
+
const refreshResolvedOAuthSelection = async (sent: OAuthAccessSnapshot): Promise<OAuthAccessSnapshot> => {
|
|
3744
|
+
const current = captureOAuthAccountSelection(route.providerName);
|
|
3745
|
+
const unchanged = current?.accountId === oauthSelection?.accountId
|
|
3746
|
+
&& current?.revision === oauthSelection?.revision;
|
|
3747
|
+
const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
|
|
3748
|
+
const admitted = await commitResolvedOAuthSelection(candidate);
|
|
3749
|
+
if (!admitted) throw new Error("OAuth selection changed during credential recovery");
|
|
3750
|
+
genericFailoverAccountId = admitted.accountId;
|
|
3751
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, admitted.accountId);
|
|
3752
|
+
return admitted;
|
|
3753
|
+
};
|
|
3496
3754
|
/**
|
|
3497
3755
|
* Config generation captured where the serving credential is RESOLVED, not where the
|
|
3498
3756
|
* quota is written. A streaming turn is a long await, so a generation captured at write
|
|
@@ -3521,11 +3779,14 @@ async function handleResponsesInner(
|
|
|
3521
3779
|
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
3522
3780
|
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
3523
3781
|
*/
|
|
3524
|
-
const applyFailoverSnapshot = (
|
|
3782
|
+
const applyFailoverSnapshot = async (
|
|
3525
3783
|
snapshot: OAuthAccessSnapshot,
|
|
3526
3784
|
retryParsed: OcxParsedRequest = parsed,
|
|
3527
|
-
): boolean => {
|
|
3785
|
+
): Promise<boolean> => {
|
|
3528
3786
|
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
3787
|
+
const committed = await commitResolvedOAuthSelection(snapshot);
|
|
3788
|
+
if (!committed) return false;
|
|
3789
|
+
snapshot = committed;
|
|
3529
3790
|
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
3530
3791
|
if (route.providerName === "github-copilot") {
|
|
3531
3792
|
rotatedProvider = resolveProviderTransport(
|
|
@@ -3549,8 +3810,149 @@ async function handleResponsesInner(
|
|
|
3549
3810
|
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
3550
3811
|
// needed -- and putting it anywhere else would let one of the three drift.
|
|
3551
3812
|
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
|
|
3813
|
+
if (route.providerName === "anthropic") {
|
|
3814
|
+
anthropicPoolAccountId = snapshot.accountId;
|
|
3815
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", snapshot.accountId, config);
|
|
3816
|
+
} else {
|
|
3817
|
+
genericFailoverAccountId = snapshot.accountId;
|
|
3818
|
+
}
|
|
3819
|
+
sentOAuthSnapshot = snapshot;
|
|
3820
|
+
replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
|
|
3552
3821
|
return true;
|
|
3553
3822
|
};
|
|
3823
|
+
const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
|
|
3824
|
+
if (route.provider.authMode === "forward") return true;
|
|
3825
|
+
if (!binding) return false;
|
|
3826
|
+
if (binding.kind === "api-key") return providerApiKeySelectionIsCurrent(config, route.providerName, binding.provider);
|
|
3827
|
+
const selected = captureOAuthAccountSelection(route.providerName);
|
|
3828
|
+
const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
|
|
3829
|
+
return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
|
|
3830
|
+
&& !!row && !row.needsReauth && row.credential.expires > Date.now()
|
|
3831
|
+
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
3832
|
+
};
|
|
3833
|
+
const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
|
|
3834
|
+
const resolved = resolveAdapter(provider, retention);
|
|
3835
|
+
if (route.provider.authMode === "forward") return resolved;
|
|
3836
|
+
const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
|
|
3837
|
+
? oauthSelection && servingOAuthSnapshot
|
|
3838
|
+
? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
|
|
3839
|
+
: undefined
|
|
3840
|
+
: { kind: "api-key", provider: { ...route.provider } };
|
|
3841
|
+
if (binding) adapterBindings.set(resolved, binding);
|
|
3842
|
+
const build = resolved.buildRequest.bind(resolved);
|
|
3843
|
+
resolved.buildRequest = async (requestParsed, incoming) => {
|
|
3844
|
+
const request = await build(requestParsed, incoming);
|
|
3845
|
+
// Capture at adapter creation, never from mutable serving state after an await.
|
|
3846
|
+
if (binding) requestBindings.set(request, binding);
|
|
3847
|
+
return request;
|
|
3848
|
+
};
|
|
3849
|
+
if (resolved.runTurn) {
|
|
3850
|
+
rawRunTurns.set(resolved, resolved.runTurn.bind(resolved));
|
|
3851
|
+
resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
|
|
3852
|
+
}
|
|
3853
|
+
return resolved;
|
|
3854
|
+
};
|
|
3855
|
+
const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
3856
|
+
if (route.provider.authMode === "oauth") {
|
|
3857
|
+
if (!servingOAuthSnapshot || !await applyFailoverSnapshot(servingOAuthSnapshot, requestParsed)) {
|
|
3858
|
+
throw new Error("OAuth account selection changed before dispatch");
|
|
3859
|
+
}
|
|
3860
|
+
} else {
|
|
3861
|
+
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
|
|
3862
|
+
if (!current) throw new Error("API key selection is unavailable before dispatch");
|
|
3863
|
+
route.provider = current;
|
|
3864
|
+
}
|
|
3865
|
+
adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
|
|
3866
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
3867
|
+
);
|
|
3868
|
+
invalidateSameTargetRequest();
|
|
3869
|
+
return adapter;
|
|
3870
|
+
};
|
|
3871
|
+
const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
3872
|
+
requestParsed._cursorIdentityScope = undefined;
|
|
3873
|
+
requestParsed._cursorConversationId = undefined;
|
|
3874
|
+
if (requestParsed._providerContinuation?.cursor) {
|
|
3875
|
+
const { cursor: _oldCursor, ...rest } = requestParsed._providerContinuation;
|
|
3876
|
+
requestParsed._providerContinuation = rest;
|
|
3877
|
+
}
|
|
3878
|
+
return refreshDispatchAdapter(requestParsed);
|
|
3879
|
+
};
|
|
3880
|
+
const runSelectedTurn = async (
|
|
3881
|
+
selectedAdapter: ProviderAdapter,
|
|
3882
|
+
...[requestParsed, incoming, emit]: Parameters<NonNullable<ProviderAdapter["runTurn"]>>
|
|
3883
|
+
): Promise<void> => {
|
|
3884
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3885
|
+
if (!selectionIsCurrent(adapterBindings.get(selectedAdapter))) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
3886
|
+
const binding = adapterBindings.get(selectedAdapter);
|
|
3887
|
+
const run = rawRunTurns.get(selectedAdapter);
|
|
3888
|
+
if (!run) throw new Error("Selected provider no longer supports this turn transport");
|
|
3889
|
+
let sent = false;
|
|
3890
|
+
let refused = false;
|
|
3891
|
+
// Both main and image-loop callers already acquired the initial pacing slot.
|
|
3892
|
+
// Subsequent physical messages retain this adapter/credential and are paced normally.
|
|
3893
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3894
|
+
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
3895
|
+
beforeDispatch: () => {
|
|
3896
|
+
if (sent) return;
|
|
3897
|
+
if (!selectionIsCurrent(binding)) {
|
|
3898
|
+
refused = true;
|
|
3899
|
+
throw new Error("Account selection changed before the first turn dispatch");
|
|
3900
|
+
}
|
|
3901
|
+
sent = true;
|
|
3902
|
+
},
|
|
3903
|
+
});
|
|
3904
|
+
try {
|
|
3905
|
+
await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
|
|
3906
|
+
} catch (error) {
|
|
3907
|
+
if (!refused) throw error;
|
|
3908
|
+
}
|
|
3909
|
+
if (!refused) return;
|
|
3910
|
+
// The adapter may map the guard's exception to an error event. Neither that
|
|
3911
|
+
// event nor a refused send may escape before retrying the newly selected account.
|
|
3912
|
+
selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
3913
|
+
}
|
|
3914
|
+
throw new Error("Account selection changed repeatedly before turn dispatch");
|
|
3915
|
+
};
|
|
3916
|
+
const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
|
|
3917
|
+
if (route.provider.authMode === "forward") return undefined;
|
|
3918
|
+
return async (input, init, execute) => {
|
|
3919
|
+
let destination = input;
|
|
3920
|
+
let dispatchInit = init;
|
|
3921
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3922
|
+
if (selectionIsCurrent(requestBindings.get(wireRequest))) {
|
|
3923
|
+
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
|
|
3924
|
+
return fetchImpl(destination, dispatchInit);
|
|
3925
|
+
}
|
|
3926
|
+
const nextAdapter = await refreshDispatchAdapter(requestParsed);
|
|
3927
|
+
const rebuilt = await nextAdapter.buildRequest(requestParsed, {
|
|
3928
|
+
headers: selectedForwardHeaders, translatorBudget,
|
|
3929
|
+
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
3930
|
+
});
|
|
3931
|
+
const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
|
|
3932
|
+
if (!bodySize.admitted) {
|
|
3933
|
+
rebuilt.releaseBodyObservation?.();
|
|
3934
|
+
return formatErrorResponse(413, "outbound_body_too_large", describeOutboundBodyRefusal(bodySize));
|
|
3935
|
+
}
|
|
3936
|
+
const headers = new Headers(dispatchInit.headers);
|
|
3937
|
+
for (const name of Object.keys(wireRequest.headers)) headers.delete(name);
|
|
3938
|
+
for (const [name, value] of Object.entries(rebuilt.headers)) headers.set(name, value);
|
|
3939
|
+
wireRequest.releaseBodyObservation?.();
|
|
3940
|
+
Object.assign(wireRequest, rebuilt);
|
|
3941
|
+
const binding = requestBindings.get(rebuilt);
|
|
3942
|
+
if (binding) requestBindings.set(wireRequest, binding);
|
|
3943
|
+
else requestBindings.delete(wireRequest);
|
|
3944
|
+
sameTargetRequest = wireRequest;
|
|
3945
|
+
sameTargetParsed = requestParsed;
|
|
3946
|
+
sameTargetToken = transportToken;
|
|
3947
|
+
destination = rebuilt.url;
|
|
3948
|
+
dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
|
|
3949
|
+
bindRouteReasoningReplayScope({ parsed: requestParsed, providerName: route.providerName, provider: route.provider,
|
|
3950
|
+
adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
|
|
3951
|
+
// The next iteration validates synchronously and calls fetch in that same turn.
|
|
3952
|
+
}
|
|
3953
|
+
throw new Error("OAuth account selection changed repeatedly before dispatch");
|
|
3954
|
+
};
|
|
3955
|
+
};
|
|
3554
3956
|
const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
|
|
3555
3957
|
? anthropicSessionKeyFromParts({
|
|
3556
3958
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
@@ -3576,12 +3978,11 @@ async function handleResponsesInner(
|
|
|
3576
3978
|
}
|
|
3577
3979
|
return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
|
|
3578
3980
|
}
|
|
3579
|
-
const
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
|
|
3981
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(selection.accountId), true, selection.reason);
|
|
3982
|
+
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
3983
|
+
anthropicPoolAccountId = admitted.accountId;
|
|
3984
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
3985
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
3585
3986
|
} else {
|
|
3586
3987
|
// Prefer the account with known headroom BEFORE the first attempt. Rotation alone
|
|
3587
3988
|
// only reacts to a 429, so a turn could open on an account a previous probe already
|
|
@@ -3630,6 +4031,10 @@ async function handleResponsesInner(
|
|
|
3630
4031
|
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
3631
4032
|
usedPreferredAccount = false;
|
|
3632
4033
|
}
|
|
4034
|
+
const admitted = await commitResolvedOAuthSelection(resolved, true);
|
|
4035
|
+
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
4036
|
+
if (admitted.accountId !== resolved.accountId) usedPreferredAccount = true;
|
|
4037
|
+
resolved = admitted;
|
|
3633
4038
|
replayOAuthCredentialSnapshot = {
|
|
3634
4039
|
accountId: resolved.accountId,
|
|
3635
4040
|
generation: resolved.generation,
|
|
@@ -3662,22 +4067,11 @@ async function handleResponsesInner(
|
|
|
3662
4067
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
3663
4068
|
parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
|
|
3664
4069
|
}
|
|
3665
|
-
//
|
|
3666
|
-
//
|
|
3667
|
-
// a fresh token with project metadata re-read from a different credential generation.
|
|
4070
|
+
// Project identity belongs to the admitted account on EVERY request, including
|
|
4071
|
+
// the request after a pool transition made that account the persisted active one.
|
|
3668
4072
|
if (route.provider.googleMode === "cloud-code-assist") {
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
// it — installing B's bearer alongside A's project. That is the #2841 pairing bug
|
|
3672
|
-
// in its original shape, so the preferred-account path replaces the project
|
|
3673
|
-
// unconditionally and refuses to dispatch at all if the chosen account has none.
|
|
3674
|
-
// A project-less preferred account already fell back above, so by here the
|
|
3675
|
-
// preferred path always has one.
|
|
3676
|
-
if (usedPreferredAccount && resolved.projectId) {
|
|
3677
|
-
route.provider = { ...route.provider, project: resolved.projectId };
|
|
3678
|
-
} else if (!route.provider.project && resolved.projectId) {
|
|
3679
|
-
route.provider = { ...route.provider, project: resolved.projectId };
|
|
3680
|
-
}
|
|
4073
|
+
if (!resolved.projectId) return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist account project is unavailable")));
|
|
4074
|
+
route.provider = { ...route.provider, project: resolved.projectId };
|
|
3681
4075
|
}
|
|
3682
4076
|
}
|
|
3683
4077
|
} catch (err) {
|
|
@@ -3719,7 +4113,7 @@ async function handleResponsesInner(
|
|
|
3719
4113
|
logCtx.provider = route.providerName;
|
|
3720
4114
|
delete logCtx.accountLogLabel;
|
|
3721
4115
|
}
|
|
3722
|
-
|
|
4116
|
+
adapter = resolveSelectionAdapter(adapterProvider, config.cacheRetention);
|
|
3723
4117
|
bindRouteReasoningReplayScope({
|
|
3724
4118
|
parsed,
|
|
3725
4119
|
providerName: route.providerName,
|
|
@@ -3747,7 +4141,8 @@ async function handleResponsesInner(
|
|
|
3747
4141
|
(logCtx.attempts ??= []).push(attempt);
|
|
3748
4142
|
}
|
|
3749
4143
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
3750
|
-
|
|
4144
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider, adapter.name);
|
|
4145
|
+
runTurnAdapter = adapter;
|
|
3751
4146
|
if (adapter.runTurn) {
|
|
3752
4147
|
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
3753
4148
|
}
|
|
@@ -3976,6 +4371,9 @@ async function handleResponsesInner(
|
|
|
3976
4371
|
}
|
|
3977
4372
|
throw error;
|
|
3978
4373
|
}
|
|
4374
|
+
const functionRepairSchemas = isCanonicalOpenAiForwardProvider(route.provider)
|
|
4375
|
+
? new Map()
|
|
4376
|
+
: collectFunctionCallRepairSchemas(clientToolAuthorizationBody);
|
|
3979
4377
|
if (!isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
3980
4378
|
for (const name of request.convertedRoutedCustomToolNames ?? []) {
|
|
3981
4379
|
if (
|
|
@@ -4041,6 +4439,14 @@ async function handleResponsesInner(
|
|
|
4041
4439
|
);
|
|
4042
4440
|
const restoreAuthorizedBareNamespaceToolCalls = (value: unknown): unknown =>
|
|
4043
4441
|
restoreRoutedNamespaceCalls(value, authorizedBareNamespaceToolAliases).value;
|
|
4442
|
+
const normalizeFunctionCompletionJson = (text: string): string => {
|
|
4443
|
+
const snapshot = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
|
|
4444
|
+
? repairResponsesSnapshotJson(text, outboundRequestBody)
|
|
4445
|
+
: text;
|
|
4446
|
+
// Sparse gateways need completion status inferred before schema repair can
|
|
4447
|
+
// distinguish completed arguments from in-progress placeholders.
|
|
4448
|
+
return repairFunctionCallsInJson(backfillResponsesFieldsJson(snapshot), functionRepairSchemas);
|
|
4449
|
+
};
|
|
4044
4450
|
let undeclaredToolGuardActive = false;
|
|
4045
4451
|
const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
|
|
4046
4452
|
outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
|
|
@@ -4153,12 +4559,15 @@ async function handleResponsesInner(
|
|
|
4153
4559
|
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
4154
4560
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
4155
4561
|
if (inspectionSawUndeclaredTool) return;
|
|
4156
|
-
const
|
|
4157
|
-
restoreAuthorizedBareNamespaceToolCalls(response),
|
|
4562
|
+
const restored = restoreRoutedCustomCalls(
|
|
4563
|
+
restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value),
|
|
4158
4564
|
routedCustomToolNames,
|
|
4159
4565
|
routedCustomToolRepairNames,
|
|
4160
4566
|
declaredWireToolNames,
|
|
4161
|
-
).value
|
|
4567
|
+
).value;
|
|
4568
|
+
const restoredResponse = (functionRepairSchemas.size > 0
|
|
4569
|
+
? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
|
|
4570
|
+
: restored) as { id?: unknown; output?: unknown; status?: unknown };
|
|
4162
4571
|
if (
|
|
4163
4572
|
undeclaredToolGuardActive
|
|
4164
4573
|
&& undeclaredToolCallNameInResponse(
|
|
@@ -4334,6 +4743,7 @@ async function handleResponsesInner(
|
|
|
4334
4743
|
body: request.body,
|
|
4335
4744
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
4336
4745
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4746
|
+
dispatchOverride: oauthDispatch(request),
|
|
4337
4747
|
providerName: route.providerName,
|
|
4338
4748
|
modelId: route.modelId,
|
|
4339
4749
|
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
@@ -4364,7 +4774,7 @@ async function handleResponsesInner(
|
|
|
4364
4774
|
const rebuildAndRefetch = async (
|
|
4365
4775
|
recovery: AttemptRecoveryKind,
|
|
4366
4776
|
): Promise<Response | { failed: Response }> => {
|
|
4367
|
-
const retryAdapter =
|
|
4777
|
+
const retryAdapter = resolveSelectionAdapter(
|
|
4368
4778
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4369
4779
|
config.cacheRetention,
|
|
4370
4780
|
);
|
|
@@ -4398,6 +4808,7 @@ async function handleResponsesInner(
|
|
|
4398
4808
|
retryAdapter.name,
|
|
4399
4809
|
logCtx.accountLogLabel,
|
|
4400
4810
|
);
|
|
4811
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
|
|
4401
4812
|
const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
|
|
4402
4813
|
if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
|
|
4403
4814
|
try {
|
|
@@ -4410,6 +4821,7 @@ async function handleResponsesInner(
|
|
|
4410
4821
|
body: request.body,
|
|
4411
4822
|
}, innerRecovery), upstream.signal, connectMs, parsed.stream,
|
|
4412
4823
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4824
|
+
dispatchOverride: oauthDispatch(request),
|
|
4413
4825
|
providerName: route.providerName,
|
|
4414
4826
|
modelId: route.modelId,
|
|
4415
4827
|
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
@@ -4468,7 +4880,7 @@ async function handleResponsesInner(
|
|
|
4468
4880
|
authCtx = replay.authCtx;
|
|
4469
4881
|
route.provider = replay.provider;
|
|
4470
4882
|
selectedForwardHeaders = replay.headers;
|
|
4471
|
-
const replayAdapter =
|
|
4883
|
+
const replayAdapter = resolveSelectionAdapter(
|
|
4472
4884
|
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
|
|
4473
4885
|
config.cacheRetention,
|
|
4474
4886
|
);
|
|
@@ -4486,6 +4898,7 @@ async function handleResponsesInner(
|
|
|
4486
4898
|
});
|
|
4487
4899
|
logCtx.providerAdapter = replayAdapter.name;
|
|
4488
4900
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel);
|
|
4901
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, replayAdapter.name);
|
|
4489
4902
|
try {
|
|
4490
4903
|
request = await replayAdapter.buildRequest(parsed, {
|
|
4491
4904
|
headers: selectedForwardHeaders,
|
|
@@ -4514,6 +4927,7 @@ async function handleResponsesInner(
|
|
|
4514
4927
|
// here on is a genuine transport attempt.
|
|
4515
4928
|
storedPoolReplayDispatchNotifier(
|
|
4516
4929
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
4930
|
+
dispatchOverride: oauthDispatch(request),
|
|
4517
4931
|
providerName: route.providerName,
|
|
4518
4932
|
modelId: route.modelId,
|
|
4519
4933
|
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
@@ -4551,12 +4965,17 @@ async function handleResponsesInner(
|
|
|
4551
4965
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
4552
4966
|
let refreshed: OAuthAccessSnapshot;
|
|
4553
4967
|
try {
|
|
4554
|
-
refreshed = await
|
|
4968
|
+
refreshed = await refreshResolvedOAuthSelection(sentOAuthSnapshot);
|
|
4555
4969
|
} catch (err) {
|
|
4556
4970
|
upstream.abort();
|
|
4557
4971
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
4558
4972
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
4559
4973
|
}
|
|
4974
|
+
if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
|
|
4975
|
+
upstream.abort();
|
|
4976
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
4977
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
|
|
4978
|
+
}
|
|
4560
4979
|
sentOAuthSnapshot = refreshed;
|
|
4561
4980
|
replayOAuthCredentialSnapshot = {
|
|
4562
4981
|
accountId: refreshed.accountId,
|
|
@@ -4567,14 +4986,18 @@ async function handleResponsesInner(
|
|
|
4567
4986
|
}
|
|
4568
4987
|
const refreshedProvider = resolveProviderTransport(
|
|
4569
4988
|
route.providerName,
|
|
4570
|
-
{
|
|
4989
|
+
{
|
|
4990
|
+
...route.provider,
|
|
4991
|
+
apiKey: refreshed.accessToken,
|
|
4992
|
+
...(refreshed.projectId ? { project: refreshed.projectId } : {}),
|
|
4993
|
+
},
|
|
4571
4994
|
parsed.options.promptCacheKey,
|
|
4572
4995
|
route.providerName === "github-copilot"
|
|
4573
4996
|
? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
|
|
4574
4997
|
: undefined,
|
|
4575
4998
|
);
|
|
4576
4999
|
route.provider = refreshedProvider;
|
|
4577
|
-
const refreshedAdapter =
|
|
5000
|
+
const refreshedAdapter = resolveSelectionAdapter(
|
|
4578
5001
|
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
4579
5002
|
config.cacheRetention,
|
|
4580
5003
|
);
|
|
@@ -4596,6 +5019,7 @@ async function handleResponsesInner(
|
|
|
4596
5019
|
refreshedAdapter.name,
|
|
4597
5020
|
logCtx.accountLogLabel,
|
|
4598
5021
|
);
|
|
5022
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
|
|
4599
5023
|
try {
|
|
4600
5024
|
request = await refreshedAdapter.buildRequest(parsed, {
|
|
4601
5025
|
headers: selectedForwardHeaders,
|
|
@@ -4623,6 +5047,7 @@ async function handleResponsesInner(
|
|
|
4623
5047
|
body: request.body,
|
|
4624
5048
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
4625
5049
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5050
|
+
dispatchOverride: oauthDispatch(request),
|
|
4626
5051
|
providerName: route.providerName,
|
|
4627
5052
|
modelId: route.modelId,
|
|
4628
5053
|
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
@@ -4661,13 +5086,10 @@ async function handleResponsesInner(
|
|
|
4661
5086
|
try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
|
|
4662
5087
|
catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
|
|
4663
5088
|
}
|
|
4664
|
-
if (snapshot && applyFailoverSnapshot(snapshot)) {
|
|
4665
|
-
genericFailoverAccountId = snapshot.accountId;
|
|
5089
|
+
if (snapshot && await applyFailoverSnapshot(snapshot)) {
|
|
4666
5090
|
genericFailovers += 1;
|
|
4667
|
-
sentOAuthSnapshot = snapshot;
|
|
4668
|
-
replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
|
|
4669
5091
|
route.provider = resolveProviderTransport(
|
|
4670
|
-
route.providerName, route.provider, parsed.options.promptCacheKey,
|
|
5092
|
+
route.providerName, route.provider, parsed.options.promptCacheKey, sentOAuthSnapshot?.apiBaseUrl,
|
|
4671
5093
|
);
|
|
4672
5094
|
bindRouteReasoningReplayScope({
|
|
4673
5095
|
parsed, providerName: route.providerName, provider: route.provider,
|
|
@@ -4725,6 +5147,7 @@ async function handleResponsesInner(
|
|
|
4725
5147
|
body: request.body,
|
|
4726
5148
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
4727
5149
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5150
|
+
dispatchOverride: oauthDispatch(request),
|
|
4728
5151
|
providerName: route.providerName,
|
|
4729
5152
|
modelId: route.modelId,
|
|
4730
5153
|
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
@@ -4851,6 +5274,48 @@ async function handleResponsesInner(
|
|
|
4851
5274
|
upstreamResponse = opaqueBlobRecovery.response;
|
|
4852
5275
|
continue passthroughRecovery;
|
|
4853
5276
|
}
|
|
5277
|
+
|
|
5278
|
+
const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5279
|
+
const streamedFunctionOutputCandidate = upstreamResponse.ok
|
|
5280
|
+
&& !!upstreamResponse.body
|
|
5281
|
+
&& (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
|
|
5282
|
+
&& !opaqueBlobRecoveryGuard.attempted
|
|
5283
|
+
&& outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
|
|
5284
|
+
if (streamedFunctionOutputCandidate) {
|
|
5285
|
+
const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
|
|
5286
|
+
const preflight = await preflightComboStreamResponse(upstreamResponse, preflightLog,
|
|
5287
|
+
payload => {
|
|
5288
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
5289
|
+
const type = (payload as { type?: unknown }).type;
|
|
5290
|
+
return (type === "error" || type === "response.failed" || type === "response.incomplete")
|
|
5291
|
+
&& upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
5292
|
+
}, {
|
|
5293
|
+
allowMissingContentType: !recoveryContentType && parsed.stream,
|
|
5294
|
+
replayReadErrors: true,
|
|
5295
|
+
});
|
|
5296
|
+
if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
|
|
5297
|
+
upstreamResponse = preflight.response;
|
|
5298
|
+
if (preflight.kind === "failed") {
|
|
5299
|
+
const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
|
|
5300
|
+
response: upstreamResponse,
|
|
5301
|
+
outboundBody: request.body,
|
|
5302
|
+
adapterName: adapter.name,
|
|
5303
|
+
parsed,
|
|
5304
|
+
guard: opaqueBlobRecoveryGuard,
|
|
5305
|
+
signal: upstream.signal,
|
|
5306
|
+
}, rebuildAndRefetch);
|
|
5307
|
+
if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
|
|
5308
|
+
if (streamedOpaqueRecovery.kind === "recovered") {
|
|
5309
|
+
resetStreamedOpaqueBlobLogContext(logCtx);
|
|
5310
|
+
upstreamResponse = streamedOpaqueRecovery.response;
|
|
5311
|
+
continue passthroughRecovery;
|
|
5312
|
+
}
|
|
5313
|
+
logCtx.upstreamError = preflightLog.upstreamError;
|
|
5314
|
+
logCtx.terminalHttpStatus = preflightLog.terminalHttpStatus;
|
|
5315
|
+
logCtx.terminalErrorCode = preflightLog.terminalErrorCode;
|
|
5316
|
+
logCtx.terminalIncompleteReason = preflightLog.terminalIncompleteReason;
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
4854
5319
|
break;
|
|
4855
5320
|
}
|
|
4856
5321
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
@@ -5004,6 +5469,12 @@ async function handleResponsesInner(
|
|
|
5004
5469
|
)
|
|
5005
5470
|
: upstreamResponse.body;
|
|
5006
5471
|
const repairConfig = route.provider.responsesItemIdRepair;
|
|
5472
|
+
// Grok Build renders deltas live but reconstructs its durable assistant
|
|
5473
|
+
// turn from the completed response snapshot. Native Responses streams
|
|
5474
|
+
// may instead carry the complete items in output_item.done, so the
|
|
5475
|
+
// explicit Grok compatibility marker enables strict terminal-only repair.
|
|
5476
|
+
// The provider's broader snapshot/lifecycle repair remains opt-in.
|
|
5477
|
+
const grokClientSnapshotRepairEnabled = logCtx.surface === "grok";
|
|
5007
5478
|
const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
|
|
5008
5479
|
const githubCopilotRepairEnabled = route.providerName === "github-copilot";
|
|
5009
5480
|
const responseModelRewrite = parsed._responseModelId !== undefined
|
|
@@ -5052,10 +5523,16 @@ async function handleResponsesInner(
|
|
|
5052
5523
|
githubCopilotRepairEnabled
|
|
5053
5524
|
? createGithubCopilotResponsesBlockRewrite(translatorBudget)
|
|
5054
5525
|
: undefined,
|
|
5526
|
+
grokClientSnapshotRepairEnabled
|
|
5527
|
+
? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
|
|
5528
|
+
: undefined,
|
|
5055
5529
|
snapshotRepairEnabled
|
|
5056
5530
|
? createResponsesSnapshotBlockRewrite(outboundRequestBody, translatorBudget)
|
|
5057
5531
|
: undefined,
|
|
5058
5532
|
createResponsesFieldBackfillBlockRewrite(),
|
|
5533
|
+
functionRepairSchemas.size > 0
|
|
5534
|
+
? createResponsesFunctionToolRepairBlockRewrite(functionRepairSchemas, translatorBudget)
|
|
5535
|
+
: undefined,
|
|
5059
5536
|
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
5060
5537
|
// compare the names the client will actually receive against the declared catalog.
|
|
5061
5538
|
undeclaredToolGuardActive
|
|
@@ -5130,11 +5607,14 @@ async function handleResponsesInner(
|
|
|
5130
5607
|
...(clientBlockRewrite
|
|
5131
5608
|
? { rewriteBlocks: clientBlockRewrite }
|
|
5132
5609
|
: {}),
|
|
5133
|
-
onSynthetic: kind => {
|
|
5610
|
+
onSynthetic: (kind, reason) => {
|
|
5134
5611
|
if (!reportNativeTerminal) return;
|
|
5135
5612
|
if (kind === "incomplete") {
|
|
5136
5613
|
logCtx.terminalSource = "synthetic";
|
|
5137
5614
|
reportNativeTerminal("incomplete");
|
|
5615
|
+
} else if (reason === "upstream_error") {
|
|
5616
|
+
logCtx.terminalSource = "synthetic";
|
|
5617
|
+
reportNativeTerminal("failed", logCtx.terminalHttpStatus ?? 502);
|
|
5138
5618
|
} else {
|
|
5139
5619
|
logCtx.transportPhase = "mid_stream";
|
|
5140
5620
|
logCtx.terminalSource = "synthetic";
|
|
@@ -5147,6 +5627,7 @@ async function handleResponsesInner(
|
|
|
5147
5627
|
}, {
|
|
5148
5628
|
clientGoneSignal: options.abortSignal,
|
|
5149
5629
|
...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
|
|
5630
|
+
...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
|
|
5150
5631
|
});
|
|
5151
5632
|
// When selected, this relay closes response.completed even if upstream
|
|
5152
5633
|
// keeps the connection alive. Marked Codex WS traffic, Windows
|
|
@@ -5229,7 +5710,12 @@ async function handleResponsesInner(
|
|
|
5229
5710
|
const rewrittenBody = clientBlockRewrite !== undefined
|
|
5230
5711
|
? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
|
|
5231
5712
|
: nativeBody;
|
|
5232
|
-
const clientBody = relaySseWithFailedTail(
|
|
5713
|
+
const clientBody = relaySseWithFailedTail(
|
|
5714
|
+
rewrittenBody,
|
|
5715
|
+
upstream,
|
|
5716
|
+
reason => clientGone.abort(reason),
|
|
5717
|
+
{ upstreamError: logCtx.upstreamError },
|
|
5718
|
+
);
|
|
5233
5719
|
return markNativePassthroughSseResponse(new Response(clientBody, {
|
|
5234
5720
|
status: upstreamResponse.status,
|
|
5235
5721
|
headers,
|
|
@@ -5273,12 +5759,10 @@ async function handleResponsesInner(
|
|
|
5273
5759
|
restored,
|
|
5274
5760
|
routedToolSearchNames,
|
|
5275
5761
|
);
|
|
5276
|
-
const repaired =
|
|
5277
|
-
? repairResponsesSnapshotJson(restoredToolSearch, outboundRequestBody)
|
|
5278
|
-
: restoredToolSearch;
|
|
5762
|
+
const repaired = normalizeFunctionCompletionJson(restoredToolSearch);
|
|
5279
5763
|
const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
|
|
5280
|
-
? rewriteResponsesModelJson(
|
|
5281
|
-
:
|
|
5764
|
+
? rewriteResponsesModelJson(repaired, parsed._responseModelId)
|
|
5765
|
+
: repaired;
|
|
5282
5766
|
// The bounded-JSON answer bypasses the SSE payload rewrite, so content-
|
|
5283
5767
|
// channel reasoning needs the same normalization here for the plain
|
|
5284
5768
|
// JSON answer and every reframed-SSE variant built from clientJson.
|
|
@@ -5483,9 +5967,8 @@ async function handleResponsesInner(
|
|
|
5483
5967
|
if (!nextAccountId) return null;
|
|
5484
5968
|
try {
|
|
5485
5969
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
5486
|
-
genericFailoverAccountId = nextAccountId;
|
|
5487
5970
|
genericFailovers += 1;
|
|
5488
|
-
if (!applyFailoverSnapshot(snapshot)) return null;
|
|
5971
|
+
if (!await applyFailoverSnapshot(snapshot)) return null;
|
|
5489
5972
|
} catch {
|
|
5490
5973
|
return null;
|
|
5491
5974
|
}
|
|
@@ -5509,12 +5992,12 @@ async function handleResponsesInner(
|
|
|
5509
5992
|
// carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
|
|
5510
5993
|
// local-cli credential rule. Both existing Anthropic rotation sites apply the token the
|
|
5511
5994
|
// same way.
|
|
5512
|
-
const
|
|
5513
|
-
|
|
5995
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
5996
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
5997
|
+
anthropicPoolAccountId = admitted.accountId;
|
|
5514
5998
|
anthropicPoolFailovers += 1;
|
|
5515
|
-
route.provider = { ...route.provider, apiKey: accessToken };
|
|
5516
|
-
|
|
5517
|
-
logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
|
|
5999
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
6000
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
5518
6001
|
} catch {
|
|
5519
6002
|
return null;
|
|
5520
6003
|
}
|
|
@@ -5523,7 +6006,7 @@ async function handleResponsesInner(
|
|
|
5523
6006
|
// credential. The 429 is terminal for this sidecar turn.
|
|
5524
6007
|
return null;
|
|
5525
6008
|
}
|
|
5526
|
-
const rotatedAdapter =
|
|
6009
|
+
const rotatedAdapter = resolveSelectionAdapter(
|
|
5527
6010
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5528
6011
|
config.cacheRetention,
|
|
5529
6012
|
);
|
|
@@ -5607,6 +6090,13 @@ async function handleResponsesInner(
|
|
|
5607
6090
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
5608
6091
|
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
5609
6092
|
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
6093
|
+
fetchForRequest: (request, iterParsed) => {
|
|
6094
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6095
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
6096
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
6097
|
+
});
|
|
6098
|
+
return fetch.unpacedFetch ?? fetch;
|
|
6099
|
+
},
|
|
5610
6100
|
onRequestBuilt: request => {
|
|
5611
6101
|
recordAdapterReasoning(logCtx, request);
|
|
5612
6102
|
recordAdapterTier(logCtx, request);
|
|
@@ -5667,6 +6157,10 @@ async function handleResponsesInner(
|
|
|
5667
6157
|
})(input, init)) as typeof globalThis.fetch;
|
|
5668
6158
|
const wsResponse = await runWithWebSearch({
|
|
5669
6159
|
parsed, adapter,
|
|
6160
|
+
fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6161
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
6162
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
6163
|
+
}),
|
|
5670
6164
|
incomingMeta: {
|
|
5671
6165
|
headers: selectedForwardHeaders,
|
|
5672
6166
|
abortSignal: options.abortSignal,
|
|
@@ -5742,6 +6236,13 @@ async function handleResponsesInner(
|
|
|
5742
6236
|
const queue = createAdapterEventQueue({
|
|
5743
6237
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
5744
6238
|
});
|
|
6239
|
+
const refreshRunTurnSelection = async (): Promise<void> => {
|
|
6240
|
+
if (selectionIsCurrent(adapterBindings.get(runTurnAdapter))) return;
|
|
6241
|
+
await refreshRunTurnAdapter(parsed);
|
|
6242
|
+
bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
|
|
6243
|
+
adapterName: runTurnAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
|
|
6244
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, runTurnAdapter.name, logCtx.accountLogLabel);
|
|
6245
|
+
};
|
|
5745
6246
|
// Initial admission must settle before the streaming Response commits HTTP 200.
|
|
5746
6247
|
// Let the outer Responses facade preserve the local retryable-429 contract.
|
|
5747
6248
|
try {
|
|
@@ -5765,6 +6266,7 @@ async function handleResponsesInner(
|
|
|
5765
6266
|
if (!pacingSlotAcquired) {
|
|
5766
6267
|
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
5767
6268
|
}
|
|
6269
|
+
await refreshRunTurnSelection();
|
|
5768
6270
|
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
|
|
5769
6271
|
const runTurnProviderFetch = providerFetch(
|
|
5770
6272
|
route.provider,
|
|
@@ -5830,9 +6332,8 @@ async function handleResponsesInner(
|
|
|
5830
6332
|
if (!nextAccountId) return false;
|
|
5831
6333
|
try {
|
|
5832
6334
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
5833
|
-
genericFailoverAccountId = nextAccountId;
|
|
5834
6335
|
genericFailovers += 1;
|
|
5835
|
-
if (!applyFailoverSnapshot(snapshot)) return false;
|
|
6336
|
+
if (!await applyFailoverSnapshot(snapshot)) return false;
|
|
5836
6337
|
// A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
|
|
5837
6338
|
// client-visible bytes, so replay is safe, but carrying its account identity into the next
|
|
5838
6339
|
// account would not be. Let the rotated adapter derive a fresh identity and conversation.
|
|
@@ -5848,7 +6349,7 @@ async function handleResponsesInner(
|
|
|
5848
6349
|
route.provider,
|
|
5849
6350
|
inboundWire,
|
|
5850
6351
|
);
|
|
5851
|
-
const rotatedAdapter =
|
|
6352
|
+
const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
|
|
5852
6353
|
if (!rotatedAdapter.runTurn) return false;
|
|
5853
6354
|
runTurnAdapter = rotatedAdapter;
|
|
5854
6355
|
bindRouteReasoningReplayScope({
|
|
@@ -5861,6 +6362,7 @@ async function handleResponsesInner(
|
|
|
5861
6362
|
forwardHeaders: selectedForwardHeaders,
|
|
5862
6363
|
});
|
|
5863
6364
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
6365
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
|
|
5864
6366
|
return true;
|
|
5865
6367
|
} catch {
|
|
5866
6368
|
return false;
|
|
@@ -5897,7 +6399,7 @@ async function handleResponsesInner(
|
|
|
5897
6399
|
if (parsed.stream) {
|
|
5898
6400
|
void runTurn();
|
|
5899
6401
|
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
5900
|
-
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
6402
|
+
if (route.provider.authMode === "oauth" || (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
|
|
5901
6403
|
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
5902
6404
|
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
5903
6405
|
eventSource = await preflightRunTurnFailover(eventSource);
|
|
@@ -5980,7 +6482,7 @@ async function handleResponsesInner(
|
|
|
5980
6482
|
await runTurn();
|
|
5981
6483
|
const firstAttemptEvents = await queue.collect();
|
|
5982
6484
|
let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
|
|
5983
|
-
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
6485
|
+
if (route.provider.authMode === "oauth" || (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
|
|
5984
6486
|
runTurnEvents = [];
|
|
5985
6487
|
for await (const event of await preflightRunTurnFailover(
|
|
5986
6488
|
(async function* () { yield* firstAttemptEvents; })(),
|
|
@@ -6052,7 +6554,7 @@ async function handleResponsesInner(
|
|
|
6052
6554
|
const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
|
|
6053
6555
|
? Math.floor(config.stallTimeoutSec * 1000)
|
|
6054
6556
|
: 300_000;
|
|
6055
|
-
|
|
6557
|
+
activeAdapter = adapter;
|
|
6056
6558
|
|
|
6057
6559
|
// One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
|
|
6058
6560
|
// auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
|
|
@@ -6151,16 +6653,15 @@ async function handleResponsesInner(
|
|
|
6151
6653
|
// Capture it in a const so the fetch callbacks read a narrowed, immutable value
|
|
6152
6654
|
// (TypeScript drops narrowing for a `let` captured by a nested function).
|
|
6153
6655
|
const builtInitialRequest = initialRequest;
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
let transportToken = 0;
|
|
6656
|
+
sameTargetRequest = builtInitialRequest;
|
|
6657
|
+
sameTargetParsed = parsed;
|
|
6658
|
+
sameTargetToken = transportToken;
|
|
6158
6659
|
/**
|
|
6159
6660
|
* Invalidate the same-target request cache. Every credential/adapter/parsed mutation MUST
|
|
6160
6661
|
* go through here: the cache keys on `parsed` REFERENCE identity, so an in-place mutation
|
|
6161
6662
|
* is invisible to it and a missed bump would replay a request built with a stale key.
|
|
6162
6663
|
*/
|
|
6163
|
-
|
|
6664
|
+
|
|
6164
6665
|
let upstreamResponse: Response;
|
|
6165
6666
|
try {
|
|
6166
6667
|
if (activeAdapter.fetchResponse) {
|
|
@@ -6171,6 +6672,7 @@ async function handleResponsesInner(
|
|
|
6171
6672
|
timeoutMs: connectMs,
|
|
6172
6673
|
stream: parsed.stream,
|
|
6173
6674
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6675
|
+
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
6174
6676
|
providerName: route.providerName,
|
|
6175
6677
|
modelId: route.modelId,
|
|
6176
6678
|
}),
|
|
@@ -6196,6 +6698,7 @@ async function handleResponsesInner(
|
|
|
6196
6698
|
body: builtInitialRequest.body,
|
|
6197
6699
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
6198
6700
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6701
|
+
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
6199
6702
|
providerName: route.providerName,
|
|
6200
6703
|
modelId: route.modelId,
|
|
6201
6704
|
}));
|
|
@@ -6227,7 +6730,6 @@ async function handleResponsesInner(
|
|
|
6227
6730
|
let rateLimitRetries = 0;
|
|
6228
6731
|
// Shared with the terminal-guard continuation below: an image-tier reduction that let the
|
|
6229
6732
|
// main request clear a 413 must not be forgotten on the very next continuation build.
|
|
6230
|
-
let imageTierBias = 0;
|
|
6231
6733
|
if (!upstreamResponse.ok) {
|
|
6232
6734
|
// Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
|
|
6233
6735
|
// anthropic 413 tightened retry
|
|
@@ -6280,6 +6782,7 @@ async function handleResponsesInner(
|
|
|
6280
6782
|
if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
|
|
6281
6783
|
logCtx.providerAdapter = activeAdapter.name;
|
|
6282
6784
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
6785
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
|
|
6283
6786
|
noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
|
|
6284
6787
|
try {
|
|
6285
6788
|
try {
|
|
@@ -6290,6 +6793,7 @@ async function handleResponsesInner(
|
|
|
6290
6793
|
timeoutMs: connectMs,
|
|
6291
6794
|
stream: parsed.stream,
|
|
6292
6795
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6796
|
+
dispatchOverride: oauthDispatch(retryRequest),
|
|
6293
6797
|
providerName: route.providerName,
|
|
6294
6798
|
modelId: route.modelId,
|
|
6295
6799
|
}),
|
|
@@ -6311,6 +6815,7 @@ async function handleResponsesInner(
|
|
|
6311
6815
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
6312
6816
|
}, recoveryKind), upstream.signal, connectMs, parsed.stream,
|
|
6313
6817
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
6818
|
+
dispatchOverride: oauthDispatch(retryRequest),
|
|
6314
6819
|
providerName: route.providerName,
|
|
6315
6820
|
modelId: route.modelId,
|
|
6316
6821
|
})),
|
|
@@ -6350,11 +6855,15 @@ async function handleResponsesInner(
|
|
|
6350
6855
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
6351
6856
|
let refreshed: OAuthAccessSnapshot;
|
|
6352
6857
|
try {
|
|
6353
|
-
refreshed = await
|
|
6858
|
+
refreshed = await refreshResolvedOAuthSelection(sentOAuthSnapshot);
|
|
6354
6859
|
} catch (err) {
|
|
6355
6860
|
cleanupUpstreamAbort();
|
|
6356
6861
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
6357
6862
|
}
|
|
6863
|
+
if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
|
|
6864
|
+
cleanupUpstreamAbort();
|
|
6865
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
|
|
6866
|
+
}
|
|
6358
6867
|
sentOAuthSnapshot = refreshed;
|
|
6359
6868
|
replayOAuthCredentialSnapshot = {
|
|
6360
6869
|
accountId: refreshed.accountId,
|
|
@@ -6365,7 +6874,11 @@ async function handleResponsesInner(
|
|
|
6365
6874
|
}
|
|
6366
6875
|
const refreshedProvider = resolveProviderTransport(
|
|
6367
6876
|
route.providerName,
|
|
6368
|
-
{
|
|
6877
|
+
{
|
|
6878
|
+
...route.provider,
|
|
6879
|
+
apiKey: refreshed.accessToken,
|
|
6880
|
+
...(refreshed.projectId ? { project: refreshed.projectId } : {}),
|
|
6881
|
+
},
|
|
6369
6882
|
parsed.options.promptCacheKey,
|
|
6370
6883
|
route.providerName === "github-copilot"
|
|
6371
6884
|
? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
|
|
@@ -6373,7 +6886,7 @@ async function handleResponsesInner(
|
|
|
6373
6886
|
);
|
|
6374
6887
|
route.provider = refreshedProvider;
|
|
6375
6888
|
invalidateSameTargetRequest();
|
|
6376
|
-
activeAdapter =
|
|
6889
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6377
6890
|
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
6378
6891
|
config.cacheRetention,
|
|
6379
6892
|
);
|
|
@@ -6406,7 +6919,7 @@ async function handleResponsesInner(
|
|
|
6406
6919
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
6407
6920
|
route.provider = rotated;
|
|
6408
6921
|
invalidateSameTargetRequest();
|
|
6409
|
-
activeAdapter =
|
|
6922
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6410
6923
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6411
6924
|
config.cacheRetention,
|
|
6412
6925
|
);
|
|
@@ -6476,7 +6989,7 @@ async function handleResponsesInner(
|
|
|
6476
6989
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
6477
6990
|
route.provider = rotated;
|
|
6478
6991
|
invalidateSameTargetRequest();
|
|
6479
|
-
activeAdapter =
|
|
6992
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6480
6993
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6481
6994
|
config.cacheRetention,
|
|
6482
6995
|
);
|
|
@@ -6507,18 +7020,19 @@ async function handleResponsesInner(
|
|
|
6507
7020
|
if (!nextAccountId) break;
|
|
6508
7021
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
6509
7022
|
try {
|
|
6510
|
-
const
|
|
6511
|
-
|
|
7023
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
7024
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
7025
|
+
anthropicPoolAccountId = admitted.accountId;
|
|
6512
7026
|
anthropicPoolFailovers += 1;
|
|
6513
|
-
route.provider = { ...route.provider, apiKey: accessToken };
|
|
7027
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
6514
7028
|
invalidateSameTargetRequest();
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
activeAdapter = resolveAdapter(
|
|
7029
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
7030
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6518
7031
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6519
7032
|
config.cacheRetention,
|
|
6520
7033
|
);
|
|
6521
7034
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
7035
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
|
|
6522
7036
|
const result = await rebuildAndRefetch("anthropic-oauth-429");
|
|
6523
7037
|
if ("failed" in result) return result.failed;
|
|
6524
7038
|
upstreamResponse = result;
|
|
@@ -6553,15 +7067,15 @@ async function handleResponsesInner(
|
|
|
6553
7067
|
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
6554
7068
|
// would mix one account's credential with another's routing data.
|
|
6555
7069
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
if (!applyFailoverSnapshot(snapshot)) break;
|
|
7070
|
+
genericFailovers += 1;
|
|
7071
|
+
if (!await applyFailoverSnapshot(snapshot)) break;
|
|
6559
7072
|
invalidateSameTargetRequest();
|
|
6560
|
-
activeAdapter =
|
|
7073
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6561
7074
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6562
7075
|
config.cacheRetention,
|
|
6563
7076
|
);
|
|
6564
7077
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
7078
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
|
|
6565
7079
|
const result = await rebuildAndRefetch("oauth-account-429");
|
|
6566
7080
|
if ("failed" in result) return result.failed;
|
|
6567
7081
|
upstreamResponse = result;
|
|
@@ -6761,6 +7275,7 @@ async function handleResponsesInner(
|
|
|
6761
7275
|
timeoutMs: connectMs,
|
|
6762
7276
|
stream: nextParsed.stream,
|
|
6763
7277
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
7278
|
+
dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
|
|
6764
7279
|
providerName: route.providerName,
|
|
6765
7280
|
modelId: nextParsed.modelId,
|
|
6766
7281
|
}),
|
|
@@ -6786,6 +7301,7 @@ async function handleResponsesInner(
|
|
|
6786
7301
|
connectMs,
|
|
6787
7302
|
nextParsed.stream,
|
|
6788
7303
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
7304
|
+
dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
|
|
6789
7305
|
providerName: route.providerName,
|
|
6790
7306
|
modelId: nextParsed.modelId,
|
|
6791
7307
|
}),
|
|
@@ -6881,7 +7397,7 @@ async function handleResponsesInner(
|
|
|
6881
7397
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
6882
7398
|
route.provider = rotated;
|
|
6883
7399
|
invalidateSameTargetRequest();
|
|
6884
|
-
activeAdapter =
|
|
7400
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6885
7401
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6886
7402
|
config.cacheRetention,
|
|
6887
7403
|
);
|
|
@@ -6917,18 +7433,19 @@ async function handleResponsesInner(
|
|
|
6917
7433
|
if (nextAccountId) {
|
|
6918
7434
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
6919
7435
|
try {
|
|
6920
|
-
const
|
|
6921
|
-
|
|
7436
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
7437
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
7438
|
+
anthropicPoolAccountId = admitted.accountId;
|
|
6922
7439
|
anthropicPoolFailovers += 1;
|
|
6923
|
-
route.provider = { ...route.provider, apiKey: accessToken };
|
|
7440
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
6924
7441
|
invalidateSameTargetRequest();
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
activeAdapter = resolveAdapter(
|
|
7442
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
7443
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6928
7444
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6929
7445
|
config.cacheRetention,
|
|
6930
7446
|
);
|
|
6931
7447
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
7448
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
|
|
6932
7449
|
nextContinuationRecoveryKind = "anthropic-oauth-429";
|
|
6933
7450
|
continue;
|
|
6934
7451
|
} catch {
|
|
@@ -6961,15 +7478,15 @@ async function handleResponsesInner(
|
|
|
6961
7478
|
// metadata, so a token-only swap would mix one account's credential with another's
|
|
6962
7479
|
// routing data.
|
|
6963
7480
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
if (applyFailoverSnapshot(snapshot, nextParsed)) {
|
|
7481
|
+
genericFailovers += 1;
|
|
7482
|
+
if (await applyFailoverSnapshot(snapshot, nextParsed)) {
|
|
6967
7483
|
invalidateSameTargetRequest();
|
|
6968
|
-
activeAdapter =
|
|
7484
|
+
activeAdapter = resolveSelectionAdapter(
|
|
6969
7485
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
6970
7486
|
config.cacheRetention,
|
|
6971
7487
|
);
|
|
6972
7488
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
7489
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
|
|
6973
7490
|
nextContinuationRecoveryKind = "oauth-account-429";
|
|
6974
7491
|
continue;
|
|
6975
7492
|
}
|