@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,157 @@
1
+ import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
+ import type { PreparedResponsesRequest } from "./request-prepare";
3
+ import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
4
+ import type { OcxProviderContinuationState } from "../../types";
5
+ import { providerContinuationPayload } from "./core-replay";
6
+ import { mergeProviderContinuationPayload } from "../../responses/provider-continuation";
7
+ import { commitReasoningReplayServingIdentity } from "../../responses/reasoning-replay-cache";
8
+ import { rememberServingConversationStateIssuer } from "./account-change-state";
9
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
10
+ import { contextRelayActivated } from "../../codex/context-compat";
11
+ import { recordContextSessionOwner } from "../../codex/context-owner";
12
+ import { resolveContextPrincipal } from "../auth-cors";
13
+ import { COMPACT_PROMPT } from "../../responses/compaction";
14
+ import type { RoutedNamespaceToolAliases } from "../../responses/namespace-tool-compat";
15
+ import type { MuseToolNameAliases } from "../../responses/muse-tool-name-alias";
16
+ import type { AdapterRequest } from "../../adapters/base";
17
+
18
+ /** Owns completion callbacks, replay publication, and live tool aliases. */
19
+ export function createResponsesEffects(
20
+ requestContext: Pick<ResponsesRequestContext, "options" | "req" | "config">,
21
+ admissionState: ResponsesAdmissionState,
22
+ requestState: Pick<
23
+ PreparedResponsesRequest,
24
+ | "parsed"
25
+ | "poolAffinityKey"
26
+ | "route"
27
+ | "substituteMainCredential"
28
+ >,
29
+ sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
30
+ ) {
31
+ const { options, req, config } = requestContext;
32
+ const { parsed, poolAffinityKey, route, substituteMainCredential } = requestState;
33
+ const { routedCompaction } = sidecarState;
34
+
35
+
36
+ const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
37
+ let responseCompletionNotified = false;
38
+ let responseCompletionCancelled = false;
39
+ const cancelResponseCompletion = (): void => { responseCompletionCancelled = true; };
40
+ const notifyResponseComplete = (response: { status?: unknown; model?: unknown }): void => {
41
+ if (responseCompletionNotified || responseCompletionCancelled
42
+ || options.abortSignal?.aborted || req.signal.aborted
43
+ || response.status !== "completed"
44
+ || typeof response.model !== "string" || !response.model.trim()) return;
45
+ responseCompletionNotified = true;
46
+ options.onResponseComplete?.(response.model);
47
+ };
48
+
49
+ const continuationStateForResponse = (
50
+ emitted?: OcxProviderContinuationState,
51
+ ): OcxProviderContinuationState | undefined => {
52
+ const cursorConversationId = parsed._cursorConversationId;
53
+ const inherited = providerContinuationPayload(parsed._providerContinuation);
54
+ const emittedPayload = providerContinuationPayload(emitted);
55
+ if (!emittedPayload && !inherited && !cursorConversationId) return undefined;
56
+ const merged = mergeProviderContinuationPayload(
57
+ inherited ?? {},
58
+ emittedPayload ?? {},
59
+ ) as OcxProviderContinuationState;
60
+ if (cursorConversationId) {
61
+ merged.cursor = { ...(merged.cursor ?? {}), conversationId: cursorConversationId };
62
+ }
63
+ return parsed._providerContinuationOwner
64
+ ? { ...merged, __ocxOwner: { ...parsed._providerContinuationOwner } }
65
+ : merged;
66
+ };
67
+
68
+ // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
69
+ // one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
70
+ // natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
71
+ // sidecar — and the bridge appends the synthetic compaction item (src/responses/compaction.ts).
72
+ // A Responses-shaped wire does not imply support for Codex's private
73
+ // `compaction_trigger` item — only the canonical ChatGPT backend speaks that
74
+ // contract. An API-key gateway would receive the trigger, answer with an ordinary
75
+ // message, and leave Codex fataling on a missing compaction item (#422).
76
+ const commitReasoningReplayServingRoute = (outboundHeaders?: HeadersInit): void => {
77
+ commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
78
+ rememberServingConversationStateIssuer(admissionState.authCtx, poolAffinityKey);
79
+ // History has no model namespace. Record the account that actually accepted this
80
+ // final attempt, after refresh/failover, rather than guessing from mutable affinity.
81
+ // Recording is relay state. With the feature off there is no relay, so building an owner
82
+ // registry for it is out of scope for this request.
83
+ if (outboundHeaders && isCanonicalOpenAiForwardProvider(route.provider) && contextRelayActivated()) {
84
+ recordContextSessionOwner(resolveContextPrincipal(req, config, options.admission), req.headers,
85
+ route.provider.baseUrl, admissionState.authCtx, new Headers(outboundHeaders), substituteMainCredential);
86
+ }
87
+ };
88
+ if (routedCompaction) {
89
+ delete parsed.context.tools;
90
+ delete parsed._webSearch;
91
+ delete parsed.options.toolChoice;
92
+ delete parsed.options.parallelToolCalls;
93
+ // The compaction turn is a plain prose summary; a surviving structured-output format
94
+ // would force schema-constrained JSON into the synthetic compaction item. The flag and
95
+ // the raw `text` control go too: the key-mode openai-responses adapter builds from
96
+ // _rawBody, so a surviving format there would still reach the upstream. (The Kiro
97
+ // guard no longer reads _rawBody.text; it refuses structured output only.)
98
+ delete parsed.options.textFormat;
99
+ delete parsed._structuredOutput;
100
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
101
+ delete (parsed._rawBody as Record<string, unknown>).text;
102
+ }
103
+ parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
104
+ }
105
+
106
+ let routedNamespaceToolAliases: RoutedNamespaceToolAliases = new Map();
107
+ let plaintextV2AgentMessageToolNames: ReadonlySet<string> = new Set();
108
+ let plaintextV2AgentMessageAliasedToolNames: ReadonlySet<string> = new Set();
109
+ let routedMuseToolNameAliases: MuseToolNameAliases = new Map();
110
+ const refreshRequestToolAliases = (builtRequest: AdapterRequest): void => {
111
+ routedNamespaceToolAliases = builtRequest.convertedRoutedNamespaceToolAliases ?? new Map();
112
+ plaintextV2AgentMessageToolNames = builtRequest.plaintextV2AgentMessageToolNames ?? new Set();
113
+ plaintextV2AgentMessageAliasedToolNames = builtRequest.plaintextV2AgentMessageAliasedToolNames ?? new Set();
114
+ routedMuseToolNameAliases = builtRequest.convertedMuseToolNameAliases ?? new Map();
115
+ };
116
+
117
+ return {
118
+ recordTerminalOutcomes,
119
+ get responseCompletionCancelled(): typeof responseCompletionCancelled {
120
+ return responseCompletionCancelled;
121
+ },
122
+ set responseCompletionCancelled(value: typeof responseCompletionCancelled) {
123
+ responseCompletionCancelled = value;
124
+ },
125
+ cancelResponseCompletion,
126
+ notifyResponseComplete,
127
+ continuationStateForResponse,
128
+ commitReasoningReplayServingRoute,
129
+ get routedNamespaceToolAliases(): RoutedNamespaceToolAliases {
130
+ return routedNamespaceToolAliases;
131
+ },
132
+ set routedNamespaceToolAliases(value: RoutedNamespaceToolAliases) {
133
+ routedNamespaceToolAliases = value;
134
+ },
135
+ get plaintextV2AgentMessageToolNames(): ReadonlySet<string> {
136
+ return plaintextV2AgentMessageToolNames;
137
+ },
138
+ set plaintextV2AgentMessageToolNames(value: ReadonlySet<string>) {
139
+ plaintextV2AgentMessageToolNames = value;
140
+ },
141
+ get plaintextV2AgentMessageAliasedToolNames(): ReadonlySet<string> {
142
+ return plaintextV2AgentMessageAliasedToolNames;
143
+ },
144
+ set plaintextV2AgentMessageAliasedToolNames(value: ReadonlySet<string>) {
145
+ plaintextV2AgentMessageAliasedToolNames = value;
146
+ },
147
+ get routedMuseToolNameAliases(): MuseToolNameAliases {
148
+ return routedMuseToolNameAliases;
149
+ },
150
+ set routedMuseToolNameAliases(value: MuseToolNameAliases) {
151
+ routedMuseToolNameAliases = value;
152
+ },
153
+ refreshRequestToolAliases,
154
+ };
155
+ }
156
+
157
+ export type ResponsesEffects = Exclude<ReturnType<typeof createResponsesEffects>, Response>;
@@ -0,0 +1,476 @@
1
+ import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
+ import type { PreparedResponsesRequest } from "./request-prepare";
3
+ import type { ResponsesTransport } from "./request-transport";
4
+ import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
5
+ import type { ResponsesEffects } from "./response-effects";
6
+ import type { ResponsesSendBudget } from "./request-send-budget";
7
+ import type { ResponsesCompletionPolicy } from "./completion-policy";
8
+ import { linkAbortSignal, runTurnAdapterSseResponses } from "./core-lifetime";
9
+ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
10
+ import {
11
+ bindRouteReasoningReplayScope,
12
+ adapterNeedsForcedContinuation,
13
+ adapterResponseReachedServingTerminal,
14
+ } from "./core-replay";
15
+ import { sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
16
+ import { waitForProviderRequestSlot, RequestPacingQueueOverloadError } from "../../providers/request-pacing";
17
+ import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
18
+ import type { AttemptRecoveryKind } from "../../usage/log";
19
+ import { providerFetch } from "./fetch-helpers";
20
+ import { normalizeLogConversationId } from "../request-log-conversation";
21
+ import type { AdapterEvent, OcxProviderContinuationState } from "../../types";
22
+ import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
23
+ import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
24
+ import {
25
+ GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
26
+ isGenericOAuthFailoverEnabled,
27
+ rotateGenericOAuthAccountOn429,
28
+ failoverAccountSnapshot,
29
+ } from "../../oauth/generic-account-failover";
30
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
31
+ import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
32
+ import { redactSecretString } from "../../lib/redact";
33
+ import {
34
+ guardEmptyCompletionEventStream,
35
+ observeEmptyCompletion,
36
+ emptyCompletionNotice,
37
+ } from "./empty-completion-guard";
38
+ import { rememberResponseState } from "../../responses/state";
39
+ import { trackStreamLifetime } from "../lifecycle";
40
+ import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
41
+
42
+ /** One responsibility of the Responses request pipeline; state owners are explicit. */
43
+ export async function executeResponsesRunTurn(
44
+ requestContext: Pick<ResponsesRequestContext, "options" | "logCtx" | "config">,
45
+ admissionState: ResponsesAdmissionState,
46
+ requestState: Pick<
47
+ PreparedResponsesRequest,
48
+ | "parsed"
49
+ | "route"
50
+ | "selectedForwardHeaders"
51
+ | "translatorBudget"
52
+ | "inboundWire"
53
+ | "toolBridgeMaps"
54
+ | "rememberKiroDeliveredFinalAnswer"
55
+ | "responseStateOptions"
56
+ >,
57
+ transportState: Pick<
58
+ ResponsesTransport,
59
+ | "selectionIsCurrent"
60
+ | "adapterBindings"
61
+ | "runTurnAdapter"
62
+ | "refreshRunTurnAdapter"
63
+ | "replayOAuthCredentialSnapshot"
64
+ | "genericFailoverAccountId"
65
+ | "genericFailovers"
66
+ | "applyFailoverSnapshot"
67
+ | "resolveSelectionAdapter"
68
+ | "adapter"
69
+ | "noteRoutedAttemptSend"
70
+ | "bindKeyUsageFromBridge"
71
+ >,
72
+ sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
73
+ responseEffects: Pick<
74
+ ResponsesEffects,
75
+ | "cancelResponseCompletion"
76
+ | "commitReasoningReplayServingRoute"
77
+ | "continuationStateForResponse"
78
+ | "notifyResponseComplete"
79
+ >,
80
+ sendBudgetState: Pick<
81
+ ResponsesSendBudget,
82
+ "adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
83
+ >,
84
+ completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
85
+ ): Promise<Response> {
86
+ const { options, logCtx, config } = requestContext;
87
+ const {
88
+ selectionIsCurrent,
89
+ adapterBindings,
90
+ refreshRunTurnAdapter,
91
+ applyFailoverSnapshot,
92
+ resolveSelectionAdapter,
93
+ } = transportState;
94
+ const {
95
+ parsed,
96
+ route,
97
+ translatorBudget,
98
+ inboundWire,
99
+ toolBridgeMaps,
100
+ rememberKiroDeliveredFinalAnswer,
101
+ responseStateOptions,
102
+ } = requestState;
103
+ const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
104
+ const { emptyCompletionGuardEnabled } = completionPolicy;
105
+ const {
106
+ cancelResponseCompletion,
107
+ commitReasoningReplayServingRoute,
108
+ continuationStateForResponse,
109
+ notifyResponseComplete,
110
+ } = responseEffects;
111
+ const { routedCompaction } = sidecarState;
112
+
113
+ const runTurnAbort = new AbortController();
114
+ const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
115
+ const queue = createAdapterEventQueue({
116
+ onBacklogExceeded: () => runTurnAbort.abort(),
117
+ });
118
+ const refreshRunTurnSelection = async (): Promise<void> => {
119
+ if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return;
120
+ await refreshRunTurnAdapter(parsed);
121
+ bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
122
+ adapterName: transportState.runTurnAdapter.name, oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot });
123
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.runTurnAdapter.name, logCtx.accountLogLabel);
124
+ };
125
+ // Initial admission must settle before the streaming Response commits HTTP 200.
126
+ // Let the outer Responses facade preserve the local retryable-429 contract.
127
+ try {
128
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
129
+ } catch (error) {
130
+ cleanupRunTurnAbort();
131
+ queue.close();
132
+ throw error;
133
+ }
134
+ // One attempt of the runTurn transport, against an explicit queue. The
135
+ // empty-completion guard re-invokes the IDENTICAL turn (same parsed request,
136
+ // same forwarded headers, same abort signal) through a fresh queue, so the
137
+ // attempt body must not capture the first queue. Each attempt consumes its
138
+ // own provider pacing slot (#1584): retries are paced like first attempts.
139
+ const runTurnAttempt = async (
140
+ targetQueue: AdapterEventQueue,
141
+ recovery?: AttemptRecoveryKind,
142
+ pacingSlotAcquired = false,
143
+ ): Promise<void> => {
144
+ try {
145
+ if (!pacingSlotAcquired) {
146
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
147
+ }
148
+ await refreshRunTurnSelection();
149
+ transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
150
+ const runTurnProviderFetch = providerFetch(
151
+ route.provider,
152
+ options.codexWsRuntimeIdentity,
153
+ {
154
+ providerName: route.providerName,
155
+ modelId: route.modelId,
156
+ // runTurnAttempt acquired this logical turn's first physical-request slot above.
157
+ // Cursor HTTP/1.1 consumes it for RunSSE; every BidiAppend and redial then waits on
158
+ // the same provider queue through this stateful wrapper.
159
+ pacingSlotAcquired: true,
160
+ },
161
+ );
162
+ await transportState.runTurnAdapter.runTurn?.(
163
+ parsed,
164
+ {
165
+ headers: requestState.selectedForwardHeaders,
166
+ abortSignal: runTurnAbort.signal,
167
+ translatorBudget,
168
+ providerFetch: runTurnProviderFetch,
169
+ // The only way the request budget reaches a transport the adapter owns. Without it
170
+ // a Cursor turn's inner ladder was three physical sends the cap read as one.
171
+ ...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
172
+ },
173
+ targetQueue.push,
174
+ );
175
+ } catch (err) {
176
+ targetQueue.push(err instanceof RequestPacingQueueOverloadError
177
+ ? {
178
+ type: "error",
179
+ status: 429,
180
+ errorType: "rate_limit_error",
181
+ retryable: true,
182
+ message: err.message,
183
+ }
184
+ : err instanceof SendBudgetExhaustedError
185
+ // A structured terminal, not a bare message. The turn is already committed to an
186
+ // SSE response by the time most of these arrive, so the only way to carry "this
187
+ // proxy refused" to the client is on the event itself -- an unstructured message
188
+ // is inferred back to 502, which the Codex client retries.
189
+ ? {
190
+ type: "error",
191
+ status: 429,
192
+ errorType: "rate_limit_error",
193
+ code: SEND_BUDGET_EXHAUSTED_CODE,
194
+ message: err.message,
195
+ }
196
+ : {
197
+ type: "error",
198
+ message: err instanceof Error ? err.message : String(err),
199
+ });
200
+ } finally {
201
+ // Cursor assigns a stable conversation id inside runTurn on the first headerless
202
+ // turn; backfill so Logs can filter/total that opening request (#330 / #522).
203
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
204
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
205
+ }
206
+ targetQueue.close();
207
+ }
208
+ };
209
+ const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
210
+ const rotateRunTurnAdapterOnPreflight429 = async (
211
+ error: Extract<AdapterEvent, { type: "error" }>,
212
+ ): Promise<boolean> => {
213
+ // Our own refusal wears a 429 now, and rotating on it would record a cooldown against an
214
+ // account that never rate-limited anything -- a fake quota signal that outlives the
215
+ // request and misroutes later ones. The passthrough path has never had this problem
216
+ // because it answers before any rotation arm is reached.
217
+ if (error.code === SEND_BUDGET_EXHAUSTED_CODE) return false;
218
+ const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
219
+ if (
220
+ status !== 429
221
+ || !transportState.genericFailoverAccountId
222
+ || transportState.genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
223
+ || !isGenericOAuthFailoverEnabled(config, route.providerName)
224
+ ) return false;
225
+ // Intersection with the request's shared budget: the roster bound above answers "may this
226
+ // credential set rotate again", this answers "may this request send again at all". The
227
+ // replayed turn is dispatched by runTurnAttempt and never reaches `onSendsConsumed`, so
228
+ // this reservation is the charge. Refusing returns false, which leaves the preflight 429
229
+ // to reach the client exactly as the adapter produced it.
230
+ const hop = reserveCredentialHop(
231
+ "auth-recovery",
232
+ `${route.providerName}|${route.modelId}|runturn-oauth-429`,
233
+ );
234
+ if (!hop.allowed) return false;
235
+ const nextAccountId = rotateGenericOAuthAccountOn429(
236
+ config,
237
+ route.providerName,
238
+ transportState.genericFailoverAccountId,
239
+ null,
240
+ );
241
+ if (!nextAccountId) {
242
+ hop.permit?.release();
243
+ return false;
244
+ }
245
+ try {
246
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
247
+ transportState.genericFailovers += 1;
248
+ if (!await applyFailoverSnapshot(snapshot)) {
249
+ hop.permit?.release();
250
+ return false;
251
+ }
252
+ // A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
253
+ // client-visible bytes, so replay is safe, but carrying its account identity into the next
254
+ // account would not be. Let the rotated adapter derive a fresh identity and conversation.
255
+ parsed._cursorIdentityScope = undefined;
256
+ parsed._cursorConversationId = undefined;
257
+ if (parsed._providerContinuation?.cursor) {
258
+ const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
259
+ parsed._providerContinuation = otherProviderState;
260
+ }
261
+ const rotatedProvider = resolveWireProtocolOverride(
262
+ route.providerName,
263
+ route.modelId,
264
+ route.provider,
265
+ inboundWire,
266
+ );
267
+ const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
268
+ if (!rotatedAdapter.runTurn) {
269
+ hop.permit?.release();
270
+ return false;
271
+ }
272
+ transportState.runTurnAdapter = rotatedAdapter;
273
+ bindRouteReasoningReplayScope({
274
+ parsed,
275
+ providerName: route.providerName,
276
+ provider: rotatedProvider,
277
+ adapterName: rotatedAdapter.name,
278
+ oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
279
+ codexAuthContext: admissionState.authCtx,
280
+ forwardHeaders: requestState.selectedForwardHeaders,
281
+ });
282
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
283
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
284
+ // The caller replays the turn on this rotation, and a runTurn adapter dispatches through
285
+ // its own reservation ladder -- Cursor reserves once per physical send. Confirming here
286
+ // would leave that ladder to charge the same replay a second time (#4709), so hand the
287
+ // reservation down and let the send that actually happens spend it.
288
+ sendBudgetState.pendingHopPermit = hop.permit;
289
+ return true;
290
+ } catch {
291
+ hop.permit?.release();
292
+ return false;
293
+ }
294
+ };
295
+ const preflightRunTurnFailover = async (
296
+ firstSource: AsyncIterable<AdapterEvent>,
297
+ ): Promise<AsyncIterable<AdapterEvent>> => {
298
+ let source = firstSource;
299
+ try {
300
+ while (true) {
301
+ const preflight = await preflightAdapterEvents(source);
302
+ if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
303
+ return preflight.stream;
304
+ }
305
+ const retryQueue = createAdapterEventQueue({
306
+ onBacklogExceeded: () => runTurnAbort.abort(),
307
+ });
308
+ void runTurnAttempt(retryQueue, "oauth-account-429");
309
+ source = retryQueue.stream();
310
+ }
311
+ } finally {
312
+ // A handed-down hop reservation belongs to the replay this loop dispatched, and the
313
+ // loop only leaves after that replay's first event has arrived -- so the adapter has
314
+ // already reserved if it was ever going to. Dropping the reference here keeps an
315
+ // adapter that reserves nothing from leaving a free send for an unrelated later leg.
316
+ sendBudgetState.pendingHopPermit = undefined;
317
+ }
318
+ };
319
+ // The empty-completion retry re-runs the turn against a fresh queue: the
320
+ // first queue is closed once its attempt settles, and pushing into it after
321
+ // close is a silent no-op.
322
+ const runTurnRetrySource = (): AsyncIterable<AdapterEvent> => {
323
+ const retryQueue = createAdapterEventQueue({
324
+ onBacklogExceeded: () => runTurnAbort.abort(),
325
+ });
326
+ void runTurnAttempt(retryQueue, "empty-completion");
327
+ return retryQueue.stream();
328
+ };
329
+
330
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
331
+ if (parsed.stream) {
332
+ void runTurn();
333
+ let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
334
+ if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
335
+ // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
336
+ // replayed transparently; after any output reaches the bridge, a later error stays terminal.
337
+ eventSource = await preflightRunTurnFailover(eventSource);
338
+ }
339
+ if (options.comboAttempt) {
340
+ const preflight = await preflightAdapterEvents(eventSource);
341
+ if (preflight.error || preflight.empty) {
342
+ runTurnAbort.abort();
343
+ queue.close();
344
+ const message = preflight.error?.message ?? "Adapter ended before producing a response";
345
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
346
+ }
347
+ eventSource = preflight.stream;
348
+ }
349
+ const guardedSource = emptyCompletionGuardEnabled
350
+ ? guardEmptyCompletionEventStream({
351
+ firstEvents: eventSource,
352
+ // Identical-turn retry: same parsed request, same headers, same
353
+ // signal — run the adapter transport again against a fresh queue.
354
+ continuation: runTurnRetrySource,
355
+ })
356
+ // Guard off (the default): leave the stream alone, but record that the turn ended
357
+ // empty so the user has something to correlate instead of an unexplained blank
358
+ // result (#2472). Retrying by default would re-send a turn that may already have had
359
+ // billable side effects, so the honest default is observability, not recovery.
360
+ : observeEmptyCompletion(eventSource, () => {
361
+ console.warn(emptyCompletionNotice(route.providerName, route.modelId));
362
+ });
363
+ const sseStream = bridgeToResponsesSSE(
364
+ guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
365
+ () => {
366
+ cancelResponseCompletion();
367
+ runTurnAbort.abort();
368
+ queue.close();
369
+ }, 2_000,
370
+ {
371
+ translatorBudget,
372
+ replayCacheScope: parsed._reasoningReplayScope,
373
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
374
+ stallTimeoutSec: config.stallTimeoutSec,
375
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
376
+ declaredToolNames,
377
+ enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
378
+ toolParameterSchemas,
379
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
380
+ ...(routedCompaction ? { compaction: true } : {}),
381
+ // grok-build's strict decoder dies on the typed response.heartbeat frame; its
382
+ // eventsource layer tolerates comment keep-alives. Codex needs the opposite.
383
+ ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
384
+ onUsage: usage => {
385
+ // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
386
+ // zero-default detail objects, so provenance must come from here (cache_detail_missing).
387
+ transportState.bindKeyUsageFromBridge(usage);
388
+ },
389
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
390
+ commitReasoningReplayServingRoute();
391
+ rememberKiroDeliveredFinalAnswer(transportState.adapter.name, response);
392
+ if (!routedCompaction) {
393
+ rememberResponseState(
394
+ parsed._rawBody,
395
+ response,
396
+ continuationStateForResponse(providerState),
397
+ responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
398
+ );
399
+ }
400
+ notifyResponseComplete(response);
401
+ },
402
+ },
403
+ );
404
+ const bridgeTurnAc = new AbortController();
405
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
406
+ const response = new Response(trackedSse, {
407
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
408
+ });
409
+ runTurnAdapterSseResponses.add(response);
410
+ return response;
411
+ }
412
+
413
+ await runTurn();
414
+ const firstAttemptEvents = await queue.collect();
415
+ let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
416
+ if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
417
+ runTurnEvents = [];
418
+ for await (const event of await preflightRunTurnFailover(
419
+ (async function* () { yield* firstAttemptEvents; })(),
420
+ )) runTurnEvents.push(event);
421
+ }
422
+ let events: AdapterEvent[];
423
+ if (emptyCompletionGuardEnabled) {
424
+ events = [];
425
+ for await (const event of guardEmptyCompletionEventStream({
426
+ firstEvents: (async function* () { yield* runTurnEvents; })(),
427
+ continuation: runTurnRetrySource,
428
+ })) events.push(event);
429
+ } else {
430
+ events = runTurnEvents;
431
+ }
432
+ if (options.comboAttempt) {
433
+ const firstMeaningful = events.find(event => event.type !== "heartbeat");
434
+ if (!firstMeaningful || firstMeaningful.type === "error") {
435
+ const message = firstMeaningful?.type === "error"
436
+ ? firstMeaningful.message
437
+ : "Adapter ended before producing a response";
438
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
439
+ }
440
+ }
441
+ let providerState: OcxProviderContinuationState | undefined;
442
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
443
+ translatorBudget,
444
+ replayCacheScope: parsed._reasoningReplayScope,
445
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
446
+ toolNsMap,
447
+ declaredToolNames,
448
+ enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
449
+ toolParameterSchemas,
450
+ freeformToolNames,
451
+ toolSearchToolNames,
452
+ ...(routedCompaction ? { compaction: true } : {}),
453
+ onProviderState: state => { providerState = state; },
454
+ onUsage: usage => {
455
+ transportState.bindKeyUsageFromBridge(usage);
456
+ },
457
+ });
458
+ if (!routedCompaction) {
459
+ rememberKiroDeliveredFinalAnswer(transportState.adapter.name, json);
460
+ rememberResponseState(
461
+ parsed._rawBody,
462
+ json,
463
+ continuationStateForResponse(providerState),
464
+ responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
465
+ );
466
+ }
467
+ // #1926 gap 2: the buffered path queued its signature persists inside
468
+ // buildResponseJSON; bound the durability window before the JSON becomes
469
+ // externally visible.
470
+ await awaitThoughtSignatureDurability();
471
+ if (adapterResponseReachedServingTerminal(events, json)) {
472
+ commitReasoningReplayServingRoute();
473
+ }
474
+ notifyResponseComplete(json);
475
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
476
+ }