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