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