@bitkyc08/opencodex 2.55.0 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- 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-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- 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 +611 -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 +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- 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 +554 -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 +422 -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 +95 -28
- 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 +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -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 +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- 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/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/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 +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- 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 +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -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/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 +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -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 +335 -0
- package/src/server/index.ts +40 -2547
- 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/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -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 +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import type { ResponsesRequestContext, ResponsesAdmissionState, ResponsesDispatchers } from "./core-options";
|
|
2
|
+
import {
|
|
3
|
+
agentTaskRecoveryConfig,
|
|
4
|
+
restoreCachedEncryptedAgentTasks,
|
|
5
|
+
recoverEncryptedAgentTaskWithResult,
|
|
6
|
+
} from "./agent-task-recovery";
|
|
7
|
+
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
|
|
8
|
+
import {
|
|
9
|
+
clientCancelledResponse,
|
|
10
|
+
decodeRequestErrorResponse,
|
|
11
|
+
comboUnavailable,
|
|
12
|
+
unreadableEncryptedAgentTaskResponse,
|
|
13
|
+
} from "./core-errors";
|
|
14
|
+
import { parseSyntheticRowId } from "../fast-row";
|
|
15
|
+
import { resolveComboId, comboIdFromRawBody, NoAvailableComboTargetsError } from "../../combos";
|
|
16
|
+
import { recallComboForLane } from "./combo-session-recall";
|
|
17
|
+
import {
|
|
18
|
+
sessionLaneIdFromRequest,
|
|
19
|
+
conversationIdFromResponsesRequest,
|
|
20
|
+
sessionIdHeaderFromRequest,
|
|
21
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
22
|
+
} from "../request-log-conversation";
|
|
23
|
+
import {
|
|
24
|
+
isShadowSourceModel,
|
|
25
|
+
shadowSourceModelPrefix,
|
|
26
|
+
shouldInterceptShadowCall,
|
|
27
|
+
} from "../../lib/shadow-call";
|
|
28
|
+
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
29
|
+
import {
|
|
30
|
+
hasUnreadableEncryptedAgentTask,
|
|
31
|
+
sanitizeEncryptedContentInPlace,
|
|
32
|
+
stripAgentMessageCiphertextInPlace,
|
|
33
|
+
} from "./encrypted-payload";
|
|
34
|
+
import {
|
|
35
|
+
codexPoolAffinityKey,
|
|
36
|
+
previewCodexPoolLineage,
|
|
37
|
+
applyCodexAuthContextToProvider,
|
|
38
|
+
} from "../../codex/auth-context";
|
|
39
|
+
import {
|
|
40
|
+
copyPreviousResponseReplayProvenance,
|
|
41
|
+
expandPreviousResponseInput,
|
|
42
|
+
previousResponseScopeMismatch,
|
|
43
|
+
previousResponseReplayFailure,
|
|
44
|
+
markBodyNonPersistable,
|
|
45
|
+
previousResponseProviderState,
|
|
46
|
+
} from "../../responses/state";
|
|
47
|
+
import { formatErrorResponse } from "../../bridge";
|
|
48
|
+
import type { OcxParsedRequest } from "../../types";
|
|
49
|
+
import { buildToolBridgeMaps } from "./collaboration";
|
|
50
|
+
import { parseRequest } from "../../responses/parser";
|
|
51
|
+
import { anthropicSessionKeyFromParts } from "../../oauth/anthropic-routing";
|
|
52
|
+
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
53
|
+
import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../responses/turn-termination";
|
|
54
|
+
import { requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
|
|
55
|
+
import type { RouteResult } from "../../router";
|
|
56
|
+
import {
|
|
57
|
+
routeConcreteModel,
|
|
58
|
+
routeCompactionModel,
|
|
59
|
+
routeModel,
|
|
60
|
+
NoEligiblePolicyCandidateError,
|
|
61
|
+
} from "../../router";
|
|
62
|
+
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
63
|
+
import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
64
|
+
import { isThreadSpawnRequest } from "../effort-policy";
|
|
65
|
+
import {
|
|
66
|
+
resolveSubagentFallbackChain,
|
|
67
|
+
maybePrimeSubagentQuota,
|
|
68
|
+
applySubagentModelFallback,
|
|
69
|
+
} from "../../codex/subagent-model-fallback";
|
|
70
|
+
import { codexAccountSelectionForTurn } from "../lifecycle";
|
|
71
|
+
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
72
|
+
import type {
|
|
73
|
+
SubagentPoolAccountPreview,
|
|
74
|
+
SubagentModelEligibleAccountIds,
|
|
75
|
+
} from "../../codex/subagent-model-fallback";
|
|
76
|
+
import {
|
|
77
|
+
codexRouteCredentialDomainHeaders,
|
|
78
|
+
codexRouteCredentialOwnership,
|
|
79
|
+
resolveResponsesCodexAuth,
|
|
80
|
+
withClaudeNativeSession,
|
|
81
|
+
} from "./core-auth";
|
|
82
|
+
import {
|
|
83
|
+
resolveSubagentFallbackModelEligibility,
|
|
84
|
+
canPassThroughEncryptedV2AgentTask,
|
|
85
|
+
applyFinalRouteRequestNormalization,
|
|
86
|
+
} from "./core-normalize";
|
|
87
|
+
import { resolveCodexModelEntitlements } from "../../codex/model-entitlements";
|
|
88
|
+
import {
|
|
89
|
+
previewCodexAccountForRequest,
|
|
90
|
+
codexQuotaScopeForModel,
|
|
91
|
+
formatCodexProviderForLog,
|
|
92
|
+
} from "../../codex/routing";
|
|
93
|
+
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
94
|
+
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
95
|
+
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
96
|
+
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
97
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
98
|
+
import { hasUnmappedRoutedCustomToolOutput } from "../../responses/custom-tool-compat";
|
|
99
|
+
import { PROVIDER_OWNED_CONTINUATION_WIRES, resolvedAdapterWire } from "../../responses/continuation-ownership";
|
|
100
|
+
import {
|
|
101
|
+
isCodexReserveHelperUnsupported,
|
|
102
|
+
CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE,
|
|
103
|
+
} from "../../codex/loopback-target";
|
|
104
|
+
import { checkInputAdmission } from "./input-admission";
|
|
105
|
+
import { nativeContextLimits } from "../../codex/catalog";
|
|
106
|
+
import { streamingContextOverflowResponse } from "./context-overflow";
|
|
107
|
+
import {
|
|
108
|
+
preAuthUpstreamHostCircuitKey,
|
|
109
|
+
upstreamHostCircuitOpenResponse,
|
|
110
|
+
applyCodexAccountGatedWireNormalization,
|
|
111
|
+
codexLogAccountId,
|
|
112
|
+
} from "./core-codex-account";
|
|
113
|
+
import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
114
|
+
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
115
|
+
import {
|
|
116
|
+
conversationStateBindingFromAuth,
|
|
117
|
+
applyAccountChangeConversationStateScrub,
|
|
118
|
+
} from "./account-change-state";
|
|
119
|
+
|
|
120
|
+
/** Parses, selects, and admits one request without changing the dispatch policy. */
|
|
121
|
+
export async function prepareResponsesRequest(
|
|
122
|
+
requestContext: Pick<ResponsesRequestContext, "options" | "config" | "req" | "logCtx">,
|
|
123
|
+
admissionState: ResponsesAdmissionState,
|
|
124
|
+
requestDispatchers: ResponsesDispatchers,
|
|
125
|
+
) {
|
|
126
|
+
const { options, config, req, logCtx } = requestContext;
|
|
127
|
+
|
|
128
|
+
// The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
|
|
129
|
+
// so an omitted value means a genuine Responses inbound.
|
|
130
|
+
const inboundWire = options.inboundWire ?? "responses";
|
|
131
|
+
const translatorBudget = options.translatorBudget;
|
|
132
|
+
const agentTaskRecovery = agentTaskRecoveryConfig(config);
|
|
133
|
+
let body: unknown;
|
|
134
|
+
try {
|
|
135
|
+
body = await readJsonRequestBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
138
|
+
return clientCancelledResponse();
|
|
139
|
+
}
|
|
140
|
+
return decodeRequestErrorResponse(err, "responses");
|
|
141
|
+
}
|
|
142
|
+
// An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
|
|
143
|
+
// as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
|
|
144
|
+
const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
|
|
145
|
+
&& typeof (body as { model?: unknown }).model === "string"
|
|
146
|
+
// One parse for both grammars, from the selector as the client sent it. Parsing them
|
|
147
|
+
// separately made the outcome depend on which ran first.
|
|
148
|
+
? parseSyntheticRowId((body as { model: string }).model, config)
|
|
149
|
+
: { fastRow: null, effortRow: null };
|
|
150
|
+
const comboEffortRow = comboRows.effortRow;
|
|
151
|
+
if (comboRows.fastRow) {
|
|
152
|
+
// Same reason as the effort row above: the combo dispatcher reads `model` next, so the
|
|
153
|
+
// selector has to be normalized before it, or a combo child is built from a synthetic id.
|
|
154
|
+
const raw = body as Record<string, unknown>;
|
|
155
|
+
raw.model = comboRows.fastRow.baseId;
|
|
156
|
+
// A caller INTENT, not a decision. decideTier still rules on eligibility downstream, so
|
|
157
|
+
// fastMode:false and an ineligible route both still suppress it.
|
|
158
|
+
raw.service_tier = "priority";
|
|
159
|
+
}
|
|
160
|
+
if (comboEffortRow) {
|
|
161
|
+
const raw = body as Record<string, unknown>;
|
|
162
|
+
raw.model = comboEffortRow.baseId;
|
|
163
|
+
const rawReasoning = raw.reasoning;
|
|
164
|
+
raw.reasoning = {
|
|
165
|
+
...(rawReasoning && typeof rawReasoning === "object" && !Array.isArray(rawReasoning)
|
|
166
|
+
? rawReasoning as Record<string, unknown>
|
|
167
|
+
: {}),
|
|
168
|
+
effort: comboEffortRow.effort,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Compaction may send the last client-visible bare model after a combo switch.
|
|
172
|
+
// Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
|
|
173
|
+
if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
|
|
174
|
+
const rawModel = (body as { model?: unknown }).model;
|
|
175
|
+
const rawInput = (body as { input?: unknown }).input;
|
|
176
|
+
const isCompactionTrigger = Array.isArray(rawInput)
|
|
177
|
+
&& rawInput.some((item: unknown) =>
|
|
178
|
+
typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger");
|
|
179
|
+
if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger
|
|
180
|
+
&& !comboRows.fastRow && !comboEffortRow
|
|
181
|
+
&& !resolveComboId(config, rawModel)) {
|
|
182
|
+
const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), rawModel);
|
|
183
|
+
if (recalledComboId) {
|
|
184
|
+
(body as Record<string, unknown>).model = `combo/${recalledComboId}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// A shadow-call replacement that names a COMBO is routing policy, not the identity of any
|
|
189
|
+
// one pick. The late intercept site below resolves it through routeModel/tryPickComboModel,
|
|
190
|
+
// which collapses the table to a single target while still tagging `routeKind: "combo"`, so
|
|
191
|
+
// the combo gate on the next line never fires, handleComboResponses never runs, and 429/5xx
|
|
192
|
+
// hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
|
|
193
|
+
// here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
|
|
194
|
+
// LOOKUP so the check can never observe a one-candidate collapse.
|
|
195
|
+
if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
|
|
196
|
+
const shadowIntercept = config.shadowCallIntercept;
|
|
197
|
+
const rawShadowModel = (body as { model?: unknown }).model;
|
|
198
|
+
if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
|
|
199
|
+
&& isShadowSourceModel(rawShadowModel, shadowIntercept.sourceModels)) {
|
|
200
|
+
const shadowComboId = resolveComboId(config, shadowIntercept.model);
|
|
201
|
+
if (shadowComboId && Object.hasOwn(config.combos ?? {}, shadowComboId)) {
|
|
202
|
+
(body as Record<string, unknown>).model = shadowIntercept.model;
|
|
203
|
+
// Same rule as the late intercept site: record the operator-configured prefix that
|
|
204
|
+
// matched, never the caller's raw model string. Matching is by prefix, so the raw
|
|
205
|
+
// value is caller-controlled and reaches usage.jsonl and /api/logs.
|
|
206
|
+
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
|
|
207
|
+
shadowSourceModelPrefix(rawShadowModel, shadowIntercept.sourceModels),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
|
|
213
|
+
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
214
|
+
options.onRequestBodyRead?.();
|
|
215
|
+
return requestDispatchers.handleComboResponses(req, body, comboId, config, logCtx, {
|
|
216
|
+
...options,
|
|
217
|
+
// The original request body was accepted above. Combo children are synthetic
|
|
218
|
+
// replays and must not repeat the caller-owned timeout transition.
|
|
219
|
+
onRequestBodyRead: undefined,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
223
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
224
|
+
);
|
|
225
|
+
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
226
|
+
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
227
|
+
const originalBody = body;
|
|
228
|
+
if (options.comboReplaySnapshot) {
|
|
229
|
+
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
230
|
+
} else {
|
|
231
|
+
body = expandPreviousResponseInput(body, inboundClientThreadId);
|
|
232
|
+
if (previousResponseScopeMismatch(body)) {
|
|
233
|
+
console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
|
|
234
|
+
}
|
|
235
|
+
if (previousResponseReplayFailure(body)) {
|
|
236
|
+
return formatErrorResponse(
|
|
237
|
+
400,
|
|
238
|
+
"previous_response_not_found",
|
|
239
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const previousResponseInputExpanded = options.comboReplaySnapshot?.previousResponseInputExpanded
|
|
244
|
+
?? (body !== originalBody
|
|
245
|
+
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string");
|
|
246
|
+
|
|
247
|
+
// Spawn-message compatibility (both directions): agent_message task payloads ride in
|
|
248
|
+
// encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
|
|
249
|
+
// parsing so every consumer sees the payload: parseRequest (routed/translated providers read
|
|
250
|
+
// the parsed messages) and the native passthrough (_rawBody is this same object, serialized
|
|
251
|
+
// verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
|
|
252
|
+
{
|
|
253
|
+
const rewritten = sanitizeEncryptedContentInPlace(
|
|
254
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
255
|
+
);
|
|
256
|
+
if (rewritten > 0)
|
|
257
|
+
console.warn(
|
|
258
|
+
`[opencodex] rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (spawn-message compatibility)`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let parsed: OcxParsedRequest;
|
|
263
|
+
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
264
|
+
try {
|
|
265
|
+
parsed = parseRequest(body);
|
|
266
|
+
parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
|
|
267
|
+
// Captured before any parser mutates it, so both grammars see the client's id.
|
|
268
|
+
const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
|
|
269
|
+
if (fastRow) {
|
|
270
|
+
parsed.modelId = fastRow.baseId;
|
|
271
|
+
parsed.options.serviceTier = "priority";
|
|
272
|
+
const raw = parsed._rawBody as Record<string, unknown>;
|
|
273
|
+
raw.model = fastRow.baseId;
|
|
274
|
+
raw.service_tier = "priority";
|
|
275
|
+
}
|
|
276
|
+
if (effortRow) {
|
|
277
|
+
parsed.modelId = effortRow.baseId;
|
|
278
|
+
parsed.options.reasoning = effortRow.effort;
|
|
279
|
+
const raw = parsed._rawBody as Record<string, unknown>;
|
|
280
|
+
const rawReasoning = raw.reasoning;
|
|
281
|
+
raw.model = effortRow.baseId;
|
|
282
|
+
raw.reasoning = {
|
|
283
|
+
...(rawReasoning && typeof rawReasoning === "object" && !Array.isArray(rawReasoning)
|
|
284
|
+
? rawReasoning as Record<string, unknown>
|
|
285
|
+
: {}),
|
|
286
|
+
effort: effortRow.effort,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (options.comboReplaySnapshot?.recoveredPlaintext) {
|
|
290
|
+
markBodyNonPersistable(parsed._rawBody);
|
|
291
|
+
}
|
|
292
|
+
toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
|
|
293
|
+
if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
|
|
294
|
+
const providerContinuationCandidate = options.comboReplaySnapshot
|
|
295
|
+
? options.comboReplaySnapshot.providerContinuation
|
|
296
|
+
: previousResponseProviderState(parsed.previousResponseId);
|
|
297
|
+
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
298
|
+
if (inboundClientThreadId) {
|
|
299
|
+
parsed._clientThreadId = inboundClientThreadId;
|
|
300
|
+
} else if (
|
|
301
|
+
options.inboundWire === "anthropic"
|
|
302
|
+
&& options.promptCacheKeyIsSharedCohort !== true
|
|
303
|
+
&& typeof parsed.options.promptCacheKey === "string"
|
|
304
|
+
&& parsed.options.promptCacheKey.trim().length > 0
|
|
305
|
+
) {
|
|
306
|
+
// Claude Code has no Codex parent-thread header, but its metadata.user_id is
|
|
307
|
+
// translated into a stable per-session prompt_cache_key. Use it as the replay
|
|
308
|
+
// thread identity so Gemini thought signatures are remembered by call_id for
|
|
309
|
+
// Anthropic Messages clients too (#1735/#1926). Keep `_clientThreadId` unset so
|
|
310
|
+
// existing provider session-id derivation (first-user-text fallback) is unchanged.
|
|
311
|
+
// Normalize through anthropicSessionKeyFromParts so overlong keys are hashed and
|
|
312
|
+
// trimming matches the affinity/session-key path exactly (no raw >128-char ids).
|
|
313
|
+
const normalizedCacheKey = anthropicSessionKeyFromParts({
|
|
314
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
315
|
+
// The enclosing branch already proves this is not the shared cohort.
|
|
316
|
+
promptCacheKeyIsSharedCohort: false,
|
|
317
|
+
});
|
|
318
|
+
if (normalizedCacheKey) {
|
|
319
|
+
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
325
|
+
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
326
|
+
code: "translation_buffer_limit",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
330
|
+
}
|
|
331
|
+
options.onRequestBodyRead?.();
|
|
332
|
+
const responseStateOptions = (force = false): { force?: boolean; clientThreadId?: string } => ({
|
|
333
|
+
...(force ? { force: true } : {}),
|
|
334
|
+
...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
|
|
335
|
+
});
|
|
336
|
+
const resolvedConversationId = conversationIdFromResponsesRequest({
|
|
337
|
+
clientThreadId: parsed._clientThreadId,
|
|
338
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
339
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
340
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
341
|
+
});
|
|
342
|
+
bindTurnTerminationScope(parsed, resolvedConversationId);
|
|
343
|
+
const rememberKiroDeliveredFinalAnswer = (adapterName: string, response: unknown): void => {
|
|
344
|
+
if (adapterName === "kiro") rememberDeliveredFinalAnswer(parsed, response);
|
|
345
|
+
};
|
|
346
|
+
// _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
|
|
347
|
+
// a dedicated raw conversation namespace so mixed headers that carry the same identity still
|
|
348
|
+
// match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
|
|
349
|
+
// Keep an Anthropic prompt_cache_key scope already bound above (#1735/#1926).
|
|
350
|
+
if (!parsed._reasoningReplayScope) {
|
|
351
|
+
const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
|
|
352
|
+
clientThreadId: parsed._clientThreadId,
|
|
353
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
354
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
355
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
356
|
+
});
|
|
357
|
+
if (reasoningReplayConversationId) {
|
|
358
|
+
parsed._reasoningReplayScope = { clientThreadId: reasoningReplayConversationId };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
362
|
+
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
363
|
+
if (!logCtx.conversationId) {
|
|
364
|
+
logCtx.conversationId = resolvedConversationId;
|
|
365
|
+
}
|
|
366
|
+
logCtx.requestedModel = parsed.modelId;
|
|
367
|
+
logCtx.requestedEffort = parsed.options.reasoning;
|
|
368
|
+
logCtx.callerServiceTier = sanitizeLogMetadataString(parsed.options.serviceTier);
|
|
369
|
+
logCtx.requestedServiceTier = parsed.options.serviceTier;
|
|
370
|
+
logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
|
|
371
|
+
logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
|
|
372
|
+
logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
|
|
373
|
+
|
|
374
|
+
let route: RouteResult;
|
|
375
|
+
let credentialDomainWasRewritten = false;
|
|
376
|
+
try {
|
|
377
|
+
// A `compaction_trigger` turn may name a bare native model the operator has
|
|
378
|
+
// no canonical OpenAI route for (#2901). Only the initial compaction route
|
|
379
|
+
// may fall back to the configured default provider; combo attempts and the
|
|
380
|
+
// later fallback/recovery re-routes keep the ordinary reservation.
|
|
381
|
+
const resolveRoute = (modelId: string) => options.comboAttempt
|
|
382
|
+
? routeConcreteModel(config, modelId)
|
|
383
|
+
: parsed._compactionRequest === true
|
|
384
|
+
? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
|
|
385
|
+
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
|
|
386
|
+
const _sci = config.shadowCallIntercept;
|
|
387
|
+
let shadowRoute: RouteResult | undefined;
|
|
388
|
+
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
389
|
+
const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
|
|
390
|
+
let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
|
|
391
|
+
try {
|
|
392
|
+
const resolvedSource = routeConcreteModel(config, parsed.modelId);
|
|
393
|
+
sourceIdentity = { providerName: resolvedSource.providerName, modelId: sourcePrefix };
|
|
394
|
+
} catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
|
|
395
|
+
const targetRoute = resolveRoute(_sci.model);
|
|
396
|
+
if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
|
|
397
|
+
credentialDomainWasRewritten = true;
|
|
398
|
+
const _sciOriginal = parsed.modelId;
|
|
399
|
+
parsed.modelId = _sci.model;
|
|
400
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
401
|
+
(parsed._rawBody as { model?: string }).model = _sci.model;
|
|
402
|
+
}
|
|
403
|
+
// Record the operator-configured prefix that matched, NOT the caller's raw model string.
|
|
404
|
+
// Matching is by prefix, so a caller can append arbitrary text and still intercept; that
|
|
405
|
+
// raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
|
|
406
|
+
// that does not recognize every credential family. The prefix is a value the operator
|
|
407
|
+
// configured, so no caller-controlled string is persisted.
|
|
408
|
+
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
|
|
409
|
+
shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
|
|
410
|
+
);
|
|
411
|
+
// Helpers must not resume/append into the parent thread's Cursor conversation.
|
|
412
|
+
parsed._cursorIsolateConversation = true;
|
|
413
|
+
shadowRoute = targetRoute;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
417
|
+
route = shadowRoute ?? resolveRoute(parsed.modelId);
|
|
418
|
+
logCtx.routeDecision = route.routeDecision;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err instanceof NoAvailableComboTargetsError) {
|
|
421
|
+
return comboUnavailable(err.comboId);
|
|
422
|
+
}
|
|
423
|
+
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
424
|
+
// Persist the evaluation trace (per-candidate exclusions + the
|
|
425
|
+
// no-eligible reason) so failed policy requests stay auditable.
|
|
426
|
+
logCtx.routeDecision = err.trace;
|
|
427
|
+
}
|
|
428
|
+
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
|
|
432
|
+
&& parsed._previousResponseInputExpanded !== true;
|
|
433
|
+
// Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
|
|
434
|
+
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
435
|
+
// provider with native continuation support below.
|
|
436
|
+
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
437
|
+
const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
|
|
438
|
+
? resolveSubagentFallbackChain(parsed, config)
|
|
439
|
+
: null;
|
|
440
|
+
const previewSelectionAdmission = threadSpawn
|
|
441
|
+
&& !options.comboAttempt
|
|
442
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
443
|
+
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
444
|
+
: undefined;
|
|
445
|
+
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
446
|
+
const nativeMainReadsForbidden = nativeMainRecoveryBlocked
|
|
447
|
+
|| previewSelectionAdmission?.mainProfileDraining === true;
|
|
448
|
+
const previewSelectionOptions = {
|
|
449
|
+
nativeMainSelectionOnly: !nativeMainRecoveryBlocked
|
|
450
|
+
&& previewSelectionAdmission?.mainProfileDraining === true,
|
|
451
|
+
};
|
|
452
|
+
let selectedForwardHeaders = req.headers;
|
|
453
|
+
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
454
|
+
let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
|
|
455
|
+
let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
|
|
456
|
+
let subagentQuotaFailureModel = parsed.modelId;
|
|
457
|
+
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
458
|
+
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
459
|
+
// Preview has to see the same lineage resolve does. Without it, a child's first turn is
|
|
460
|
+
// previewed as a cold pick and resolved onto the family account, and the subagent fallback
|
|
461
|
+
// then decides model eligibility against an account the request will never use.
|
|
462
|
+
//
|
|
463
|
+
// "The same" means both halves of the question the final resolution asks. The Authorization
|
|
464
|
+
// it will be given, because the lineage scope is an HMAC of exactly that header; and its own
|
|
465
|
+
// Pool-state predicate, because a fixed account selector and a request-owned credential
|
|
466
|
+
// deliberately create no affinity at all -- previewing a family binding for one of those would
|
|
467
|
+
// hand model fallback an account this request can never authenticate as. Read-only: the record
|
|
468
|
+
// is written by the resolution that binds, never by a preview that may own no Pool state.
|
|
469
|
+
const previewAuthHeaders = codexRouteCredentialDomainHeaders(
|
|
470
|
+
req,
|
|
471
|
+
route,
|
|
472
|
+
options,
|
|
473
|
+
credentialDomainWasRewritten,
|
|
474
|
+
);
|
|
475
|
+
const poolLineage = previewCodexPoolLineage(previewAuthHeaders, options.codexAuthPolicy ?? config, {
|
|
476
|
+
accountId: route.codexAccountId,
|
|
477
|
+
modelId: route.modelId,
|
|
478
|
+
admission: options.admission,
|
|
479
|
+
requestScopedMainCredential: codexRouteCredentialOwnership(
|
|
480
|
+
previewAuthHeaders,
|
|
481
|
+
config,
|
|
482
|
+
route,
|
|
483
|
+
options,
|
|
484
|
+
).requestScopedMainCredential,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
if (
|
|
489
|
+
threadSpawn
|
|
490
|
+
&& route.codexAccountId === undefined
|
|
491
|
+
&& !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
|
|
492
|
+
) {
|
|
493
|
+
await maybePrimeSubagentQuota(config, Date.now(), { nativeMainReadsForbidden });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Subagent fallback must settle the final model/provider BEFORE route-dependent
|
|
497
|
+
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
498
|
+
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
499
|
+
// tokens — auth is resolved only after the final route is selected.
|
|
500
|
+
if (
|
|
501
|
+
threadSpawn
|
|
502
|
+
&& !options.comboAttempt
|
|
503
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
504
|
+
) {
|
|
505
|
+
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
506
|
+
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
507
|
+
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
508
|
+
// model scope, making the preview diverge from the account that actually authenticates.
|
|
509
|
+
const fallbackChain = initialSubagentFallbackChain;
|
|
510
|
+
subagentFallbackModelEligibleAccountIdsForModel = await resolveSubagentFallbackModelEligibility({
|
|
511
|
+
config,
|
|
512
|
+
fallbackChain,
|
|
513
|
+
nativeMainReadsForbidden,
|
|
514
|
+
resolver: options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements,
|
|
515
|
+
});
|
|
516
|
+
const fallbackNow = Date.now();
|
|
517
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
518
|
+
poolAffinityKey,
|
|
519
|
+
config,
|
|
520
|
+
previewNow,
|
|
521
|
+
codexQuotaScopeForModel(modelId),
|
|
522
|
+
{ ...previewSelectionOptions, modelEligibleAccountIds },
|
|
523
|
+
modelId,
|
|
524
|
+
poolLineage,
|
|
525
|
+
);
|
|
526
|
+
const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
|
|
527
|
+
route.modelId,
|
|
528
|
+
fallbackNow,
|
|
529
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(route.modelId),
|
|
530
|
+
);
|
|
531
|
+
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
532
|
+
const fallback = applySubagentModelFallback(
|
|
533
|
+
parsed,
|
|
534
|
+
req.headers,
|
|
535
|
+
config,
|
|
536
|
+
previewAccountId,
|
|
537
|
+
fallbackNow,
|
|
538
|
+
unreadableEncryptedAgentTask,
|
|
539
|
+
previewSelectionOptions,
|
|
540
|
+
subagentFallbackAccountPreview,
|
|
541
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
542
|
+
fallbackChain,
|
|
543
|
+
candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
|
|
544
|
+
);
|
|
545
|
+
if (fallback) {
|
|
546
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
547
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
548
|
+
if (isInjectionDebugEnabled()) {
|
|
549
|
+
injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
|
|
553
|
+
|
|
554
|
+
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
555
|
+
try {
|
|
556
|
+
route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
|
|
557
|
+
credentialDomainWasRewritten = true;
|
|
558
|
+
logCtx.routeDecision = route.routeDecision;
|
|
559
|
+
} catch (err) {
|
|
560
|
+
if (err instanceof NoAvailableComboTargetsError) {
|
|
561
|
+
return comboUnavailable(err.comboId);
|
|
562
|
+
}
|
|
563
|
+
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
564
|
+
logCtx.routeDecision = err.trace;
|
|
565
|
+
}
|
|
566
|
+
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} finally {
|
|
571
|
+
previewSelectionAdmission?.release();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
575
|
+
// Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
|
|
576
|
+
// so recover only after final route selection.
|
|
577
|
+
//
|
|
578
|
+
// Deliberately NOT gated on `threadSpawn` (#4089). Switching a live thread from a native
|
|
579
|
+
// ChatGPT model to a routed provider replays a backend-minted encrypted agent message on every
|
|
580
|
+
// later turn, and a model switch is not a spawn, so the spawn requirement failed the thread
|
|
581
|
+
// closed permanently without ever attempting recovery. The trust boundary is
|
|
582
|
+
// `recoveryAdmission()` in ./agent-task-recovery -- Codex originator, live native ChatGPT
|
|
583
|
+
// bearer, matching chatgpt-account-id, no inbound API key, no proxy-admission secret -- which
|
|
584
|
+
// admits only the owner of the session that would be spent. `threadSpawn` narrowed which of
|
|
585
|
+
// that owner's own requests could use their own session; it kept nobody else out. The combo
|
|
586
|
+
// gate above keeps its spawn requirement: that path has its own native-target filtering and
|
|
587
|
+
// per-attempt failover, and the reported defect is on this path.
|
|
588
|
+
if (
|
|
589
|
+
inboundWire === "responses"
|
|
590
|
+
&& agentTaskRecovery
|
|
591
|
+
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
592
|
+
&& !options.comboAttempt
|
|
593
|
+
&& !canPassThroughEncryptedV2AgentTask(route, inboundWire)
|
|
594
|
+
) {
|
|
595
|
+
let recovered = restoreCachedEncryptedAgentTasks(
|
|
596
|
+
req, (body as { input?: unknown } | undefined)?.input, config, { parentThreadId },
|
|
597
|
+
) > 0;
|
|
598
|
+
unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
599
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
600
|
+
);
|
|
601
|
+
if (unreadableEncryptedAgentTask) try {
|
|
602
|
+
const result = await recoverEncryptedAgentTaskWithResult(
|
|
603
|
+
req,
|
|
604
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
605
|
+
agentTaskRecovery,
|
|
606
|
+
config,
|
|
607
|
+
{ parentThreadId, abortSignal: options.abortSignal },
|
|
608
|
+
);
|
|
609
|
+
recovered = result.recovered;
|
|
610
|
+
recoveryFailureReason = result.recovered ? undefined : result.reason;
|
|
611
|
+
} catch {
|
|
612
|
+
recovered = false;
|
|
613
|
+
recoveryFailureReason = undefined;
|
|
614
|
+
}
|
|
615
|
+
if (recovered) {
|
|
616
|
+
unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
617
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
618
|
+
);
|
|
619
|
+
if (!unreadableEncryptedAgentTask) {
|
|
620
|
+
try {
|
|
621
|
+
const reparsed = parseRequest(body);
|
|
622
|
+
const kept: Array<keyof OcxParsedRequest> = [
|
|
623
|
+
"_previousResponseInputExpanded",
|
|
624
|
+
"_providerContinuation",
|
|
625
|
+
"_providerContinuationCandidate",
|
|
626
|
+
"_providerContinuationOwner",
|
|
627
|
+
"_cursorConversationId",
|
|
628
|
+
"_clientThreadId",
|
|
629
|
+
"_promptCacheKeyIsSharedCohort",
|
|
630
|
+
"_cursorClientThreadId",
|
|
631
|
+
"_reasoningReplayScope",
|
|
632
|
+
"_cursorIsolateConversation",
|
|
633
|
+
];
|
|
634
|
+
for (const key of kept) {
|
|
635
|
+
if (parsed[key] !== undefined) {
|
|
636
|
+
(reparsed as unknown as Record<string, unknown>)[key] = parsed[key];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
bindTurnTerminationScope(reparsed, resolvedConversationId);
|
|
640
|
+
parsed = reparsed;
|
|
641
|
+
// The recovery mutated `body.input` in place, so `_rawBody` now carries decrypted task
|
|
642
|
+
// text. Bar it from the continuation cache before any recording path can reach it —
|
|
643
|
+
// that cache is persisted to disk, which would defeat the recovery cache's TTL.
|
|
644
|
+
markBodyNonPersistable(parsed._rawBody);
|
|
645
|
+
|
|
646
|
+
// The ciphertext-only pass intentionally excludes routed candidates. Once recovery
|
|
647
|
+
// makes the assignment readable, run selection again with the full configured chain
|
|
648
|
+
// and keep the route in sync with any newly selected fallback.
|
|
649
|
+
const recoverySelectionAdmission = codexAccountSelectionForTurn(options.turnAdmissionLease)?.();
|
|
650
|
+
const fallback = (() => {
|
|
651
|
+
try {
|
|
652
|
+
const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
|
|
653
|
+
const recoverySelectionOptions = {
|
|
654
|
+
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
655
|
+
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
656
|
+
};
|
|
657
|
+
const recoveryNow = Date.now();
|
|
658
|
+
// Carry the entitlement filter through recovery too (#2509/#2623). The scope was
|
|
659
|
+
// already re-previewed per candidate here; the ELIGIBLE-ACCOUNT set was not, so a
|
|
660
|
+
// recovered assignment could select an account that is not entitled to the model
|
|
661
|
+
// and then fail closed at final auth — the same class of stale-selection bug as
|
|
662
|
+
// the quota scope, one layer over.
|
|
663
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
664
|
+
poolAffinityKey,
|
|
665
|
+
config,
|
|
666
|
+
previewNow,
|
|
667
|
+
codexQuotaScopeForModel(modelId),
|
|
668
|
+
{ ...recoverySelectionOptions, modelEligibleAccountIds },
|
|
669
|
+
modelId,
|
|
670
|
+
poolLineage,
|
|
671
|
+
);
|
|
672
|
+
const recoveryPreviewAccountId = subagentFallbackAccountPreview(
|
|
673
|
+
parsed.modelId,
|
|
674
|
+
recoveryNow,
|
|
675
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(parsed.modelId),
|
|
676
|
+
);
|
|
677
|
+
return applySubagentModelFallback(
|
|
678
|
+
parsed,
|
|
679
|
+
req.headers,
|
|
680
|
+
config,
|
|
681
|
+
recoveryPreviewAccountId,
|
|
682
|
+
recoveryNow,
|
|
683
|
+
false,
|
|
684
|
+
recoverySelectionOptions,
|
|
685
|
+
subagentFallbackAccountPreview,
|
|
686
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
687
|
+
);
|
|
688
|
+
} finally {
|
|
689
|
+
recoverySelectionAdmission?.release();
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
692
|
+
if (fallback) {
|
|
693
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
694
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
695
|
+
if (isInjectionDebugEnabled()) {
|
|
696
|
+
injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
|
|
700
|
+
|
|
701
|
+
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
702
|
+
try {
|
|
703
|
+
route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
|
|
704
|
+
credentialDomainWasRewritten = true;
|
|
705
|
+
logCtx.routeDecision = route.routeDecision;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (err instanceof NoAvailableComboTargetsError) {
|
|
708
|
+
return comboUnavailable(err.comboId);
|
|
709
|
+
}
|
|
710
|
+
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
711
|
+
logCtx.routeDecision = err.trace;
|
|
712
|
+
}
|
|
713
|
+
return formatErrorResponse(
|
|
714
|
+
404,
|
|
715
|
+
"invalid_request_error",
|
|
716
|
+
err instanceof Error ? err.message : String(err),
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} catch {
|
|
721
|
+
unreadableEncryptedAgentTask = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
728
|
+
|
|
729
|
+
// Encrypted child tasks may reach the canonical native backend or an explicitly trusted
|
|
730
|
+
// direct Responses route. This runs against the FINAL route so native-only fallback can
|
|
731
|
+
// rescue an incompatible primary without weakening combo behavior.
|
|
732
|
+
const finalRouteCanPassThroughEncryptedTask = !options.comboAttempt
|
|
733
|
+
&& canPassThroughEncryptedV2AgentTask(route, inboundWire);
|
|
734
|
+
if (
|
|
735
|
+
(route.combo !== undefined || !isCanonicalOpenAiForwardProvider(route.provider))
|
|
736
|
+
&& !finalRouteCanPassThroughEncryptedTask
|
|
737
|
+
&& unreadableEncryptedAgentTask
|
|
738
|
+
) {
|
|
739
|
+
return unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// The guard above asks whether the CURRENT worker task is readable, and it only inspects the
|
|
743
|
+
// tail item. An `agent_message` that mixes readable text with backend ciphertext answers
|
|
744
|
+
// "readable" to that question at every position, so it passed -- and then
|
|
745
|
+
// `normalizeRoutedAgentMessages` refused to lower it, because lowering requires every part to
|
|
746
|
+
// be representable. The raw Responses passthrough serialized the private item as it stood, so
|
|
747
|
+
// backend ciphertext and an item type only the Codex backend declares reached a third-party
|
|
748
|
+
// provider, which answered `422 unknown item type "agent_message"` (#4454).
|
|
749
|
+
//
|
|
750
|
+
// The opaque-blob path already knows the repair: replace the undecryptable part with an
|
|
751
|
+
// omission marker, which leaves the item lowerable. It applied that repair only AFTER an
|
|
752
|
+
// upstream rejection. For a destination that cannot accept the private item under any
|
|
753
|
+
// circumstances, that round trip was never going to succeed and sent the ciphertext to find
|
|
754
|
+
// out, so do the repair here instead. Recovery above has already had its chance to turn the
|
|
755
|
+
// same bytes into real plaintext; only what it could not rescue reaches this.
|
|
756
|
+
if (inboundWire === "responses" && !finalRouteCanPassThroughEncryptedTask) {
|
|
757
|
+
// Only the raw Responses passthrough puts input items on the wire verbatim, so that is the
|
|
758
|
+
// only wire this has to repair: translated wires rebuild the body from parsed messages, where
|
|
759
|
+
// `inputContentParts` drops an encrypted part instead of forwarding it. The exemption is the
|
|
760
|
+
// canonical Codex backend alone, because it is the one destination that minted these bytes and
|
|
761
|
+
// can read them. `authMode: "forward"` is NOT that test -- a noncanonical forward gateway is
|
|
762
|
+
// somebody else's server that happens to be configured for passthrough, and it receives the
|
|
763
|
+
// ciphertext like any other third party.
|
|
764
|
+
//
|
|
765
|
+
// Combo children run this too. Each child carries its own `structuredClone` of the body
|
|
766
|
+
// (`concreteComboRequestBody`) and its own concrete route, so a sibling's repair is invisible
|
|
767
|
+
// here and a target that resolves to a routed Responses wire would otherwise send the
|
|
768
|
+
// ciphertext that the parent's own dispatch no longer does.
|
|
769
|
+
const wireProvider = resolveWireProtocolOverride(
|
|
770
|
+
route.providerName,
|
|
771
|
+
route.modelId,
|
|
772
|
+
route.provider,
|
|
773
|
+
inboundWire,
|
|
774
|
+
);
|
|
775
|
+
if (wireProvider.adapter === "openai-responses" && !isCanonicalOpenAiForwardProvider(wireProvider)) {
|
|
776
|
+
const repaired = stripAgentMessageCiphertextInPlace((body as { input?: unknown } | undefined)?.input);
|
|
777
|
+
if (repaired > 0) {
|
|
778
|
+
console.warn(
|
|
779
|
+
`[opencodex] replaced ciphertext in ${repaired} replayed agent message(s) with an omission marker; the selected provider cannot read native ChatGPT ciphertext`,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
|
|
786
|
+
// safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
|
|
787
|
+
// I/O instead of stripping the id and silently forwarding a context-free delta (#702).
|
|
788
|
+
// Codex recognizes previous_response_not_found on WebSocket errors and reconnects with its
|
|
789
|
+
// full input. A generic invalid_request_error instead terminates the task after cache expiry.
|
|
790
|
+
if (
|
|
791
|
+
hasUnexpandedPreviousResponse
|
|
792
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
793
|
+
) {
|
|
794
|
+
return formatErrorResponse(
|
|
795
|
+
400,
|
|
796
|
+
"previous_response_not_found",
|
|
797
|
+
"OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.",
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (hasUnexpandedPreviousResponse) {
|
|
802
|
+
const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
803
|
+
// Can the DESTINATION see the history this process failed to restore? Only the native
|
|
804
|
+
// Responses passthrough can: it forwards previous_response_id to a backend that stored the
|
|
805
|
+
// chain. Every translated wire rebuilds the conversation from this request's input alone —
|
|
806
|
+
// including the three that look stateful, for the reasons recorded in
|
|
807
|
+
// responses/continuation-ownership.ts — so a replay miss there is not a degraded turn. It is
|
|
808
|
+
// the entire conversation deleted, with one user line left in its place and nothing in the
|
|
809
|
+
// response saying so. Refuse before auth or upstream I/O and let the client resend.
|
|
810
|
+
const continuationWire = resolvedAdapterWire(continuationProvider.adapter);
|
|
811
|
+
const upstreamOwnsOmittedHistory = continuationWire === "openai-responses"
|
|
812
|
+
// Stateless destinations cannot resolve the omitted prefix. Stateful destinations may,
|
|
813
|
+
// but a lowered custom result still needs its call to recover the original wire type.
|
|
814
|
+
// Native function/custom continuations without lowering keep their upstream-owned state.
|
|
815
|
+
? !(continuationProvider.statelessResponses === true
|
|
816
|
+
|| hasUnmappedRoutedCustomToolOutput(parsed._rawBody, continuationProvider.supportsResponsesCustomTools))
|
|
817
|
+
// An unknown adapter is left to the resolution error it already raises below.
|
|
818
|
+
: continuationWire === undefined || PROVIDER_OWNED_CONTINUATION_WIRES.has(continuationWire);
|
|
819
|
+
if (!upstreamOwnsOmittedHistory) {
|
|
820
|
+
return formatErrorResponse(
|
|
821
|
+
400,
|
|
822
|
+
"previous_response_not_found",
|
|
823
|
+
"Routed continuation requires unavailable local history; resend the full conversation without previous_response_id.",
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Captured before normalization: whether the CLIENT asked for SSE. The
|
|
829
|
+
// transport-neutral upstream-streaming policy below may force a bounded JSON
|
|
830
|
+
// upstream for reliability (#875); the answer must then be reframed to SSE
|
|
831
|
+
// for streaming clients.
|
|
832
|
+
const clientRequestedStream = parsed.stream;
|
|
833
|
+
await applyFinalRouteRequestNormalization({
|
|
834
|
+
parsed,
|
|
835
|
+
route,
|
|
836
|
+
config,
|
|
837
|
+
req,
|
|
838
|
+
logCtx,
|
|
839
|
+
inboundWire,
|
|
840
|
+
inboundTransport: options.inboundTransport,
|
|
841
|
+
claudeGoAffinity: options.claudeGoAffinity,
|
|
842
|
+
});
|
|
843
|
+
// Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
|
|
844
|
+
// the normal post-resolution provider label is assigned.
|
|
845
|
+
if (route.codexAccountNamespace) {
|
|
846
|
+
logCtx.provider = `${route.providerName}-${route.codexAccountNamespace}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
850
|
+
// Resolve aliases/combo children before refusing helpers; do not spend main auth or host budget.
|
|
851
|
+
if (isCanonicalOpenAiForwardProvider(route.provider)
|
|
852
|
+
&& isCodexReserveHelperUnsupported(options.codexAuthPolicy ?? config, route.modelId,
|
|
853
|
+
options.admission, options.visionDescribeTerminal === true)) {
|
|
854
|
+
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE);
|
|
855
|
+
}
|
|
856
|
+
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
857
|
+
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
858
|
+
//
|
|
859
|
+
// Compaction turns are exempt: Codex sends compaction_trigger BECAUSE context is full, so
|
|
860
|
+
// refusing the turn that shrinks the context would deadlock the client against the very
|
|
861
|
+
// limit this gate reports — it would be told to compact and then denied the compaction.
|
|
862
|
+
if (parsed._compactionRequest !== true) {
|
|
863
|
+
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId, nativeContextLimits(config));
|
|
864
|
+
if (!inputAdmission.admitted) {
|
|
865
|
+
// #1524: this is a LOCAL preflight refusal, not an upstream verdict. A policy or combo
|
|
866
|
+
// fallback must be able to skip this candidate and try one whose context window fits,
|
|
867
|
+
// instead of treating the first incompatible candidate as the end of the chain. The
|
|
868
|
+
// distinct code is what lets the fallback layer tell the two apart -- an upstream
|
|
869
|
+
// `context_length_exceeded` still stops, because retrying it elsewhere is guesswork.
|
|
870
|
+
if (clientRequestedStream && !options.comboAttempt) {
|
|
871
|
+
return streamingContextOverflowResponse(
|
|
872
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
873
|
+
translatorBudget,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return formatErrorResponse(
|
|
877
|
+
413,
|
|
878
|
+
"input_admission_refused",
|
|
879
|
+
`Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
|
|
880
|
+
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
|
|
881
|
+
+ `model with a larger context window.`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
|
|
886
|
+
if (preAuthHostKey) {
|
|
887
|
+
const admission = acquireUpstreamHostAdmission(
|
|
888
|
+
preAuthHostKey,
|
|
889
|
+
config.upstreamHostCircuitThreshold,
|
|
890
|
+
);
|
|
891
|
+
if (admission.kind === "blocked") {
|
|
892
|
+
return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
|
|
893
|
+
}
|
|
894
|
+
admissionState.pendingHostAdmissionLease = admission.lease;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
let substituteMainCredential = false;
|
|
898
|
+
let callerAuthHeaders: Headers;
|
|
899
|
+
{
|
|
900
|
+
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options, credentialDomainWasRewritten);
|
|
901
|
+
if (!finalAuth.ok) return finalAuth.response;
|
|
902
|
+
admissionState.authCtx = finalAuth.authCtx;
|
|
903
|
+
selectedForwardHeaders = withClaudeNativeSession(finalAuth.headers, route.provider, options.claudeNativeSessionId);
|
|
904
|
+
callerAuthHeaders = withClaudeNativeSession(finalAuth.callerAuthHeaders, route.provider, options.claudeNativeSessionId);
|
|
905
|
+
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
|
|
909
|
+
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
910
|
+
logCtx.provider = route.codexAccountNamespace
|
|
911
|
+
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
912
|
+
: formatCodexProviderForLog(route.providerName, codexLogAccountId(admissionState.authCtx), config);
|
|
913
|
+
logCtx.accountLogLabel = codexAuthContextLogLabel(admissionState.authCtx, config);
|
|
914
|
+
// A move is the expensive event: it discards the prefix warmed on the previous account. Record
|
|
915
|
+
// it as an event with its cause, so the operator reads it off one line instead of inferring it
|
|
916
|
+
// from account labels across many (#4546).
|
|
917
|
+
if (admissionState.authCtx.kind === "pool" && admissionState.authCtx.affinityDecision) {
|
|
918
|
+
logCtx.affinity = admissionState.authCtx.affinityDecision.move;
|
|
919
|
+
logCtx.affinityReason = admissionState.authCtx.affinityDecision.reason;
|
|
920
|
+
}
|
|
921
|
+
{
|
|
922
|
+
const binding = conversationStateBindingFromAuth(admissionState.authCtx, poolAffinityKey);
|
|
923
|
+
if (binding) {
|
|
924
|
+
applyAccountChangeConversationStateScrub({
|
|
925
|
+
body: parsed._rawBody,
|
|
926
|
+
parsed,
|
|
927
|
+
bindingKey: binding.bindingKey,
|
|
928
|
+
servingAccountId: binding.accountId,
|
|
929
|
+
logCtx,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Seed an account-derived scope before final adapter binding. Cursor never treats it as
|
|
934
|
+
// authoritative: bindRouteReasoningReplayScope replaces it with the exact route owner or a
|
|
935
|
+
// per-request fail-closed sentinel after the final provider and credential are known.
|
|
936
|
+
const identityScope = codexLogAccountId(admissionState.authCtx);
|
|
937
|
+
if (identityScope) parsed._cursorIdentityScope = identityScope;
|
|
938
|
+
subagentFallbackAccountId = admissionState.authCtx.kind === "pool" || admissionState.authCtx.kind === "main-pool"
|
|
939
|
+
? admissionState.authCtx.accountId
|
|
940
|
+
: config.activeCodexAccountId ?? null;
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
inboundWire,
|
|
944
|
+
translatorBudget,
|
|
945
|
+
parsed,
|
|
946
|
+
toolBridgeMaps,
|
|
947
|
+
responseStateOptions,
|
|
948
|
+
rememberKiroDeliveredFinalAnswer,
|
|
949
|
+
route,
|
|
950
|
+
get selectedForwardHeaders(): typeof selectedForwardHeaders {
|
|
951
|
+
return selectedForwardHeaders;
|
|
952
|
+
},
|
|
953
|
+
set selectedForwardHeaders(value: typeof selectedForwardHeaders) {
|
|
954
|
+
selectedForwardHeaders = value;
|
|
955
|
+
},
|
|
956
|
+
get subagentFallbackAccountId(): typeof subagentFallbackAccountId {
|
|
957
|
+
return subagentFallbackAccountId;
|
|
958
|
+
},
|
|
959
|
+
set subagentFallbackAccountId(value: typeof subagentFallbackAccountId) {
|
|
960
|
+
subagentFallbackAccountId = value;
|
|
961
|
+
},
|
|
962
|
+
subagentQuotaFailureModel,
|
|
963
|
+
poolAffinityKey,
|
|
964
|
+
clientRequestedStream,
|
|
965
|
+
substituteMainCredential,
|
|
966
|
+
callerAuthHeaders,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export type PreparedResponsesRequest = Exclude<Awaited<ReturnType<typeof prepareResponsesRequest>>, Response>;
|