@bitkyc08/opencodex 2.55.0 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -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,448 @@
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, noteAttemptSend, 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 } from "../../lib/errors";
23
+ import {
24
+ GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
25
+ isGenericOAuthFailoverEnabled,
26
+ rotateGenericOAuthAccountOn429,
27
+ failoverAccountSnapshot,
28
+ } from "../../oauth/generic-account-failover";
29
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
30
+ import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
31
+ import { redactSecretString } from "../../lib/redact";
32
+ import {
33
+ guardEmptyCompletionEventStream,
34
+ observeEmptyCompletion,
35
+ emptyCompletionNotice,
36
+ } from "./empty-completion-guard";
37
+ import { rememberResponseState } from "../../responses/state";
38
+ import { trackStreamLifetime } from "../lifecycle";
39
+ import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
40
+
41
+ /** One responsibility of the Responses request pipeline; state owners are explicit. */
42
+ export async function executeResponsesRunTurn(
43
+ requestContext: Pick<ResponsesRequestContext, "options" | "logCtx" | "config">,
44
+ admissionState: ResponsesAdmissionState,
45
+ requestState: Pick<
46
+ PreparedResponsesRequest,
47
+ | "parsed"
48
+ | "route"
49
+ | "selectedForwardHeaders"
50
+ | "translatorBudget"
51
+ | "inboundWire"
52
+ | "toolBridgeMaps"
53
+ | "rememberKiroDeliveredFinalAnswer"
54
+ | "responseStateOptions"
55
+ >,
56
+ transportState: Pick<
57
+ ResponsesTransport,
58
+ | "selectionIsCurrent"
59
+ | "adapterBindings"
60
+ | "runTurnAdapter"
61
+ | "refreshRunTurnAdapter"
62
+ | "replayOAuthCredentialSnapshot"
63
+ | "genericFailoverAccountId"
64
+ | "genericFailovers"
65
+ | "applyFailoverSnapshot"
66
+ | "resolveSelectionAdapter"
67
+ | "adapter"
68
+ >,
69
+ sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
70
+ responseEffects: Pick<
71
+ ResponsesEffects,
72
+ | "cancelResponseCompletion"
73
+ | "commitReasoningReplayServingRoute"
74
+ | "continuationStateForResponse"
75
+ | "notifyResponseComplete"
76
+ >,
77
+ sendBudgetState: Pick<ResponsesSendBudget, "adapterSendBudget" | "reserveCredentialHop">,
78
+ completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
79
+ ): Promise<Response> {
80
+ const { options, logCtx, config } = requestContext;
81
+ const {
82
+ selectionIsCurrent,
83
+ adapterBindings,
84
+ refreshRunTurnAdapter,
85
+ applyFailoverSnapshot,
86
+ resolveSelectionAdapter,
87
+ } = transportState;
88
+ const {
89
+ parsed,
90
+ route,
91
+ translatorBudget,
92
+ inboundWire,
93
+ toolBridgeMaps,
94
+ rememberKiroDeliveredFinalAnswer,
95
+ responseStateOptions,
96
+ } = requestState;
97
+ const { adapterSendBudget, reserveCredentialHop } = sendBudgetState;
98
+ const { emptyCompletionGuardEnabled } = completionPolicy;
99
+ const {
100
+ cancelResponseCompletion,
101
+ commitReasoningReplayServingRoute,
102
+ continuationStateForResponse,
103
+ notifyResponseComplete,
104
+ } = responseEffects;
105
+ const { routedCompaction } = sidecarState;
106
+
107
+ const runTurnAbort = new AbortController();
108
+ const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
109
+ const queue = createAdapterEventQueue({
110
+ onBacklogExceeded: () => runTurnAbort.abort(),
111
+ });
112
+ const refreshRunTurnSelection = async (): Promise<void> => {
113
+ if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return;
114
+ await refreshRunTurnAdapter(parsed);
115
+ bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
116
+ adapterName: transportState.runTurnAdapter.name, oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot });
117
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.runTurnAdapter.name, logCtx.accountLogLabel);
118
+ };
119
+ // Initial admission must settle before the streaming Response commits HTTP 200.
120
+ // Let the outer Responses facade preserve the local retryable-429 contract.
121
+ try {
122
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
123
+ } catch (error) {
124
+ cleanupRunTurnAbort();
125
+ queue.close();
126
+ throw error;
127
+ }
128
+ // One attempt of the runTurn transport, against an explicit queue. The
129
+ // empty-completion guard re-invokes the IDENTICAL turn (same parsed request,
130
+ // same forwarded headers, same abort signal) through a fresh queue, so the
131
+ // attempt body must not capture the first queue. Each attempt consumes its
132
+ // own provider pacing slot (#1584): retries are paced like first attempts.
133
+ const runTurnAttempt = async (
134
+ targetQueue: AdapterEventQueue,
135
+ recovery?: AttemptRecoveryKind,
136
+ pacingSlotAcquired = false,
137
+ ): Promise<void> => {
138
+ try {
139
+ if (!pacingSlotAcquired) {
140
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
141
+ }
142
+ await refreshRunTurnSelection();
143
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
144
+ const runTurnProviderFetch = providerFetch(
145
+ route.provider,
146
+ options.codexWsRuntimeIdentity,
147
+ {
148
+ providerName: route.providerName,
149
+ modelId: route.modelId,
150
+ // runTurnAttempt acquired this logical turn's first physical-request slot above.
151
+ // Cursor HTTP/1.1 consumes it for RunSSE; every BidiAppend and redial then waits on
152
+ // the same provider queue through this stateful wrapper.
153
+ pacingSlotAcquired: true,
154
+ },
155
+ );
156
+ await transportState.runTurnAdapter.runTurn?.(
157
+ parsed,
158
+ {
159
+ headers: requestState.selectedForwardHeaders,
160
+ abortSignal: runTurnAbort.signal,
161
+ translatorBudget,
162
+ providerFetch: runTurnProviderFetch,
163
+ // The only way the request budget reaches a transport the adapter owns. Without it
164
+ // a Cursor turn's inner ladder was three physical sends the cap read as one.
165
+ ...(adapterSendBudget ? { sendBudget: adapterSendBudget } : {}),
166
+ },
167
+ targetQueue.push,
168
+ );
169
+ } catch (err) {
170
+ targetQueue.push(err instanceof RequestPacingQueueOverloadError
171
+ ? {
172
+ type: "error",
173
+ status: 429,
174
+ errorType: "rate_limit_error",
175
+ retryable: true,
176
+ message: err.message,
177
+ }
178
+ : {
179
+ type: "error",
180
+ message: err instanceof Error ? err.message : String(err),
181
+ });
182
+ } finally {
183
+ // Cursor assigns a stable conversation id inside runTurn on the first headerless
184
+ // turn; backfill so Logs can filter/total that opening request (#330 / #522).
185
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
186
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
187
+ }
188
+ targetQueue.close();
189
+ }
190
+ };
191
+ const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
192
+ const rotateRunTurnAdapterOnPreflight429 = async (
193
+ error: Extract<AdapterEvent, { type: "error" }>,
194
+ ): Promise<boolean> => {
195
+ const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
196
+ if (
197
+ status !== 429
198
+ || !transportState.genericFailoverAccountId
199
+ || transportState.genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
200
+ || !isGenericOAuthFailoverEnabled(config, route.providerName)
201
+ ) return false;
202
+ // Intersection with the request's shared budget: the roster bound above answers "may this
203
+ // credential set rotate again", this answers "may this request send again at all". The
204
+ // replayed turn is dispatched by runTurnAttempt and never reaches `onSendsConsumed`, so
205
+ // this reservation is the charge. Refusing returns false, which leaves the preflight 429
206
+ // to reach the client exactly as the adapter produced it.
207
+ const hop = reserveCredentialHop(
208
+ "auth-recovery",
209
+ `${route.providerName}|${route.modelId}|runturn-oauth-429`,
210
+ );
211
+ if (!hop.allowed) return false;
212
+ const nextAccountId = rotateGenericOAuthAccountOn429(
213
+ config,
214
+ route.providerName,
215
+ transportState.genericFailoverAccountId,
216
+ null,
217
+ );
218
+ if (!nextAccountId) {
219
+ hop.permit?.release();
220
+ return false;
221
+ }
222
+ try {
223
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
224
+ transportState.genericFailovers += 1;
225
+ if (!await applyFailoverSnapshot(snapshot)) {
226
+ hop.permit?.release();
227
+ return false;
228
+ }
229
+ // A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
230
+ // client-visible bytes, so replay is safe, but carrying its account identity into the next
231
+ // account would not be. Let the rotated adapter derive a fresh identity and conversation.
232
+ parsed._cursorIdentityScope = undefined;
233
+ parsed._cursorConversationId = undefined;
234
+ if (parsed._providerContinuation?.cursor) {
235
+ const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
236
+ parsed._providerContinuation = otherProviderState;
237
+ }
238
+ const rotatedProvider = resolveWireProtocolOverride(
239
+ route.providerName,
240
+ route.modelId,
241
+ route.provider,
242
+ inboundWire,
243
+ );
244
+ const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
245
+ if (!rotatedAdapter.runTurn) {
246
+ hop.permit?.release();
247
+ return false;
248
+ }
249
+ transportState.runTurnAdapter = rotatedAdapter;
250
+ bindRouteReasoningReplayScope({
251
+ parsed,
252
+ providerName: route.providerName,
253
+ provider: rotatedProvider,
254
+ adapterName: rotatedAdapter.name,
255
+ oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
256
+ codexAuthContext: admissionState.authCtx,
257
+ forwardHeaders: requestState.selectedForwardHeaders,
258
+ });
259
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
260
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
261
+ // The caller replays the turn on this rotation, so the reservation is now confirmed.
262
+ hop.permit?.use();
263
+ return true;
264
+ } catch {
265
+ hop.permit?.release();
266
+ return false;
267
+ }
268
+ };
269
+ const preflightRunTurnFailover = async (
270
+ firstSource: AsyncIterable<AdapterEvent>,
271
+ ): Promise<AsyncIterable<AdapterEvent>> => {
272
+ let source = firstSource;
273
+ while (true) {
274
+ const preflight = await preflightAdapterEvents(source);
275
+ if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
276
+ return preflight.stream;
277
+ }
278
+ const retryQueue = createAdapterEventQueue({
279
+ onBacklogExceeded: () => runTurnAbort.abort(),
280
+ });
281
+ void runTurnAttempt(retryQueue, "oauth-account-429");
282
+ source = retryQueue.stream();
283
+ }
284
+ };
285
+ // The empty-completion retry re-runs the turn against a fresh queue: the
286
+ // first queue is closed once its attempt settles, and pushing into it after
287
+ // close is a silent no-op.
288
+ const runTurnRetrySource = (): AsyncIterable<AdapterEvent> => {
289
+ const retryQueue = createAdapterEventQueue({
290
+ onBacklogExceeded: () => runTurnAbort.abort(),
291
+ });
292
+ void runTurnAttempt(retryQueue, "empty-completion");
293
+ return retryQueue.stream();
294
+ };
295
+
296
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
297
+ if (parsed.stream) {
298
+ void runTurn();
299
+ let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
300
+ if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
301
+ // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
302
+ // replayed transparently; after any output reaches the bridge, a later error stays terminal.
303
+ eventSource = await preflightRunTurnFailover(eventSource);
304
+ }
305
+ if (options.comboAttempt) {
306
+ const preflight = await preflightAdapterEvents(eventSource);
307
+ if (preflight.error || preflight.empty) {
308
+ runTurnAbort.abort();
309
+ queue.close();
310
+ const message = preflight.error?.message ?? "Adapter ended before producing a response";
311
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
312
+ }
313
+ eventSource = preflight.stream;
314
+ }
315
+ const guardedSource = emptyCompletionGuardEnabled
316
+ ? guardEmptyCompletionEventStream({
317
+ firstEvents: eventSource,
318
+ // Identical-turn retry: same parsed request, same headers, same
319
+ // signal — run the adapter transport again against a fresh queue.
320
+ continuation: runTurnRetrySource,
321
+ })
322
+ // Guard off (the default): leave the stream alone, but record that the turn ended
323
+ // empty so the user has something to correlate instead of an unexplained blank
324
+ // result (#2472). Retrying by default would re-send a turn that may already have had
325
+ // billable side effects, so the honest default is observability, not recovery.
326
+ : observeEmptyCompletion(eventSource, () => {
327
+ console.warn(emptyCompletionNotice(route.providerName, route.modelId));
328
+ });
329
+ const sseStream = bridgeToResponsesSSE(
330
+ guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
331
+ () => {
332
+ cancelResponseCompletion();
333
+ runTurnAbort.abort();
334
+ queue.close();
335
+ }, 2_000,
336
+ {
337
+ translatorBudget,
338
+ replayCacheScope: parsed._reasoningReplayScope,
339
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
340
+ stallTimeoutSec: config.stallTimeoutSec,
341
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
342
+ declaredToolNames,
343
+ toolParameterSchemas,
344
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
345
+ ...(routedCompaction ? { compaction: true } : {}),
346
+ // grok-build's strict decoder dies on the typed response.heartbeat frame; its
347
+ // eventsource layer tolerates comment keep-alives. Codex needs the opposite.
348
+ ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
349
+ onUsage: usage => {
350
+ // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
351
+ // zero-default detail objects, so provenance must come from here (cache_detail_missing).
352
+ logCtx.usageFromBridge = true;
353
+ if (usage) {
354
+ logCtx.usage = usage;
355
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
356
+ }
357
+ },
358
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
359
+ commitReasoningReplayServingRoute();
360
+ rememberKiroDeliveredFinalAnswer(transportState.adapter.name, response);
361
+ if (!routedCompaction) {
362
+ rememberResponseState(
363
+ parsed._rawBody,
364
+ response,
365
+ continuationStateForResponse(providerState),
366
+ responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
367
+ );
368
+ }
369
+ notifyResponseComplete(response);
370
+ },
371
+ },
372
+ );
373
+ const bridgeTurnAc = new AbortController();
374
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
375
+ const response = new Response(trackedSse, {
376
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
377
+ });
378
+ runTurnAdapterSseResponses.add(response);
379
+ return response;
380
+ }
381
+
382
+ await runTurn();
383
+ const firstAttemptEvents = await queue.collect();
384
+ let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
385
+ if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
386
+ runTurnEvents = [];
387
+ for await (const event of await preflightRunTurnFailover(
388
+ (async function* () { yield* firstAttemptEvents; })(),
389
+ )) runTurnEvents.push(event);
390
+ }
391
+ let events: AdapterEvent[];
392
+ if (emptyCompletionGuardEnabled) {
393
+ events = [];
394
+ for await (const event of guardEmptyCompletionEventStream({
395
+ firstEvents: (async function* () { yield* runTurnEvents; })(),
396
+ continuation: runTurnRetrySource,
397
+ })) events.push(event);
398
+ } else {
399
+ events = runTurnEvents;
400
+ }
401
+ if (options.comboAttempt) {
402
+ const firstMeaningful = events.find(event => event.type !== "heartbeat");
403
+ if (!firstMeaningful || firstMeaningful.type === "error") {
404
+ const message = firstMeaningful?.type === "error"
405
+ ? firstMeaningful.message
406
+ : "Adapter ended before producing a response";
407
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
408
+ }
409
+ }
410
+ let providerState: OcxProviderContinuationState | undefined;
411
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
412
+ translatorBudget,
413
+ replayCacheScope: parsed._reasoningReplayScope,
414
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
415
+ toolNsMap,
416
+ declaredToolNames,
417
+ toolParameterSchemas,
418
+ freeformToolNames,
419
+ toolSearchToolNames,
420
+ ...(routedCompaction ? { compaction: true } : {}),
421
+ onProviderState: state => { providerState = state; },
422
+ onUsage: usage => {
423
+ logCtx.usageFromBridge = true;
424
+ if (usage) {
425
+ logCtx.usage = usage;
426
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
427
+ }
428
+ },
429
+ });
430
+ if (!routedCompaction) {
431
+ rememberKiroDeliveredFinalAnswer(transportState.adapter.name, json);
432
+ rememberResponseState(
433
+ parsed._rawBody,
434
+ json,
435
+ continuationStateForResponse(providerState),
436
+ responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
437
+ );
438
+ }
439
+ // #1926 gap 2: the buffered path queued its signature persists inside
440
+ // buildResponseJSON; bound the durability window before the JSON becomes
441
+ // externally visible.
442
+ await awaitThoughtSignatureDurability();
443
+ if (adapterResponseReachedServingTerminal(events, json)) {
444
+ commitReasoningReplayServingRoute();
445
+ }
446
+ notifyResponseComplete(json);
447
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
448
+ }