@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,624 @@
1
+ import type {
2
+ AdapterEvent,
3
+ OcxMessagePhase,
4
+ OcxProviderContinuationState,
5
+ OcxProviderOpaqueToolCallMetadata,
6
+ OcxReasoningReplayScopeRef,
7
+ OcxUsage,
8
+ } from "../types";
9
+ import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
10
+ import {
11
+ adapterFailureFromMessage,
12
+ classifyError,
13
+ cyberPolicyErrorType,
14
+ CYBER_POLICY_ERROR_CODE,
15
+ isCyberPolicyCode,
16
+ type OcxErrorPayload,
17
+ } from "../lib/errors";
18
+ import { mayBecomePatchEnvelope, repairFreeformToolInput } from "../responses/apply-patch-envelope";
19
+ import { encodeCompactionSummary } from "../responses/compaction";
20
+ import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
21
+ import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
22
+ import { encodeReasoningEnvelope, type ReasoningEnvelope } from "../responses/reasoning-envelope";
23
+ import { rememberReasoningForCall } from "../responses/reasoning-replay-cache";
24
+ import {
25
+ rememberAndSerializeExtraContent,
26
+ rememberExtraContentForReplay,
27
+ awaitThoughtSignatureDurability,
28
+ } from "../responses/thought-signature-replay";
29
+ import {
30
+ createCitationMarkerFilter,
31
+ stripCitationMarkers,
32
+ type CitationMarkerFilter,
33
+ } from "../responses/citation-markers";
34
+ import { declaresCodeModeExec, normalizeDeclaredToolName } from "../types";
35
+ import { appendSafeWebSearchSource, safeWebSearchSources } from "../web-search/sources";
36
+ import {
37
+ isTranslatorBudgetExceededError,
38
+ releaseTranslatedEvent,
39
+ createTranslatorBudget,
40
+ type TranslatorBudget,
41
+ type TranslatorBufferKind,
42
+ } from "../lib/translator-budget";
43
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
44
+ import type { OutputItem, StringChunks } from "./internal";
45
+ import { bridgeToResponsesSSE } from "./sse";
46
+
47
+ export function buildResponseJSON(
48
+ events: AdapterEvent[],
49
+ modelId: string,
50
+ options?: Parameters<typeof buildResponseJSONWithBudget>[2],
51
+ ): Record<string, unknown> {
52
+ // Default-budget safety net: a caller that omits the budget gets a bounded
53
+ // default (disposed with the call), never the unbounded append path.
54
+ if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
55
+ const budget = createTranslatorBudget();
56
+ try {
57
+ return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
58
+ } finally {
59
+ budget.dispose();
60
+ }
61
+ }
62
+
63
+ function buildResponseJSONWithBudget(
64
+ events: AdapterEvent[],
65
+ modelId: string,
66
+ options?: {
67
+ hideThinkingSummary?: boolean;
68
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
69
+ /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
70
+ declaredToolNames?: ReadonlySet<string>;
71
+ /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
72
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
73
+ freeformToolNames?: Set<string>;
74
+ toolSearchToolNames?: Set<string>;
75
+ /** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
76
+ compaction?: boolean;
77
+ onProviderState?: (state: OcxProviderContinuationState) => void;
78
+ /** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
79
+ onUsage?: (usage: OcxUsage | undefined) => void;
80
+ translatorBudget?: TranslatorBudget;
81
+ /** Conversation identity for the reasoning replay cache (issue #950). */
82
+ replayCacheScope?: OcxReasoningReplayScopeRef;
83
+ },
84
+ ): Record<string, unknown> {
85
+ const responseId = `resp_${uuid()}`;
86
+ const replayCacheScope = options?.replayCacheScope;
87
+ const output: OutputItem[] = [];
88
+ const budget = options?.translatorBudget;
89
+ const encoder = new TextEncoder();
90
+ const bytesOf = (value: string): number => Buffer.byteLength(value);
91
+ const appendBatchString = (
92
+ previous: StringChunks,
93
+ fragment: string,
94
+ kind: TranslatorBufferKind,
95
+ callId?: string,
96
+ ): StringChunks => {
97
+ const fragmentBytes = bytesOf(fragment);
98
+ if (fragmentBytes === 0) return previous;
99
+ const nextBytes = previous.bytes + fragmentBytes;
100
+ if (!budget) {
101
+ previous.chunks.push(fragment);
102
+ return { chunks: previous.chunks, bytes: nextBytes };
103
+ }
104
+ const scope = { kind, ...(callId ? { callId } : {}) };
105
+ const reservation = budget.reserveTransient(nextBytes, scope);
106
+ try {
107
+ previous.chunks.push(fragment);
108
+ const result: StringChunks = { chunks: previous.chunks, bytes: nextBytes };
109
+ reservation.commitRetained();
110
+ budget.releaseRetained(previous.bytes, scope);
111
+ return result;
112
+ } catch (error) {
113
+ reservation.release();
114
+ throw error;
115
+ }
116
+ };
117
+ // Batch counterpart: tool-call arguments require direct string representation for immediate
118
+ // JSON serialization compatibility.
119
+ const appendBatchStringDirect = (
120
+ previous: string,
121
+ previousBytes: number,
122
+ fragment: string,
123
+ kind: TranslatorBufferKind,
124
+ callId?: string,
125
+ ): { value: string; bytes: number } => {
126
+ const nextBytes = previousBytes + bytesOf(fragment);
127
+ if (!budget) return { value: previous + fragment, bytes: nextBytes };
128
+ const scope = { kind, ...(callId ? { callId } : {}) };
129
+ const reservation = budget.reserveTransient(nextBytes, scope);
130
+ try {
131
+ const value = previous + fragment;
132
+ reservation.commitRetained();
133
+ budget.releaseRetained(previousBytes, scope);
134
+ return { value, bytes: nextBytes };
135
+ } catch (error) {
136
+ reservation.release();
137
+ throw error;
138
+ }
139
+ };
140
+ const replaceBatchRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
141
+ const nextBytes = bytesOf(next);
142
+ if (!budget) return nextBytes;
143
+ const reservation = budget.reserveTransient(nextBytes, { kind });
144
+ reservation.commitRetained();
145
+ budget.releaseRetained(previousBytes, { kind });
146
+ return nextBytes;
147
+ };
148
+ const pushOutput = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
149
+ const reservation = budget?.reserveTransient(bytesOf(JSON.stringify(item)), { kind });
150
+ output.push(item);
151
+ reservation?.commitRetained();
152
+ if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
153
+ };
154
+ let usage: OcxUsage | undefined;
155
+ let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
156
+ let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
157
+ let endTurn: boolean | undefined;
158
+ let stopReason: string | undefined;
159
+ // The adapter's stop reason exactly as it arrived. `stopReason` above is deliberately narrowed
160
+ // to the two reasons that map onto a Responses `incomplete_details`; the raw value is what the
161
+ // truncation guard needs, because adapters disagree on vocabulary (`length`, `refusal`, ...).
162
+ let rawStopReason: string | undefined;
163
+ let cleanDone = false;
164
+ // Whether the adapter emitted ANY terminal (done/error/incomplete). Distinct from `cleanDone`,
165
+ // which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
166
+ // stopped emitting has no terminal at all, and must not be reported as a success.
167
+ let sawTerminal = false;
168
+ let batchCompaction = emptyChunks();
169
+ let compactionEncryptedContent: string | undefined;
170
+
171
+ let currentText = emptyChunks();
172
+ let currentTextPhase: OcxMessagePhase | undefined;
173
+ let currentSummaryReasoning = emptyChunks();
174
+ let currentRawReasoning = emptyChunks();
175
+ // Same replay-cache handoff as the streaming path (issue #950): the most
176
+ // recently flushed raw reasoning waits for the tool call it preceded.
177
+ let rawReasoningForNextToolCall = "";
178
+ // Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
179
+ let batchSignature: string | undefined;
180
+ let batchSignatureBytes = 0;
181
+ let batchRedacted: string[] = [];
182
+ let batchRedactedBytes = 0;
183
+ // Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
184
+ // message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
185
+ let batchKiroRedacted: string | undefined;
186
+ let batchKiroRedactedBytes = 0;
187
+ let currentToolCallId = "";
188
+ let currentToolCallName = "";
189
+ let currentToolCallCodeModeHelperName: string | undefined;
190
+ let currentToolCallArgs = "";
191
+ let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
192
+ let currentToolCallArgsBytes = 0;
193
+ // Web-search citations awaiting the next assistant message (attached as url_citation annotations).
194
+ let pendingWebSources: { url: string; title?: string }[] = [];
195
+
196
+ const freeformInput = (
197
+ args: string,
198
+ toolName: string,
199
+ namespace?: string,
200
+ codeModeHelperName?: string,
201
+ ): string => {
202
+ const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
203
+ return helper
204
+ ? compileCodeModeHelperInput(args, helper)
205
+ : repairFreeformToolInput(args, toolName, namespace);
206
+ };
207
+ const parseArgsObj = (args: string): Record<string, unknown> => {
208
+ try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
209
+ };
210
+
211
+ const flushText = (inferredPhase?: OcxMessagePhase) => {
212
+ const currentTextStr = joinChunks(currentText);
213
+ if (!currentTextStr) return;
214
+ const phase = currentTextPhase ?? inferredPhase;
215
+ // ChatGPT-backend citation markers arrive as literal private-use characters that the
216
+ // Codex TUI prints verbatim (#3150). Strip them here rather than at the accumulator so
217
+ // the retained byte accounting above still describes what the upstream actually sent.
218
+ const text = stripCitationMarkers(currentTextStr);
219
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
220
+ const annotations = pendingWebSources.map(s => ({
221
+ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
222
+ }));
223
+ pendingWebSources = [];
224
+ const item = {
225
+ type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
226
+ content: [{ type: "output_text", text, annotations }],
227
+ ...(phase ? { phase } : {}),
228
+ } as OutputItem;
229
+ pushOutput(item, currentText.bytes);
230
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
231
+ currentText = emptyChunks();
232
+ currentTextPhase = undefined;
233
+ };
234
+ const flushSummaryReasoning = () => {
235
+ const summaryText = joinChunks(currentSummaryReasoning);
236
+ if (!summaryText && !batchSignature && batchRedacted.length === 0) return;
237
+ const envelope: ReasoningEnvelope = {};
238
+ if (batchSignature) envelope.sig = batchSignature;
239
+ if (batchRedacted.length > 0) envelope.red = batchRedacted;
240
+ const hidden = options?.hideThinkingSummary === true;
241
+ if (hidden && summaryText && (envelope.sig || envelope.red)) envelope.txt = summaryText;
242
+ const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope, budget) : undefined;
243
+ const sourceBytes = currentSummaryReasoning.bytes + batchSignatureBytes + batchRedactedBytes;
244
+ batchSignature = undefined;
245
+ batchSignatureBytes = 0;
246
+ batchRedacted = [];
247
+ batchRedactedBytes = 0;
248
+ if (hidden && !encrypted) {
249
+ budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
250
+ currentSummaryReasoning = emptyChunks();
251
+ return;
252
+ }
253
+ const item = {
254
+ type: "reasoning", id: `rs_${uuid()}`,
255
+ summary: !hidden && summaryText ? [{ type: "summary_text", text: summaryText }] : [],
256
+ ...(encrypted ? { encrypted_content: encrypted } : {}),
257
+ } as OutputItem;
258
+ pushOutput(item, sourceBytes, "reasoning");
259
+ currentSummaryReasoning = emptyChunks();
260
+ };
261
+ const flushRawReasoning = () => {
262
+ const rawText = joinChunks(currentRawReasoning);
263
+ if (!rawText) return;
264
+ rawReasoningForNextToolCall = rawText;
265
+ if (options?.hideThinkingSummary === true) {
266
+ // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
267
+ pushOutput({
268
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
269
+ encrypted_content: encodeReasoningEnvelope({ txt: rawText }, budget),
270
+ }, currentRawReasoning.bytes, "reasoning");
271
+ currentRawReasoning = emptyChunks();
272
+ return;
273
+ }
274
+ pushOutput({
275
+ type: "reasoning", id: `rs_${uuid()}`,
276
+ summary: [],
277
+ content: [{ type: "reasoning_text", text: rawText }],
278
+ }, currentRawReasoning.bytes, "reasoning");
279
+ currentRawReasoning = emptyChunks();
280
+ };
281
+ const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
282
+ if (!currentToolCallId) return;
283
+ const mapped = options?.toolNsMap?.get(currentToolCallName);
284
+ const realName = mapped?.name ?? currentToolCallName;
285
+ const ns = mapped?.namespace;
286
+ const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
287
+ const freeform = !toolSearch && (mapped
288
+ ? mapped.freeform === true
289
+ : (options?.freeformToolNames?.has(realName) ?? false));
290
+ // #1611: same integral-float repair as the streaming path. Keyed by the wire name
291
+ // the request declared, which is the pre-namespace-mapping `currentToolCallName`.
292
+ const coercedArgs = coerceIntegerToolArguments(
293
+ currentToolCallArgs,
294
+ options?.toolParameterSchemas?.get(currentToolCallName),
295
+ ns === undefined ? realName : undefined,
296
+ );
297
+ // Freeform tools serialize as custom_tool_call without extra_content; remember the
298
+ // signature server-side regardless so the replayed call can be re-signed (#1735).
299
+ void rememberExtraContentForReplay(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope);
300
+ if (toolSearch) {
301
+ pushOutput({
302
+ type: "tool_search_call", id: `tsc_${uuid()}`,
303
+ call_id: currentToolCallId, execution: "client",
304
+ arguments: parseArgsObj(coercedArgs), status,
305
+ });
306
+ } else if (freeform) {
307
+ pushOutput({
308
+ type: "custom_tool_call", id: `ctc_${uuid()}`,
309
+ call_id: currentToolCallId, name: realName,
310
+ ...(ns ? { namespace: ns } : {}),
311
+ input: freeformInput(currentToolCallArgs, realName, ns, currentToolCallCodeModeHelperName), status,
312
+ });
313
+ } else {
314
+ pushOutput({
315
+ type: "function_call", id: `fc_${uuid()}`,
316
+ call_id: currentToolCallId, name: realName,
317
+ arguments: coercedArgs || "{}", status,
318
+ ...(ns ? { namespace: ns } : {}),
319
+ ...(rememberAndSerializeExtraContent(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope).extra ?? {}),
320
+ });
321
+ }
322
+ budget?.closeCall(currentToolCallId);
323
+ currentToolCallId = "";
324
+ currentToolCallName = "";
325
+ currentToolCallCodeModeHelperName = undefined;
326
+ currentToolCallProviderMetadata = undefined;
327
+ currentToolCallArgs = "";
328
+ currentToolCallArgsBytes = 0;
329
+ };
330
+
331
+ for (const e of events) {
332
+ if (errorEvent) {
333
+ // Match streaming: once the turn fails, later parallel calls must not become executable
334
+ // completed output. Still release every retained event in order and preserve terminal usage.
335
+ if (e.type === "error" || e.type === "incomplete" || e.type === "done") {
336
+ usage = e.usage ?? usage;
337
+ }
338
+ if (budget) releaseTranslatedEvent(e, budget);
339
+ continue;
340
+ }
341
+ if (batchSignature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") {
342
+ flushSummaryReasoning();
343
+ }
344
+ switch (e.type) {
345
+ case "assistant_boundary":
346
+ flushText("commentary");
347
+ flushSummaryReasoning();
348
+ flushRawReasoning();
349
+ rawReasoningForNextToolCall = "";
350
+ flushToolCall();
351
+ break;
352
+ case "text_delta":
353
+ // Only flush on an explicit phase change. A later delta that omits `phase` must keep
354
+ // appending under the previously established phase.
355
+ if (currentText.bytes > 0 && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
356
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
357
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
358
+ // Empty text deltas (batch chat responses always carry content, often "") must
359
+ // not wipe reasoning that precedes a tool call (#950 non-streaming path).
360
+ if (e.text.length > 0) rawReasoningForNextToolCall = "";
361
+ if (currentToolCallId) flushToolCall();
362
+ // Compaction turns keep the summary out of normal message output (replay dedup — see
363
+ // bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
364
+ if (options?.compaction) {
365
+ batchCompaction = appendBatchString(
366
+ batchCompaction, e.text, "retained_collectors",
367
+ );
368
+ }
369
+ else {
370
+ if (e.phase !== undefined) currentTextPhase = e.phase;
371
+ currentText = appendBatchString(
372
+ currentText, e.text, "retained_collectors",
373
+ );
374
+ }
375
+ break;
376
+ case "thinking_delta":
377
+ if (currentText.bytes > 0) flushText("commentary");
378
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
379
+ if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
380
+ if (currentToolCallId) flushToolCall();
381
+ {
382
+ currentSummaryReasoning = appendBatchString(
383
+ currentSummaryReasoning, e.thinking, "reasoning",
384
+ );
385
+ }
386
+ break;
387
+ case "thinking_signature":
388
+ // Like streaming, retain the latest signature update until the next semantic
389
+ // event. Flushing every update would manufacture signature-only siblings.
390
+ batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
391
+ batchSignature = e.signature;
392
+ break;
393
+ case "redacted_thinking":
394
+ flushText("commentary");
395
+ flushSummaryReasoning();
396
+ flushRawReasoning();
397
+ flushToolCall();
398
+ {
399
+ const dataBytes = bytesOf(e.data);
400
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
401
+ batchRedactedBytes += dataBytes;
402
+ }
403
+ batchRedacted.push(e.data);
404
+ flushSummaryReasoning();
405
+ break;
406
+ case "kiro_redacted_reasoning":
407
+ // Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
408
+ {
409
+ const dataBytes = bytesOf(e.data);
410
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
411
+ if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
412
+ batchKiroRedactedBytes = dataBytes;
413
+ }
414
+ batchKiroRedacted = e.data;
415
+ break;
416
+ case "reasoning_raw_delta":
417
+ if (currentText.bytes > 0) flushText("commentary");
418
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
419
+ if (currentToolCallId) flushToolCall();
420
+ {
421
+ currentRawReasoning = appendBatchString(
422
+ currentRawReasoning, e.text, "reasoning",
423
+ );
424
+ }
425
+ break;
426
+ case "tool_call_start": {
427
+ if (currentText.bytes > 0) flushText("commentary");
428
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
429
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
430
+ if (rawReasoningForNextToolCall) {
431
+ rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
432
+ }
433
+ flushToolCall();
434
+ const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
435
+ if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
436
+ errorEvent = {
437
+ type: "error",
438
+ message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
439
+ status: 502,
440
+ errorType: "upstream_error",
441
+ };
442
+ break;
443
+ }
444
+ currentToolCallId = e.id;
445
+ budget?.openCall(e.id);
446
+ currentToolCallName = effectiveName;
447
+ currentToolCallCodeModeHelperName = effectiveName === "exec" && e.name !== effectiveName
448
+ ? e.name
449
+ : undefined;
450
+ currentToolCallArgs = "";
451
+ currentToolCallArgsBytes = 0;
452
+ currentToolCallProviderMetadata = e.providerMetadata;
453
+ break;
454
+ }
455
+ case "tool_call_delta":
456
+ {
457
+ ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchStringDirect(
458
+ currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
459
+ ));
460
+ }
461
+ break;
462
+ case "tool_call_end":
463
+ if (!toolCallArgumentsUsable(currentToolCallArgs) && currentToolCallId) {
464
+ // Mirror the streaming path: refuse to complete unusable arguments.
465
+ const mapped = options?.toolNsMap?.get(currentToolCallName);
466
+ const realName = mapped?.name ?? currentToolCallName;
467
+ const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
468
+ const freeform = !toolSearch && (mapped
469
+ ? mapped.freeform === true
470
+ : (options?.freeformToolNames?.has(realName) ?? false));
471
+ if (!freeform && !toolSearch) {
472
+ flushToolCall("incomplete");
473
+ errorEvent = {
474
+ type: "error",
475
+ message: "upstream stream produced malformed tool call arguments",
476
+ status: 502,
477
+ errorType: "upstream_error",
478
+ };
479
+ break;
480
+ }
481
+ }
482
+ flushToolCall();
483
+ break;
484
+ case "web_search_call_begin":
485
+ // Batch/non-streaming output has no in_progress phase to animate — the search cell is a
486
+ // single finalized item, emitted on `end`. Begin is a no-op here.
487
+ break;
488
+ case "web_search_call_end": {
489
+ if (currentText.bytes > 0) flushText("commentary");
490
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
491
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
492
+ flushToolCall();
493
+ const safeSources = safeWebSearchSources(e.sources);
494
+ pushOutput({
495
+ type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
496
+ action: webSearchAction(e.queries),
497
+ ...(safeSources.length > 0 ? { sources: safeSources } : {}),
498
+ });
499
+ if (safeSources.length > 0) {
500
+ for (const source of safeSources) {
501
+ if (appendSafeWebSearchSource(pendingWebSources, source)) {
502
+ budget?.chargeRetained(bytesOf(JSON.stringify(source)), { kind: "tool_search_sources" });
503
+ }
504
+ }
505
+ }
506
+ break;
507
+ }
508
+ case "error":
509
+ errorEvent = e;
510
+ sawTerminal = true;
511
+ usage = e.usage ?? usage;
512
+ break;
513
+ case "incomplete":
514
+ incompleteEvent = e;
515
+ sawTerminal = true;
516
+ endTurn = e.endTurn;
517
+ if (e.providerState) options?.onProviderState?.(e.providerState);
518
+ break;
519
+ case "done":
520
+ usage = e.usage;
521
+ compactionEncryptedContent = e.compactionEncryptedContent;
522
+ sawTerminal = true;
523
+ endTurn = e.endTurn;
524
+ cleanDone = e.stopReason === undefined;
525
+ rawStopReason = e.stopReason;
526
+ if (e.providerState) options?.onProviderState?.(e.providerState);
527
+ // Match streaming: max_tokens and content_filter both terminate as incomplete.
528
+ // Normalize every adapter's truncation vocabulary to the canonical pair, so a raw
529
+ // `length` or `refusal` reaches the status/incomplete_details logic below instead of
530
+ // silently reading as a clean stop.
531
+ {
532
+ const truncation = truncationReasonFor(e.stopReason);
533
+ if (truncation) stopReason = truncation === "max_output_tokens" ? "max_tokens" : "content_filter";
534
+ }
535
+ break;
536
+ }
537
+ if (budget) releaseTranslatedEvent(e, budget);
538
+ }
539
+ flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
540
+ if (pendingWebSources.length > 0) {
541
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
542
+ pendingWebSources = [];
543
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
544
+ }
545
+ flushSummaryReasoning();
546
+ flushRawReasoning();
547
+ // Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
548
+ // must one left open by a stream that stopped without any terminal at all. That case previously
549
+ // fell through to "completed", handing back a function_call whose arguments were half-written
550
+ // JSON, inside a turn also marked completed.
551
+ if (currentToolCallId) {
552
+ flushToolCall(errorEvent || incompleteEvent || !sawTerminal || isTruncatedStopReason(rawStopReason)
553
+ ? "incomplete" : "completed");
554
+ }
555
+ if (batchKiroRedacted) {
556
+ // pushOutput reserves the item itself and releases the retained raw blob it replaces.
557
+ pushOutput({
558
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
559
+ encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }, budget),
560
+ }, batchKiroRedactedBytes, "reasoning");
561
+ batchKiroRedacted = undefined;
562
+ batchKiroRedactedBytes = 0;
563
+ }
564
+ // A truncated turn must never be installed as replacement history: emit the
565
+ // compaction item only when the turn actually completed (#422).
566
+ if (
567
+ options?.compaction
568
+ && !errorEvent
569
+ && !incompleteEvent
570
+ // A stream that stopped without any terminal did not complete either. The original guard
571
+ // could only see explicit failure events, so an adapter EOF slipped past it and installed a
572
+ // truncated summary as replacement history — the exact #422 hazard, reached by a route that
573
+ // did not exist when the guard was written.
574
+ && sawTerminal
575
+ && !isTruncatedStopReason(rawStopReason)
576
+ ) {
577
+ const item = {
578
+ type: "compaction", id: `cmp_${uuid()}`,
579
+ encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
580
+ };
581
+ pushOutput(item, compactionEncryptedContent ? bytesOf(compactionEncryptedContent) : batchCompaction.bytes);
582
+ }
583
+
584
+ const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
585
+ const status = errorEvent
586
+ ? "failed"
587
+ : incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
588
+ ? "incomplete"
589
+ : sawTerminal
590
+ ? "completed"
591
+ // The adapter stopped emitting without any terminal, so the turn was cut short. Streaming
592
+ // already reports this as response.incomplete / adapter_eof (see the !terminated branch);
593
+ // defaulting the buffered path to "completed" handed callers a truncated turn — including
594
+ // one carrying a never-closed tool call with half-written JSON arguments — as a success.
595
+ : "incomplete";
596
+ options?.onUsage?.(incompleteEvent?.usage ?? usage);
597
+ return {
598
+ id: responseId, object: "response",
599
+ created_at: Math.floor(Date.now() / 1000),
600
+ status,
601
+ model: modelId, output,
602
+ ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
603
+ ...(failure ? { error: failure.error, last_error: failure.error } : {}),
604
+ ...(failure && isCyberPolicyCode(failure.error.code)
605
+ ? { retryable: false }
606
+ : errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
607
+ ...(incompleteEvent ? {
608
+ incomplete_details: {
609
+ reason: incompleteEvent.reason,
610
+ ...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
611
+ ...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
612
+ },
613
+ } : stopReason === "max_tokens" ? {
614
+ incomplete_details: { reason: "max_output_tokens" },
615
+ } : stopReason === "content_filter" ? {
616
+ incomplete_details: { reason: "content_filter" },
617
+ } : !sawTerminal ? {
618
+ // Same reason string the streaming path uses, so a caller sees one signal for one condition
619
+ // regardless of which surface it asked for.
620
+ incomplete_details: { reason: "adapter_eof" },
621
+ } : {}),
622
+ usage: responsesUsage(incompleteEvent?.usage ?? usage),
623
+ };
624
+ }