@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
4
|
+
import {
|
|
5
|
+
captureOAuthAccountSelection,
|
|
6
|
+
commitOAuthAccountSelection,
|
|
7
|
+
getAccountCredentialWithStatus,
|
|
8
|
+
credentialGeneration,
|
|
9
|
+
} from "../../oauth/store";
|
|
10
|
+
import type { ProviderAdapter, AdapterRequest } from "../../adapters/base";
|
|
11
|
+
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
|
|
12
|
+
import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
|
|
13
|
+
import {
|
|
14
|
+
isAnthropicAccountPoolEnabled,
|
|
15
|
+
getAnthropicPoolAccessSnapshot,
|
|
16
|
+
commitAnthropicSelectionRouting,
|
|
17
|
+
formatAnthropicProviderForLog,
|
|
18
|
+
anthropicSessionKeyFromParts,
|
|
19
|
+
resolveAnthropicAccountForSession,
|
|
20
|
+
getAnthropicPoolRetryAfterSeconds,
|
|
21
|
+
hasAnthropicFailoverQuorum,
|
|
22
|
+
} from "../../oauth/anthropic-routing";
|
|
23
|
+
import {
|
|
24
|
+
getValidAccessSnapshotForAccount,
|
|
25
|
+
forceRefreshOAuthAccessSnapshot,
|
|
26
|
+
getValidAccessTokenSnapshot,
|
|
27
|
+
publicOAuthAuthenticationErrorMessage,
|
|
28
|
+
UnsupportedOAuthProviderError,
|
|
29
|
+
} from "../../oauth";
|
|
30
|
+
import {
|
|
31
|
+
forgetGenericFailoverRoster,
|
|
32
|
+
isGenericFailoverProvider,
|
|
33
|
+
preferredInitialAccount,
|
|
34
|
+
noteGenericPoolSelection,
|
|
35
|
+
} from "../../oauth/generic-account-failover";
|
|
36
|
+
import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
|
|
37
|
+
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
38
|
+
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
39
|
+
import {
|
|
40
|
+
providerApiKeySelectionIsCurrent,
|
|
41
|
+
resolveCurrentProviderApiKeyTransport,
|
|
42
|
+
} from "../../providers/api-key-selection";
|
|
43
|
+
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
44
|
+
import { providerFetch } from "./fetch-helpers";
|
|
45
|
+
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
46
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
47
|
+
import { recordAnthropicAccountQuotaFromHeaders, hasPassiveAccountQuota } from "../../providers/quota";
|
|
48
|
+
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
49
|
+
import { formatErrorResponse } from "../../bridge";
|
|
50
|
+
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
51
|
+
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
52
|
+
import { redactSecretString } from "../../lib/redact";
|
|
53
|
+
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
54
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
55
|
+
import { providerConsumesCallerAuthorization } from "../../providers/caller-authorization";
|
|
56
|
+
import { releaseCodexAuthContextProbeLease, stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
57
|
+
import {
|
|
58
|
+
beginRequestAttempt,
|
|
59
|
+
sealRequestAttemptIdentity,
|
|
60
|
+
recordAttemptCredentialSource,
|
|
61
|
+
recordAdapterTierMetadata,
|
|
62
|
+
noteProviderAttemptSend,
|
|
63
|
+
recordKeyAttemptFailure,
|
|
64
|
+
recordKeyAttemptUsage,
|
|
65
|
+
} from "../request-log";
|
|
66
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
67
|
+
import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
|
|
68
|
+
|
|
69
|
+
/** Owns live credential selection and adapter bindings for one request. */
|
|
70
|
+
export async function prepareResponsesTransport(
|
|
71
|
+
requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
|
|
72
|
+
admissionState: ResponsesAdmissionState,
|
|
73
|
+
requestState: Pick<
|
|
74
|
+
PreparedResponsesRequest,
|
|
75
|
+
| "route"
|
|
76
|
+
| "parsed"
|
|
77
|
+
| "inboundWire"
|
|
78
|
+
| "selectedForwardHeaders"
|
|
79
|
+
| "translatorBudget"
|
|
80
|
+
>,
|
|
81
|
+
) {
|
|
82
|
+
const { config, logCtx, options, req } = requestContext;
|
|
83
|
+
const { route, parsed, inboundWire, translatorBudget } = requestState;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
87
|
+
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
88
|
+
const isOAuth401ReplayProvider = (
|
|
89
|
+
route.providerName === "xai"
|
|
90
|
+
|| route.providerName === "github-copilot"
|
|
91
|
+
|| route.providerName === "kiro"
|
|
92
|
+
|| route.providerName === "google-antigravity"
|
|
93
|
+
|| route.providerName === "orcarouter-oauth"
|
|
94
|
+
) && route.provider.authMode === "oauth";
|
|
95
|
+
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
96
|
+
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
97
|
+
let anthropicPoolAccountId: string | null = null;
|
|
98
|
+
let anthropicPoolFailovers = 0;
|
|
99
|
+
// Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
|
|
100
|
+
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
101
|
+
let genericFailoverAccountId: string | null = null;
|
|
102
|
+
let genericFailovers = 0;
|
|
103
|
+
let oauthSelection = route.provider.authMode === "oauth"
|
|
104
|
+
? captureOAuthAccountSelection(route.providerName) : null;
|
|
105
|
+
let servingOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
106
|
+
// These owners also serve early passthrough and sidecar sends. A dispatch-time
|
|
107
|
+
// rebuild must update every later builder, without entering a later block's TDZ.
|
|
108
|
+
let adapter: ProviderAdapter;
|
|
109
|
+
let activeAdapter: ProviderAdapter;
|
|
110
|
+
let runTurnAdapter: ProviderAdapter;
|
|
111
|
+
let sameTargetRequest: AdapterRequest | undefined;
|
|
112
|
+
let sameTargetParsed: OcxParsedRequest | undefined;
|
|
113
|
+
let sameTargetToken = 0;
|
|
114
|
+
let transportToken = 0;
|
|
115
|
+
let imageTierBias = 0;
|
|
116
|
+
const invalidateSameTargetRequest = (): void => { transportToken += 1; };
|
|
117
|
+
type DispatchBinding =
|
|
118
|
+
| { kind: "oauth"; selection: NonNullable<typeof oauthSelection>; snapshot: OAuthAccessSnapshot }
|
|
119
|
+
| { kind: "api-key"; provider: OcxProviderConfig };
|
|
120
|
+
const requestBindings = new WeakMap<AdapterRequest, DispatchBinding>();
|
|
121
|
+
const adapterBindings = new WeakMap<ProviderAdapter, DispatchBinding>();
|
|
122
|
+
const rawRunTurns = new WeakMap<ProviderAdapter, NonNullable<ProviderAdapter["runTurn"]>>();
|
|
123
|
+
const commitResolvedOAuthSelection = async (
|
|
124
|
+
candidate: OAuthAccessSnapshot,
|
|
125
|
+
proactive = false,
|
|
126
|
+
anthropicReason?: AnthropicAccountSelectionReason,
|
|
127
|
+
): Promise<OAuthAccessSnapshot | null> => {
|
|
128
|
+
const maxSelectionAttempts = 3;
|
|
129
|
+
for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
|
|
130
|
+
if (!oauthSelection) return null;
|
|
131
|
+
const proactiveEnabled = route.providerName === "anthropic"
|
|
132
|
+
? isAnthropicAccountPoolEnabled(config)
|
|
133
|
+
: (config.providers[route.providerName]?.oauthAccountFailover?.enabled
|
|
134
|
+
?? config.oauthAccountFailover?.enabled) === true;
|
|
135
|
+
if (proactive && candidate.accountId !== oauthSelection.accountId && !proactiveEnabled) {
|
|
136
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
137
|
+
if (!oauthSelection) return null;
|
|
138
|
+
candidate = route.providerName === "anthropic"
|
|
139
|
+
? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
|
|
140
|
+
: await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
|
|
141
|
+
}
|
|
142
|
+
const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
|
|
143
|
+
expectedSelection: oauthSelection,
|
|
144
|
+
expectedCredentialGeneration: candidate.generation,
|
|
145
|
+
requireUsableAccount: true,
|
|
146
|
+
});
|
|
147
|
+
if (committed) {
|
|
148
|
+
if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
|
|
149
|
+
candidate.accountId, oauthSelection, committed,
|
|
150
|
+
{ config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation },
|
|
151
|
+
)) return null;
|
|
152
|
+
oauthSelection = committed;
|
|
153
|
+
servingOAuthSnapshot = candidate;
|
|
154
|
+
forgetGenericFailoverRoster(route.providerName);
|
|
155
|
+
return candidate;
|
|
156
|
+
}
|
|
157
|
+
// A newer manual choice wins over this request's old proposal, including A→B→A.
|
|
158
|
+
// Resolve that choice, not the rejected candidate, before trying admission again.
|
|
159
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
160
|
+
if (!oauthSelection) return null;
|
|
161
|
+
candidate = route.providerName === "anthropic"
|
|
162
|
+
? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
|
|
163
|
+
: await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
|
|
164
|
+
if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
const refreshResolvedOAuthSelection = async (sent: OAuthAccessSnapshot): Promise<OAuthAccessSnapshot> => {
|
|
169
|
+
const current = captureOAuthAccountSelection(route.providerName);
|
|
170
|
+
const unchanged = current?.accountId === oauthSelection?.accountId
|
|
171
|
+
&& current?.revision === oauthSelection?.revision;
|
|
172
|
+
const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
|
|
173
|
+
const admitted = await commitResolvedOAuthSelection(candidate);
|
|
174
|
+
if (!admitted) throw new Error("OAuth selection changed during credential recovery");
|
|
175
|
+
genericFailoverAccountId = admitted.accountId;
|
|
176
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, admitted.accountId);
|
|
177
|
+
return admitted;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Config generation captured where the serving credential is RESOLVED, not where the
|
|
181
|
+
* quota is written. A streaming turn is a long await, so a generation captured at write
|
|
182
|
+
* time cannot see a config or account change that happened earlier in the same turn —
|
|
183
|
+
* the case the fence exists for. Stays 0 for every provider without a passive quota.
|
|
184
|
+
*/
|
|
185
|
+
let passiveQuotaWriterGeneration = 0;
|
|
186
|
+
/**
|
|
187
|
+
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
188
|
+
*
|
|
189
|
+
* One helper for all three rotation sites on purpose. Each site used to inline the same four
|
|
190
|
+
* lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
|
|
191
|
+
* metadata paired with it stayed behind.
|
|
192
|
+
*
|
|
193
|
+
* Returns false when the snapshot cannot be used safely, and the caller must then abandon the
|
|
194
|
+
* rotation rather than send a half-applied identity:
|
|
195
|
+
*
|
|
196
|
+
* - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
|
|
197
|
+
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host. The
|
|
198
|
+
* snapshot value is RESOLVED first: `rotatedProvider` is a clone of the FAILED account's
|
|
199
|
+
* provider, so passing a bare `undefined` origin let the transport resolver fall through its
|
|
200
|
+
* own `?? validateCopilotApiBaseUrl(provider.baseUrl)` step to the previous account's host —
|
|
201
|
+
* pairing B's bearer with A's accepted origin. Login and refresh always persist a resolved
|
|
202
|
+
* origin, so this fallback protects malformed or manually seeded credentials.
|
|
203
|
+
* - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
|
|
204
|
+
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
205
|
+
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
206
|
+
*/
|
|
207
|
+
const applyFailoverSnapshot = async (
|
|
208
|
+
snapshot: OAuthAccessSnapshot,
|
|
209
|
+
retryParsed: OcxParsedRequest = parsed,
|
|
210
|
+
): Promise<boolean> => {
|
|
211
|
+
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
212
|
+
const committed = await commitResolvedOAuthSelection(snapshot);
|
|
213
|
+
if (!committed) return false;
|
|
214
|
+
snapshot = committed;
|
|
215
|
+
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
216
|
+
if (route.providerName === "github-copilot") {
|
|
217
|
+
rotatedProvider = resolveProviderTransport(
|
|
218
|
+
route.providerName,
|
|
219
|
+
rotatedProvider,
|
|
220
|
+
parsed.options.promptCacheKey,
|
|
221
|
+
resolveCopilotApiBaseUrl(snapshot.apiBaseUrl),
|
|
222
|
+
) as OcxProviderConfig;
|
|
223
|
+
}
|
|
224
|
+
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
225
|
+
route.provider = rotatedProvider;
|
|
226
|
+
if (route.providerName === "kiro") {
|
|
227
|
+
const kiroContext = { ...(snapshot.kiro ?? {}) };
|
|
228
|
+
// Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
|
|
229
|
+
// outer request pairs the new bearer with the failed account's region/profile on
|
|
230
|
+
// the retry. Keep both owners synchronized; for ordinary paths they are identical.
|
|
231
|
+
parsed._kiroAuthContext = kiroContext;
|
|
232
|
+
if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
|
|
233
|
+
}
|
|
234
|
+
// Re-stamp: a request that rotated accounts must be attributed to the account that actually
|
|
235
|
+
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
236
|
+
// needed -- and putting it anywhere else would let one of the three drift.
|
|
237
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
|
|
238
|
+
if (route.providerName === "anthropic") {
|
|
239
|
+
anthropicPoolAccountId = snapshot.accountId;
|
|
240
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", snapshot.accountId, config);
|
|
241
|
+
} else {
|
|
242
|
+
genericFailoverAccountId = snapshot.accountId;
|
|
243
|
+
}
|
|
244
|
+
sentOAuthSnapshot = snapshot;
|
|
245
|
+
replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
|
|
246
|
+
return true;
|
|
247
|
+
};
|
|
248
|
+
// Key sends may be rebuilt while queued. Keep metadata pending until the guarded
|
|
249
|
+
// physical dispatch binds it to the selection that actually reaches the upstream.
|
|
250
|
+
let pendingKeySend: { estimate: number | undefined; recovery?: AttemptRecoveryKind } | undefined;
|
|
251
|
+
const noteRoutedAttemptSend = (estimate: number | undefined, recovery?: AttemptRecoveryKind): void => {
|
|
252
|
+
if (usesApiKeyAccount(route.provider)) pendingKeySend = { estimate, recovery };
|
|
253
|
+
else noteProviderAttemptSend(logCtx, route.providerName, route.provider, estimate, recovery);
|
|
254
|
+
};
|
|
255
|
+
const commitKeyAttemptSend = (): void => {
|
|
256
|
+
if (!usesApiKeyAccount(route.provider)) return;
|
|
257
|
+
noteProviderAttemptSend(logCtx, route.providerName, route.provider,
|
|
258
|
+
pendingKeySend?.estimate ?? logCtx.usageLogInputTokens, pendingKeySend?.recovery);
|
|
259
|
+
pendingKeySend = undefined;
|
|
260
|
+
};
|
|
261
|
+
const bindKeyUsageFromBridge = (usage: OcxUsage | undefined): void => {
|
|
262
|
+
logCtx.usageFromBridge = true;
|
|
263
|
+
if (usesApiKeyAccount(route.provider)) {
|
|
264
|
+
logCtx.usage = logCtx.activeAttempt?.usage;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (usage) {
|
|
268
|
+
logCtx.usage = usage;
|
|
269
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
|
|
273
|
+
if (route.provider.authMode === "forward") return true;
|
|
274
|
+
if (!binding) return false;
|
|
275
|
+
if (binding.kind === "api-key") return providerApiKeySelectionIsCurrent(config, route.providerName, binding.provider);
|
|
276
|
+
const selected = captureOAuthAccountSelection(route.providerName);
|
|
277
|
+
const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
|
|
278
|
+
return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
|
|
279
|
+
&& !!row && !row.needsReauth && row.credential.expires > Date.now()
|
|
280
|
+
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
281
|
+
};
|
|
282
|
+
const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
|
|
283
|
+
const resolved = resolveAdapter(provider, retention, route.providerName);
|
|
284
|
+
if (route.provider.authMode === "forward") return resolved;
|
|
285
|
+
const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
|
|
286
|
+
? oauthSelection && servingOAuthSnapshot
|
|
287
|
+
? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
|
|
288
|
+
: undefined
|
|
289
|
+
: { kind: "api-key", provider: { ...route.provider } };
|
|
290
|
+
if (binding) adapterBindings.set(resolved, binding);
|
|
291
|
+
// Observe terminals before search/image loops or continuation guards hide earlier rounds.
|
|
292
|
+
// Each adapter parser is called once per physical response; bridge totals are client-only.
|
|
293
|
+
const observedResponses = new WeakSet<object>();
|
|
294
|
+
const observeUsage = (event: AdapterEvent, response: object): void => {
|
|
295
|
+
if (usesApiKeyAccount(provider) && "usage" in event && event.usage && !observedResponses.has(response)) {
|
|
296
|
+
observedResponses.add(response);
|
|
297
|
+
recordKeyAttemptUsage(logCtx, event.usage);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const parseStream = resolved.parseStream.bind(resolved);
|
|
301
|
+
resolved.parseStream = async function* (...args) {
|
|
302
|
+
for await (const event of parseStream(...args)) { observeUsage(event, args[0]); yield event; }
|
|
303
|
+
};
|
|
304
|
+
if (resolved.parseResponse) {
|
|
305
|
+
const parseResponse = resolved.parseResponse.bind(resolved);
|
|
306
|
+
resolved.parseResponse = async (...args) => {
|
|
307
|
+
const events = await parseResponse(...args);
|
|
308
|
+
events.forEach(event => observeUsage(event, args[0]));
|
|
309
|
+
return events;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const build = resolved.buildRequest.bind(resolved);
|
|
313
|
+
resolved.buildRequest = async (requestParsed, incoming) => {
|
|
314
|
+
const request = await build(requestParsed, incoming);
|
|
315
|
+
// Capture at adapter creation, never from mutable serving state after an await.
|
|
316
|
+
if (binding) requestBindings.set(request, binding);
|
|
317
|
+
return request;
|
|
318
|
+
};
|
|
319
|
+
if (resolved.runTurn) {
|
|
320
|
+
const runTurn = resolved.runTurn.bind(resolved);
|
|
321
|
+
rawRunTurns.set(resolved, (requestParsed, incoming, emit) => {
|
|
322
|
+
const response = {};
|
|
323
|
+
return runTurn(requestParsed, incoming, event => { observeUsage(event, response); emit(event); });
|
|
324
|
+
});
|
|
325
|
+
resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
|
|
326
|
+
}
|
|
327
|
+
return resolved;
|
|
328
|
+
};
|
|
329
|
+
const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
330
|
+
if (route.provider.authMode === "oauth") {
|
|
331
|
+
if (!servingOAuthSnapshot || !await applyFailoverSnapshot(servingOAuthSnapshot, requestParsed)) {
|
|
332
|
+
throw new Error("OAuth account selection changed before dispatch");
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
|
|
336
|
+
if (!current) throw new Error("API key selection is unavailable before dispatch");
|
|
337
|
+
route.provider = current;
|
|
338
|
+
}
|
|
339
|
+
adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
|
|
340
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
341
|
+
);
|
|
342
|
+
invalidateSameTargetRequest();
|
|
343
|
+
return adapter;
|
|
344
|
+
};
|
|
345
|
+
const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
346
|
+
requestParsed._cursorIdentityScope = undefined;
|
|
347
|
+
requestParsed._cursorConversationId = undefined;
|
|
348
|
+
if (requestParsed._providerContinuation?.cursor) {
|
|
349
|
+
const { cursor: _oldCursor, ...rest } = requestParsed._providerContinuation;
|
|
350
|
+
requestParsed._providerContinuation = rest;
|
|
351
|
+
}
|
|
352
|
+
return refreshDispatchAdapter(requestParsed);
|
|
353
|
+
};
|
|
354
|
+
const runSelectedTurn = async (
|
|
355
|
+
selectedAdapter: ProviderAdapter,
|
|
356
|
+
...[requestParsed, incoming, emit]: Parameters<NonNullable<ProviderAdapter["runTurn"]>>
|
|
357
|
+
): Promise<void> => {
|
|
358
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
359
|
+
if (!selectionIsCurrent(adapterBindings.get(selectedAdapter))) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
360
|
+
const binding = adapterBindings.get(selectedAdapter);
|
|
361
|
+
const run = rawRunTurns.get(selectedAdapter);
|
|
362
|
+
if (!run) throw new Error("Selected provider no longer supports this turn transport");
|
|
363
|
+
let sent = false;
|
|
364
|
+
let refused = false;
|
|
365
|
+
// Both main and image-loop callers already acquired the initial pacing slot.
|
|
366
|
+
// Subsequent physical messages retain this adapter/credential and are paced normally.
|
|
367
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
368
|
+
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
369
|
+
beforeDispatch: () => {
|
|
370
|
+
if (sent) return;
|
|
371
|
+
if (!selectionIsCurrent(binding)) {
|
|
372
|
+
refused = true;
|
|
373
|
+
throw new Error("Account selection changed before the first turn dispatch");
|
|
374
|
+
}
|
|
375
|
+
commitKeyAttemptSend();
|
|
376
|
+
sent = true;
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
try {
|
|
380
|
+
await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!refused) throw error;
|
|
383
|
+
}
|
|
384
|
+
if (!refused) return;
|
|
385
|
+
// The adapter may map the guard's exception to an error event. Neither that
|
|
386
|
+
// event nor a refused send may escape before retrying the newly selected account.
|
|
387
|
+
selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
388
|
+
}
|
|
389
|
+
throw new Error("Account selection changed repeatedly before turn dispatch");
|
|
390
|
+
};
|
|
391
|
+
const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
|
|
392
|
+
if (route.provider.authMode === "forward") return undefined;
|
|
393
|
+
return async (input, init, execute) => {
|
|
394
|
+
let destination = input;
|
|
395
|
+
let dispatchInit = init;
|
|
396
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
397
|
+
if (selectionIsCurrent(requestBindings.get(wireRequest))) {
|
|
398
|
+
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
|
|
399
|
+
const binding = requestBindings.get(wireRequest);
|
|
400
|
+
const snapshot = route.providerName === "anthropic" && anthropicPoolAccountId && binding?.kind === "oauth"
|
|
401
|
+
? binding.snapshot : undefined;
|
|
402
|
+
const writerGeneration = snapshot ? captureConfigGeneration() : 0;
|
|
403
|
+
const sentHeaders = snapshot ? new Headers(dispatchInit.headers) : undefined;
|
|
404
|
+
const ownsBearer = snapshot !== undefined
|
|
405
|
+
&& sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
|
|
406
|
+
&& !sentHeaders?.has("x-api-key");
|
|
407
|
+
// Reselection can choose a provider override instead of the supplied executor.
|
|
408
|
+
commitKeyAttemptSend();
|
|
409
|
+
const response = await fetchImpl(destination, { ...dispatchInit, redirect: "manual" });
|
|
410
|
+
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
411
|
+
// Observe each physical response before retries replace it. The binding belongs to
|
|
412
|
+
// this dispatch, so a manual switch cannot file A's headers against B. Header
|
|
413
|
+
// overrides and credential replacement make ownership unprovable: skip those writes.
|
|
414
|
+
if (ownsBearer && snapshot) {
|
|
415
|
+
try {
|
|
416
|
+
const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
|
|
417
|
+
if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
|
|
418
|
+
recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
|
|
419
|
+
}
|
|
420
|
+
} catch { /* best-effort observation cannot fail the response */ }
|
|
421
|
+
}
|
|
422
|
+
return response;
|
|
423
|
+
}
|
|
424
|
+
const nextAdapter = await refreshDispatchAdapter(requestParsed);
|
|
425
|
+
const rebuilt = await nextAdapter.buildRequest(requestParsed, {
|
|
426
|
+
headers: requestState.selectedForwardHeaders, translatorBudget,
|
|
427
|
+
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
428
|
+
});
|
|
429
|
+
const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
|
|
430
|
+
if (!bodySize.admitted) {
|
|
431
|
+
rebuilt.releaseBodyObservation?.();
|
|
432
|
+
return formatErrorResponse(413, "outbound_body_too_large", describeOutboundBodyRefusal(bodySize));
|
|
433
|
+
}
|
|
434
|
+
const headers = new Headers(dispatchInit.headers);
|
|
435
|
+
for (const name of Object.keys(wireRequest.headers)) headers.delete(name);
|
|
436
|
+
for (const [name, value] of Object.entries(rebuilt.headers)) headers.set(name, value);
|
|
437
|
+
wireRequest.releaseBodyObservation?.();
|
|
438
|
+
Object.assign(wireRequest, rebuilt);
|
|
439
|
+
const binding = requestBindings.get(rebuilt);
|
|
440
|
+
if (binding) requestBindings.set(wireRequest, binding);
|
|
441
|
+
else requestBindings.delete(wireRequest);
|
|
442
|
+
sameTargetRequest = wireRequest;
|
|
443
|
+
sameTargetParsed = requestParsed;
|
|
444
|
+
sameTargetToken = transportToken;
|
|
445
|
+
destination = rebuilt.url;
|
|
446
|
+
dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
|
|
447
|
+
bindRouteReasoningReplayScope({ parsed: requestParsed, providerName: route.providerName, provider: route.provider,
|
|
448
|
+
adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
|
|
449
|
+
// The next iteration validates synchronously and calls fetch in that same turn.
|
|
450
|
+
}
|
|
451
|
+
throw new Error("OAuth account selection changed repeatedly before dispatch");
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
|
|
455
|
+
? anthropicSessionKeyFromParts({
|
|
456
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
457
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
458
|
+
promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
|
|
459
|
+
clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
|
|
460
|
+
promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
|
|
461
|
+
})
|
|
462
|
+
: null;
|
|
463
|
+
if (route.provider.authMode === "oauth") {
|
|
464
|
+
try {
|
|
465
|
+
if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
|
|
466
|
+
const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
|
|
467
|
+
if (!selection.accountId) {
|
|
468
|
+
if (selection.reason === "all-cooled") {
|
|
469
|
+
const retryAfterSec = getAnthropicPoolRetryAfterSeconds();
|
|
470
|
+
return formatErrorResponse(
|
|
471
|
+
429,
|
|
472
|
+
"rate_limit_error",
|
|
473
|
+
"All Anthropic OAuth accounts are temporarily rate-limited",
|
|
474
|
+
retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
|
|
478
|
+
}
|
|
479
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(selection.accountId), true, selection.reason);
|
|
480
|
+
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
481
|
+
anthropicPoolAccountId = admitted.accountId;
|
|
482
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
483
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
484
|
+
} else {
|
|
485
|
+
// Prefer the account with known headroom BEFORE the first attempt. Rotation alone
|
|
486
|
+
// only reacts to a 429, so a turn could open on an account a previous probe already
|
|
487
|
+
// measured as spent. A null answer means "use the active account", so every provider
|
|
488
|
+
// without quota evidence keeps the resolution it has today.
|
|
489
|
+
const preferredAccountId = isGenericFailoverProvider(route.providerName, route.provider)
|
|
490
|
+
? preferredInitialAccount(config, route.providerName)
|
|
491
|
+
: null;
|
|
492
|
+
// Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
|
|
493
|
+
// rotation site, and rotation sites must apply their credential through
|
|
494
|
+
// applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
|
|
495
|
+
// already pairs the snapshot's Kiro metadata, Copilot origin and Antigravity project
|
|
496
|
+
// with this same bearer, exactly as it does for the active account.
|
|
497
|
+
let usedPreferredAccount = preferredAccountId !== null;
|
|
498
|
+
let resolved: OAuthAccessSnapshot;
|
|
499
|
+
if (preferredAccountId) {
|
|
500
|
+
try {
|
|
501
|
+
// `requireUsableAccount` makes a removed OR reauth-flagged account throw from
|
|
502
|
+
// inside the resolver's own store read. Without it a revoked account resolves
|
|
503
|
+
// successfully — its credential is still readable — and the request would
|
|
504
|
+
// dispatch on an account already known to need a fresh login.
|
|
505
|
+
resolved = await getValidAccessSnapshotForAccount(
|
|
506
|
+
route.providerName,
|
|
507
|
+
preferredAccountId,
|
|
508
|
+
{ requireUsableAccount: true },
|
|
509
|
+
);
|
|
510
|
+
} catch {
|
|
511
|
+
// The roster is read behind a short TTL, so a preferred account can be removed
|
|
512
|
+
// or flagged for reauth in the window after it was cached. Resolving it then
|
|
513
|
+
// throws, and a PREFERENCE that turns a healthy request into a 401 is worse
|
|
514
|
+
// than no preference at all — the active account is still perfectly usable.
|
|
515
|
+
// Drop the stale roster so the next request re-reads it, and carry on.
|
|
516
|
+
forgetGenericFailoverRoster(route.providerName);
|
|
517
|
+
usedPreferredAccount = false;
|
|
518
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
522
|
+
}
|
|
523
|
+
// A Cloud Code Assist account needs its own project. Antigravity's refresh path
|
|
524
|
+
// tolerates project discovery failing, so a stored account can legitimately have
|
|
525
|
+
// none — and a PREFERENCE must never turn a working request into an error. Fall
|
|
526
|
+
// back to the ordinary active-account resolution instead, which is exactly what
|
|
527
|
+
// would have happened had the preference never existed.
|
|
528
|
+
if (usedPreferredAccount && route.provider.googleMode === "cloud-code-assist" && !resolved.projectId) {
|
|
529
|
+
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
530
|
+
usedPreferredAccount = false;
|
|
531
|
+
}
|
|
532
|
+
const admitted = await commitResolvedOAuthSelection(resolved, true);
|
|
533
|
+
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
534
|
+
if (admitted.accountId !== resolved.accountId) usedPreferredAccount = true;
|
|
535
|
+
resolved = admitted;
|
|
536
|
+
replayOAuthCredentialSnapshot = {
|
|
537
|
+
accountId: resolved.accountId,
|
|
538
|
+
generation: resolved.generation,
|
|
539
|
+
};
|
|
540
|
+
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
541
|
+
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
542
|
+
// Attribution is independent of failover (#2699): stamped from the resolved snapshot
|
|
543
|
+
// itself, not from inside the `isGenericFailoverProvider` branch below, so a future
|
|
544
|
+
// narrowing of that predicate cannot silently switch attribution off.
|
|
545
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, resolved.accountId);
|
|
546
|
+
// Remember which account actually served this request so a 429 cools THAT one, not
|
|
547
|
+
// whichever account is active by the time the response comes back (#2568).
|
|
548
|
+
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
549
|
+
genericFailoverAccountId = resolved.accountId;
|
|
550
|
+
// Advance the pool cursor only now that this account is actually admitted. The
|
|
551
|
+
// helper returns immediately unless the kernel is on AND the strategy is
|
|
552
|
+
// round-robin, so quota and fill-first pools reach it without being touched.
|
|
553
|
+
noteGenericPoolSelection(config, route.providerName, resolved.accountId);
|
|
554
|
+
}
|
|
555
|
+
// Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
|
|
556
|
+
// a fail-closed local-cli credential rule -- so without this stamp its identity is
|
|
557
|
+
// dropped whenever the pool flag is off, and a later 429 has no account to cool. Reactive
|
|
558
|
+
// failover needs only the id: no affinity bind, no promotion, no quota-ranked pick. Those
|
|
559
|
+
// are proactive and stay behind anthropicAccountPool.enabled.
|
|
560
|
+
if (route.providerName === "anthropic" && hasAnthropicFailoverQuorum()) {
|
|
561
|
+
anthropicPoolAccountId = resolved.accountId;
|
|
562
|
+
}
|
|
563
|
+
// Captured beside the account it fences, so the two can never disagree.
|
|
564
|
+
if (hasPassiveAccountQuota(route.providerName)) {
|
|
565
|
+
passiveQuotaWriterGeneration = captureConfigGeneration();
|
|
566
|
+
}
|
|
567
|
+
if (route.providerName === "kiro") {
|
|
568
|
+
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
569
|
+
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
570
|
+
parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
|
|
571
|
+
}
|
|
572
|
+
// Project identity belongs to the admitted account on EVERY request, including
|
|
573
|
+
// the request after a pool transition made that account the persisted active one.
|
|
574
|
+
if (route.provider.googleMode === "cloud-code-assist") {
|
|
575
|
+
if (!resolved.projectId) return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist account project is unavailable")));
|
|
576
|
+
route.provider = { ...route.provider, project: resolved.projectId };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
if (err instanceof UnsupportedOAuthProviderError) {
|
|
581
|
+
const safeProviderName = redactSecretString(route.providerName);
|
|
582
|
+
return formatErrorResponse(
|
|
583
|
+
400,
|
|
584
|
+
"invalid_request_error",
|
|
585
|
+
`${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Key-auth twin of the OAuth preference above: pick a warm key BEFORE the first attempt when
|
|
592
|
+
// the committed one is already cooling, instead of spending the request earning a 429 the
|
|
593
|
+
// runtime could already predict. The picker refuses to override a healthy committed key and
|
|
594
|
+
// returns null without a configured strategy, so an ordinary install evaluates one predicate.
|
|
595
|
+
//
|
|
596
|
+
// It RETURNS a rebuilt route rather than mutating one, and the assignment has to land here --
|
|
597
|
+
// ahead of the transport pin below, the adapterProvider copy that follows it, and the request
|
|
598
|
+
// the HTTP path bakes later. The image bridge and web search read route.provider directly and
|
|
599
|
+
// have no stale-selection re-read to save them, so ordering is the whole correctness argument.
|
|
600
|
+
//
|
|
601
|
+
// The Transport variant, not the bare picker: the picker answers with the PERSISTED row, and
|
|
602
|
+
// a built-in provider stored in its valid minimal form would lose the adapter id, base URL
|
|
603
|
+
// and static headers registry backfill supplies, throwing `Unknown adapter: undefined`.
|
|
604
|
+
const proactiveKeyProvider = selectProactiveApiKeyTransport(
|
|
605
|
+
config,
|
|
606
|
+
route.providerName,
|
|
607
|
+
route.provider,
|
|
608
|
+
parsed.options.promptCacheKey,
|
|
609
|
+
);
|
|
610
|
+
if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
|
|
611
|
+
route.provider = resolveProviderTransport(
|
|
612
|
+
route.providerName,
|
|
613
|
+
route.provider,
|
|
614
|
+
parsed.options.promptCacheKey,
|
|
615
|
+
route.providerName === "github-copilot" && route.provider.authMode === "oauth"
|
|
616
|
+
? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
|
|
617
|
+
: undefined,
|
|
618
|
+
);
|
|
619
|
+
let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
620
|
+
const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
621
|
+
&& !isCanonicalOpenAiForwardProvider(adapterProvider)
|
|
622
|
+
&& ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
|
|
623
|
+
|| providerConsumesCallerAuthorization(adapterProvider));
|
|
624
|
+
if (stripClaudeMainAuth) {
|
|
625
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
626
|
+
admissionState.authCtx = { kind: "main", accountId: null };
|
|
627
|
+
route.provider = stripCodexRuntimeProviderFields(route.provider);
|
|
628
|
+
adapterProvider = stripCodexRuntimeProviderFields(adapterProvider);
|
|
629
|
+
requestState.selectedForwardHeaders = new Headers(requestState.selectedForwardHeaders);
|
|
630
|
+
requestState.selectedForwardHeaders.delete("authorization");
|
|
631
|
+
requestState.selectedForwardHeaders.delete("chatgpt-account-id");
|
|
632
|
+
delete route.codexAccountMode;
|
|
633
|
+
delete route.codexAccountId;
|
|
634
|
+
delete route.codexAccountNamespace;
|
|
635
|
+
logCtx.provider = route.providerName;
|
|
636
|
+
delete logCtx.accountLogLabel;
|
|
637
|
+
}
|
|
638
|
+
adapter = resolveSelectionAdapter(adapterProvider, config.cacheRetention);
|
|
639
|
+
bindRouteReasoningReplayScope({
|
|
640
|
+
parsed,
|
|
641
|
+
providerName: route.providerName,
|
|
642
|
+
provider: adapterProvider,
|
|
643
|
+
adapterName: adapter.name,
|
|
644
|
+
oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
|
|
645
|
+
codexAuthContext: admissionState.authCtx,
|
|
646
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
647
|
+
});
|
|
648
|
+
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
649
|
+
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
650
|
+
}
|
|
651
|
+
logCtx.providerAdapter = adapter.name;
|
|
652
|
+
// Ordinary requests receive one durable attempt only after their final initial
|
|
653
|
+
// adapter is resolved. Combo children own their attempt and retries keep it.
|
|
654
|
+
if (!options.comboAttempt && !logCtx.activeAttempt) {
|
|
655
|
+
const attempt = beginRequestAttempt(
|
|
656
|
+
(logCtx.attempts?.length ?? 0) + 1,
|
|
657
|
+
logCtx.provider,
|
|
658
|
+
route.modelId,
|
|
659
|
+
adapter.name,
|
|
660
|
+
);
|
|
661
|
+
logCtx.activeAttempt = attempt;
|
|
662
|
+
logCtx.activeAttemptStartedAt = Date.now();
|
|
663
|
+
(logCtx.attempts ??= []).push(attempt);
|
|
664
|
+
}
|
|
665
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
666
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider, adapter.name);
|
|
667
|
+
runTurnAdapter = adapter;
|
|
668
|
+
if (adapter.runTurn) {
|
|
669
|
+
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
670
|
+
}
|
|
671
|
+
// Optional route-identity linkage for attempt correlation (CL-09 consumes it). The slot
|
|
672
|
+
// resolves to null unless an opt-in subsystem registered a linker, so an install without
|
|
673
|
+
// routing profiles does no work here and loads no additional module. The non-throwing
|
|
674
|
+
// guarantee lives in the slot helper.
|
|
675
|
+
if (logCtx.activeAttempt && !logCtx.activeAttempt.labRouteSubjectId) {
|
|
676
|
+
const passiveSubjectId = resolvePassiveRouteSubjectId(
|
|
677
|
+
config,
|
|
678
|
+
route.providerName,
|
|
679
|
+
route.modelId,
|
|
680
|
+
route.provider,
|
|
681
|
+
inboundWire,
|
|
682
|
+
);
|
|
683
|
+
if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
|
|
684
|
+
}
|
|
685
|
+
const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
|
|
686
|
+
|
|
687
|
+
const rawInput = (parsed._rawBody as { input?: unknown }).input;
|
|
688
|
+
if (!isPassthrough && Array.isArray(rawInput) && rawInput.some(
|
|
689
|
+
item => item !== null && typeof item === "object" && item.type === "computer_call_output",
|
|
690
|
+
)) {
|
|
691
|
+
return formatErrorResponse(
|
|
692
|
+
400,
|
|
693
|
+
"invalid_request_error",
|
|
694
|
+
"computer_call_output requires a Responses passthrough route; send screenshots as user input_image content on translated routes.",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
isOAuth401ReplayProvider,
|
|
700
|
+
get sentOAuthSnapshot(): OAuthAccessSnapshot | undefined {
|
|
701
|
+
return sentOAuthSnapshot;
|
|
702
|
+
},
|
|
703
|
+
set sentOAuthSnapshot(value: OAuthAccessSnapshot | undefined) {
|
|
704
|
+
sentOAuthSnapshot = value;
|
|
705
|
+
},
|
|
706
|
+
get replayOAuthCredentialSnapshot(): Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined {
|
|
707
|
+
return replayOAuthCredentialSnapshot;
|
|
708
|
+
},
|
|
709
|
+
set replayOAuthCredentialSnapshot(value: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined) {
|
|
710
|
+
replayOAuthCredentialSnapshot = value;
|
|
711
|
+
},
|
|
712
|
+
get anthropicPoolAccountId(): string | null {
|
|
713
|
+
return anthropicPoolAccountId;
|
|
714
|
+
},
|
|
715
|
+
set anthropicPoolAccountId(value: string | null) {
|
|
716
|
+
anthropicPoolAccountId = value;
|
|
717
|
+
},
|
|
718
|
+
get anthropicPoolFailovers(): typeof anthropicPoolFailovers {
|
|
719
|
+
return anthropicPoolFailovers;
|
|
720
|
+
},
|
|
721
|
+
set anthropicPoolFailovers(value: typeof anthropicPoolFailovers) {
|
|
722
|
+
anthropicPoolFailovers = value;
|
|
723
|
+
},
|
|
724
|
+
get genericFailoverAccountId(): string | null {
|
|
725
|
+
return genericFailoverAccountId;
|
|
726
|
+
},
|
|
727
|
+
set genericFailoverAccountId(value: string | null) {
|
|
728
|
+
genericFailoverAccountId = value;
|
|
729
|
+
},
|
|
730
|
+
get genericFailovers(): typeof genericFailovers {
|
|
731
|
+
return genericFailovers;
|
|
732
|
+
},
|
|
733
|
+
set genericFailovers(value: typeof genericFailovers) {
|
|
734
|
+
genericFailovers = value;
|
|
735
|
+
},
|
|
736
|
+
get adapter(): ProviderAdapter {
|
|
737
|
+
return adapter;
|
|
738
|
+
},
|
|
739
|
+
set adapter(value: ProviderAdapter) {
|
|
740
|
+
adapter = value;
|
|
741
|
+
},
|
|
742
|
+
get activeAdapter(): ProviderAdapter {
|
|
743
|
+
return activeAdapter;
|
|
744
|
+
},
|
|
745
|
+
set activeAdapter(value: ProviderAdapter) {
|
|
746
|
+
activeAdapter = value;
|
|
747
|
+
},
|
|
748
|
+
get runTurnAdapter(): ProviderAdapter {
|
|
749
|
+
return runTurnAdapter;
|
|
750
|
+
},
|
|
751
|
+
set runTurnAdapter(value: ProviderAdapter) {
|
|
752
|
+
runTurnAdapter = value;
|
|
753
|
+
},
|
|
754
|
+
get sameTargetRequest(): AdapterRequest | undefined {
|
|
755
|
+
return sameTargetRequest;
|
|
756
|
+
},
|
|
757
|
+
set sameTargetRequest(value: AdapterRequest | undefined) {
|
|
758
|
+
sameTargetRequest = value;
|
|
759
|
+
},
|
|
760
|
+
get sameTargetParsed(): OcxParsedRequest | undefined {
|
|
761
|
+
return sameTargetParsed;
|
|
762
|
+
},
|
|
763
|
+
set sameTargetParsed(value: OcxParsedRequest | undefined) {
|
|
764
|
+
sameTargetParsed = value;
|
|
765
|
+
},
|
|
766
|
+
get sameTargetToken(): typeof sameTargetToken {
|
|
767
|
+
return sameTargetToken;
|
|
768
|
+
},
|
|
769
|
+
set sameTargetToken(value: typeof sameTargetToken) {
|
|
770
|
+
sameTargetToken = value;
|
|
771
|
+
},
|
|
772
|
+
get transportToken(): typeof transportToken {
|
|
773
|
+
return transportToken;
|
|
774
|
+
},
|
|
775
|
+
set transportToken(value: typeof transportToken) {
|
|
776
|
+
transportToken = value;
|
|
777
|
+
},
|
|
778
|
+
get imageTierBias(): typeof imageTierBias {
|
|
779
|
+
return imageTierBias;
|
|
780
|
+
},
|
|
781
|
+
set imageTierBias(value: typeof imageTierBias) {
|
|
782
|
+
imageTierBias = value;
|
|
783
|
+
},
|
|
784
|
+
invalidateSameTargetRequest,
|
|
785
|
+
requestBindings,
|
|
786
|
+
adapterBindings,
|
|
787
|
+
commitResolvedOAuthSelection,
|
|
788
|
+
refreshResolvedOAuthSelection,
|
|
789
|
+
passiveQuotaWriterGeneration,
|
|
790
|
+
applyFailoverSnapshot,
|
|
791
|
+
selectionIsCurrent,
|
|
792
|
+
resolveSelectionAdapter,
|
|
793
|
+
refreshRunTurnAdapter,
|
|
794
|
+
oauthDispatch,
|
|
795
|
+
noteRoutedAttemptSend,
|
|
796
|
+
commitKeyAttemptSend,
|
|
797
|
+
bindKeyUsageFromBridge,
|
|
798
|
+
anthropicSessionKey,
|
|
799
|
+
isPassthrough,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export type ResponsesTransport = Exclude<Awaited<ReturnType<typeof prepareResponsesTransport>>, Response>;
|