@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,509 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
3
+ import { isPlainObject } from "./internal";
4
+
5
+ const MAX_RESPONSES_CALL_ID_LENGTH = 64;
6
+
7
+ const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
8
+ const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
9
+
10
+ /**
11
+ * The ChatGPT Responses backend rejects input `call_id` values longer than 64 characters. Codex
12
+ * sidechat/fork replay can namespace call ids from routed providers past that limit. Forward mode
13
+ * already sends explicit replay input without `previous_response_id`, so it is safe to replace each
14
+ * oversized id and every matching call/output occurrence with one deterministic request-local alias.
15
+ * Raw API-key continuations are intentionally excluded because an output-only continuation may
16
+ * reference a call stored upstream under the original id. Proxy-expanded API-key replays are
17
+ * explicit and stateless here, so they are safe to repair too.
18
+ */
19
+ export function repairOversizedReplayCallIds(body: unknown): unknown {
20
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
21
+
22
+ const occupied = new Set<string>();
23
+ for (const item of body.input) {
24
+ if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
25
+ if (item.call_id.length <= MAX_RESPONSES_CALL_ID_LENGTH) occupied.add(item.call_id);
26
+ }
27
+
28
+ const aliases = new Map<string, string>();
29
+ let changed = false;
30
+ const input = body.input.map(item => {
31
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return item;
32
+ const original = item.call_id;
33
+ if (original.length <= MAX_RESPONSES_CALL_ID_LENGTH) return item;
34
+
35
+ let alias = aliases.get(original);
36
+ if (!alias) {
37
+ let salt = 0;
38
+ do {
39
+ const hashInput = salt === 0 ? original : `${original}\0${salt}`;
40
+ const digest = createHash("sha256").update(hashInput).digest("hex");
41
+ alias = `${REPAIRED_CALL_ID_PREFIX}${digest.slice(0, REPAIRED_CALL_ID_DIGEST_LENGTH)}`;
42
+ salt += 1;
43
+ } while (occupied.has(alias));
44
+ aliases.set(original, alias);
45
+ occupied.add(alias);
46
+ }
47
+
48
+ changed = true;
49
+ return { ...item, call_id: alias };
50
+ });
51
+
52
+ return changed ? { ...body, input } : body;
53
+ }
54
+
55
+ /** Flatten a Responses tool-output `output` value (string or content-part array) to plain text. */
56
+ function toolOutputText(output: unknown): string {
57
+ if (typeof output === "string") return output;
58
+ if (!Array.isArray(output)) return JSON.stringify(output ?? "");
59
+ return output.map(part => {
60
+ if (!isPlainObject(part)) return "";
61
+ if (typeof part.text === "string") return part.text;
62
+ if (part.type === "refusal" && typeof part.refusal === "string") return `[refusal] ${part.refusal}`;
63
+ return "";
64
+ }).filter(Boolean).join("\n");
65
+ }
66
+
67
+ /** True when an output can be losslessly represented as user-message content. */
68
+ function isRepairableToolOutput(output: unknown): output is string | Record<string, unknown>[] {
69
+ if (typeof output === "string") return true;
70
+ if (!Array.isArray(output)) return false;
71
+ return output.every(part => {
72
+ if (!isPlainObject(part)) return false;
73
+ if (typeof part.type !== "string") return false;
74
+ if (["output_text", "text", "input_text"].includes(part.type)) {
75
+ return typeof part.text === "string";
76
+ }
77
+ if (part.type === "refusal") return typeof part.refusal === "string";
78
+ if (part.type === "encrypted_content") return typeof part.encrypted_content === "string";
79
+ if (part.type !== "input_image") return false;
80
+ const imageUrl = part.image_url;
81
+ const fileId = part.file_id;
82
+ const imageUrlIsString = typeof imageUrl === "string";
83
+ const fileIdIsString = typeof fileId === "string";
84
+ const hasUsableSource = (imageUrlIsString && imageUrl.length > 0)
85
+ || (fileIdIsString && fileId.length > 0);
86
+ const validSource = hasUsableSource
87
+ && (part.image_url === undefined || imageUrlIsString)
88
+ && (part.file_id === undefined || fileIdIsString);
89
+ const validDetail = part.detail === undefined
90
+ || (typeof part.detail === "string"
91
+ && ["auto", "low", "high", "original"].includes(part.detail));
92
+ return validSource && validDetail;
93
+ });
94
+ }
95
+
96
+ /** Convert orphaned tool output to user-message content without discarding valid images. */
97
+ function orphanedToolOutputContent(output: unknown, callId = ""): Record<string, unknown>[] {
98
+ const marker = `[tool output for ${callId || "unknown call"}]`;
99
+ if (typeof output !== "string" && !Array.isArray(output)) {
100
+ return [{ type: "input_text", text: marker }];
101
+ }
102
+ if (!Array.isArray(output)) {
103
+ return [{ type: "input_text", text: `${marker}\n${toolOutputText(output)}` }];
104
+ }
105
+
106
+ const content: Record<string, unknown>[] = [{ type: "input_text", text: marker }];
107
+ for (const part of output) {
108
+ if (!isPlainObject(part)) continue;
109
+ if (part.type === "input_image") {
110
+ content.push(part);
111
+ } else if (part.type === "encrypted_content" && typeof part.encrypted_content === "string") {
112
+ content.push({ type: "input_text", text: "[encrypted content omitted]" });
113
+ } else if (typeof part.text === "string") {
114
+ content.push({ type: "input_text", text: part.text });
115
+ } else if (part.type === "refusal" && typeof part.refusal === "string") {
116
+ content.push({ type: "input_text", text: `[refusal] ${part.refusal}` });
117
+ }
118
+ }
119
+ return content;
120
+ }
121
+
122
+ /** True when a Responses tool output item is present but carries no usable content. */
123
+ function isToolOutputEmpty(output: unknown): boolean {
124
+ if (typeof output === "string") return output.trim() === "";
125
+ if (Array.isArray(output)) {
126
+ // Mirror the Chat wire rule through the shared contract: only a pure
127
+ // text/refusal part array whose joined content trims empty is annotated.
128
+ // input_image, encrypted_content, input_file and any other non-text part is
129
+ // real output and must never be replaced.
130
+ return isWhitespaceOnlyTextPartArray(output);
131
+ }
132
+ // A missing or null `output` is not a present-but-empty result: it is an
133
+ // incomplete payload. Leave it untouched so the upstream contract fails
134
+ // closed, and the orphan repair can surface it honestly instead of claiming
135
+ // the tool ran with no output.
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Rewrite present-but-empty tool outputs to an explicit annotation. Synthetic
141
+ * missing-result placeholders are non-empty and pass through untouched. No-op unless
142
+ * the provider opts in (`annotateEmptyToolOutputs`).
143
+ */
144
+ export function annotateEmptyResponsesToolOutputs(body: unknown, enabled: boolean): unknown {
145
+ if (!enabled || !isPlainObject(body) || !Array.isArray(body.input)) return body;
146
+ let changed = false;
147
+ const input = body.input.map(item => {
148
+ if (!isPlainObject(item) || (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")) return item;
149
+ if (!isToolOutputEmpty(item.output)) return item;
150
+ changed = true;
151
+ return { ...item, output: EMPTY_TOOL_OUTPUT_ANNOTATION };
152
+ });
153
+ return changed ? { ...body, input } : body;
154
+ }
155
+
156
+ /**
157
+ * Preserve the text of structurally invalid tool-output items before they reach a strict
158
+ * Responses parser. Stateful destinations may legitimately receive an output whose matching
159
+ * call lives behind `previous_response_id`, so ordinary orphan repair cannot run universally.
160
+ * A missing or empty `call_id`, however, cannot identify stored state on any destination.
161
+ */
162
+ export function repairUnidentifiedToolOutputItems(body: unknown): unknown {
163
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
164
+ let changed = false;
165
+ const input = body.input.map(item => {
166
+ if (!isPlainObject(item)
167
+ || (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")
168
+ || (typeof item.call_id === "string" && item.call_id.length > 0)) {
169
+ return item;
170
+ }
171
+ if (!isRepairableToolOutput(item.output)) return item;
172
+ changed = true;
173
+ return {
174
+ type: "message",
175
+ role: "user",
176
+ content: orphanedToolOutputContent(item.output),
177
+ };
178
+ });
179
+ return changed ? { ...body, input } : body;
180
+ }
181
+
182
+ /**
183
+ * Repair a forward-mode input array whose continuation context was lost. When the replay
184
+ * expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
185
+ * (the ChatGPT backend rejects it), so the delta may carry items that reference now-absent
186
+ * prior items and 400 upstream:
187
+ * - `function_call`/`local_shell_call`/`custom_tool_call` without their paired output item
188
+ * ("No tool output found for tool call <call_id>"). A stateless upstream cannot resolve
189
+ * the pair from its own storage, so a placeholder output is synthesized to keep the
190
+ * turn continuable without pretending the result was real. Synthetic outputs are
191
+ * emitted after the complete parallel call batch, in call order alongside any real
192
+ * outputs, so the adjacency normalizer can still recognize the batch as one
193
+ * reasoning-bearing assistant turn (#1477). Gated on
194
+ * `synthesizeMissingCallOutputs` (stateless AND non-forward wires); forward replay keeps
195
+ * fail-closed behavior.
196
+ * - `function_call_output`/`custom_tool_call_output` without their paired call item
197
+ * ("No tool call found for function call output with call_id ..."). Converted to user
198
+ * messages so the result text survives. `function_call_output` also pairs with
199
+ * `local_shell_call` (codex-rs emits shell outputs as function_call_output).
200
+ * - `reasoning` items ("Item 'rs_*' ... was provided without its required following item").
201
+ * Dropped, but only when `dropReasoning` (unexpanded miss): on a replay hit the prior
202
+ * reasoning chain is intact and must be preserved.
203
+ * Runs on every forward request; with intact pairs it returns the original reference.
204
+ */
205
+ /**
206
+ * Repair a replayed `web_search_call` action that is missing either key.
207
+ *
208
+ * `webSearchAction()` in the bridge now emits both keys, but that only helps items
209
+ * created after the fix. A conversation that already recorded
210
+ * `{type:"search", query:"..."}` or `{type:"search", queries:[...]}` replays that stored
211
+ * item on every subsequent turn. DeepSeek's native Responses parser requires `queries`
212
+ * (#930) and Console Go's validator requires `query` (#3071), so upgrading alone leaves
213
+ * those threads permanently 400ing in one direction or the other. The repair runs both
214
+ * ways.
215
+ *
216
+ * Input items carry a loose schema, so a stored `queries` is not necessarily an array of
217
+ * strings. A partly- or wholly-malformed array is left alone rather than used as a source
218
+ * for the singular field: writing `query: 123` would satisfy the presence check and still
219
+ * fail the validator this repair exists to satisfy, and deriving `query` from
220
+ * `["a", 42]` would satisfy Console Go while leaving DeepSeek to reject the same replay.
221
+ * An empty `queries: []` canonicalizes to the shape the bridge emits for an empty search,
222
+ * keeping an existing `query` when the item has one.
223
+ *
224
+ * Runs on every Responses request, on both `input` items and the `action` nested inside
225
+ * them. Returns the original reference when nothing needs repair, so the common path
226
+ * allocates nothing.
227
+ */
228
+ export function backfillWebSearchQueries(body: unknown): unknown {
229
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
230
+ let changed = false;
231
+ const input = body.input.map(item => {
232
+ if (!isPlainObject(item) || item.type !== "web_search_call") return item;
233
+ const action = item.action;
234
+ if (!isPlainObject(action) || action.type !== "search") return item;
235
+ // Repair whichever side is missing so both strict parsers pass:
236
+ // DeepSeek native Responses requires `queries`; Console Go requires `query`.
237
+ const rep: Record<string, unknown> = { ...action };
238
+ let itemChanged = false;
239
+ const hasQuery = typeof action.query === "string";
240
+ const queries = Array.isArray(action.queries) ? action.queries : undefined;
241
+ if (queries !== undefined && queries.length === 0) {
242
+ // An empty array satisfies neither validator. Canonicalize to the empty-search
243
+ // shape the bridge emits, keeping an existing query rather than discarding it.
244
+ const query = hasQuery ? action.query as string : "";
245
+ rep.query = query;
246
+ rep.queries = [query];
247
+ itemChanged = true;
248
+ } else if (!hasQuery && queries !== undefined) {
249
+ // A plural array is only a usable source for the singular field when EVERY member
250
+ // is a string: deriving `query` from a partly-malformed array would satisfy Console
251
+ // Go while leaving DeepSeek to reject the same replay. Wholly malformed arrays are
252
+ // left untouched — coercing or dropping members would invent semantics the stored
253
+ // item never had.
254
+ if (queries.every(entry => typeof entry === "string")) {
255
+ rep.query = queries[0]; // multi-query item recorded before the fix
256
+ itemChanged = true;
257
+ }
258
+ } else if (hasQuery && queries === undefined) {
259
+ rep.queries = [action.query]; // single-query item recorded before the fix
260
+ itemChanged = true;
261
+ }
262
+ if (itemChanged) changed = true;
263
+ return itemChanged ? { ...item, action: rep } : item;
264
+ });
265
+ return changed ? { ...body, input } : body;
266
+ }
267
+
268
+ export function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
269
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
270
+ const input = body.input;
271
+
272
+ const functionCallIds = new Set<string>();
273
+ const customCallIds = new Set<string>();
274
+ const functionOutputIds = new Set<string>();
275
+ const customOutputIds = new Set<string>();
276
+ for (const item of input) {
277
+ if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
278
+ if (item.type === "function_call" || item.type === "local_shell_call") functionCallIds.add(item.call_id);
279
+ else if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
280
+ else if (item.type === "function_call_output") functionOutputIds.add(item.call_id);
281
+ else if (item.type === "custom_tool_call_output") customOutputIds.add(item.call_id);
282
+ }
283
+
284
+ let changed = false;
285
+ const repaired: unknown[] = [];
286
+ const syntheticKeys = new Set<string>();
287
+ const pendingSyntheticOutputs: unknown[] = [];
288
+ const flushPendingSyntheticOutputs = (): void => {
289
+ if (pendingSyntheticOutputs.length === 0) return;
290
+ repaired.push(...pendingSyntheticOutputs);
291
+ pendingSyntheticOutputs.length = 0;
292
+ };
293
+ for (const item of input) {
294
+ if (!isPlainObject(item)) { flushPendingSyntheticOutputs(); repaired.push(item); continue; }
295
+ if (dropReasoning && item.type === "reasoning") { changed = true; continue; }
296
+ const isFnOutput = item.type === "function_call_output";
297
+ const isCustomOutput = item.type === "custom_tool_call_output";
298
+ if (isFnOutput || isCustomOutput) {
299
+ flushPendingSyntheticOutputs();
300
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
301
+ const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
302
+ const usableOutput = isRepairableToolOutput(item.output);
303
+ // A known orphan call is still useful as a labeled user message even when its output is
304
+ // incomplete. With no call id and no output, preserve the invalid item so validation fails
305
+ // closed rather than pretending any tool result exists.
306
+ const knownNullOutput = callId.length > 0 && item.output == null;
307
+ if (!paired && (knownNullOutput || usableOutput)) {
308
+ changed = true;
309
+ repaired.push({
310
+ type: "message",
311
+ role: "user",
312
+ content: orphanedToolOutputContent(item.output, callId),
313
+ });
314
+ continue;
315
+ }
316
+ }
317
+ const isFnCall = item.type === "function_call" || item.type === "local_shell_call";
318
+ const isCustomCall = item.type === "custom_tool_call";
319
+ if (isFnCall || isCustomCall) {
320
+ repaired.push(item);
321
+ if (synthesizeMissingCallOutputs) {
322
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
323
+ const hasOutput = isFnCall ? functionOutputIds.has(callId) : customOutputIds.has(callId);
324
+ if (!hasOutput && callId) {
325
+ changed = true;
326
+ const name = typeof item.name === "string" && item.name.length > 0 ? item.name : callId;
327
+ const text = `[ocx] no tool result was recorded for "${name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`;
328
+ syntheticKeys.add(`${isFnCall ? "function" : "custom"}:${callId}`);
329
+ pendingSyntheticOutputs.push(isFnCall
330
+ ? { type: "function_call_output", call_id: callId, output: text }
331
+ : { type: "custom_tool_call_output", call_id: callId, output: text });
332
+ }
333
+ }
334
+ continue;
335
+ }
336
+ flushPendingSyntheticOutputs();
337
+ repaired.push(item);
338
+ }
339
+ flushPendingSyntheticOutputs();
340
+
341
+ const callKeyOf = (item: unknown): string | null => {
342
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
343
+ if (item.type === "function_call" || item.type === "local_shell_call") return `function:${item.call_id}`;
344
+ if (item.type === "custom_tool_call") return `custom:${item.call_id}`;
345
+ return null;
346
+ };
347
+ const outputKeyOf = (item: unknown): string | null => {
348
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
349
+ if (item.type === "function_call_output") return `function:${item.call_id}`;
350
+ if (item.type === "custom_tool_call_output") return `custom:${item.call_id}`;
351
+ return null;
352
+ };
353
+ const reorderBatchOutputs = (items: unknown[]): unknown[] => {
354
+ const ordered: unknown[] = [];
355
+ const claimedOutputIndexes = new Set<number>();
356
+ const outputIndexesByKey = new Map<string, { indexes: number[]; offset: number }>();
357
+ for (let outputIndex = 0; outputIndex < items.length; outputIndex += 1) {
358
+ const outputKey = outputKeyOf(items[outputIndex]);
359
+ if (outputKey === null) continue;
360
+ const bucket = outputIndexesByKey.get(outputKey);
361
+ if (bucket) bucket.indexes.push(outputIndex);
362
+ else outputIndexesByKey.set(outputKey, { indexes: [outputIndex], offset: 0 });
363
+ }
364
+ let index = 0;
365
+ while (index < items.length) {
366
+ if (claimedOutputIndexes.has(index)) { index += 1; continue; }
367
+ const key = callKeyOf(items[index]);
368
+ if (key === null) { ordered.push(items[index]); index += 1; continue; }
369
+ const batch: unknown[] = [];
370
+ const batchKeys: string[] = [];
371
+ let cursor = index;
372
+ while (cursor < items.length) {
373
+ const nextKey = callKeyOf(items[cursor]);
374
+ if (nextKey === null) break;
375
+ batch.push(items[cursor]);
376
+ batchKeys.push(nextKey);
377
+ cursor += 1;
378
+ }
379
+ const hasSynthetic = batchKeys.some(batchKey => syntheticKeys.has(batchKey));
380
+ if (!hasSynthetic) {
381
+ ordered.push(...batch);
382
+ index = cursor;
383
+ continue;
384
+ }
385
+ const batchOutputs: unknown[] = [];
386
+ for (const batchKey of batchKeys) {
387
+ const bucket = outputIndexesByKey.get(batchKey);
388
+ if (!bucket) continue;
389
+ while (bucket.offset < bucket.indexes.length && bucket.indexes[bucket.offset]! < cursor) {
390
+ bucket.offset += 1;
391
+ }
392
+ while (bucket.offset < bucket.indexes.length) {
393
+ const outputIndex = bucket.indexes[bucket.offset]!;
394
+ bucket.offset += 1;
395
+ if (claimedOutputIndexes.has(outputIndex)) continue;
396
+ claimedOutputIndexes.add(outputIndex);
397
+ batchOutputs.push(items[outputIndex]);
398
+ break;
399
+ }
400
+ }
401
+ ordered.push(...batch, ...batchOutputs);
402
+ index = cursor;
403
+ }
404
+ return ordered;
405
+ };
406
+
407
+ return changed ? { ...body, input: reorderBatchOutputs(repaired) } : body;
408
+ }
409
+
410
+ /**
411
+ * Make unambiguous Responses tool batches contiguous for upstream parsers that require it.
412
+ *
413
+ * [Decision Log]
414
+ * - 목적과 의도: Keep Codex hook-injected developer context without splitting a parallel tool-call turn away from its reasoning or making a strict upstream reject matching results.
415
+ * - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence, while the original pair-by-pair reorder turned `reasoning, call A, call B, output A, output B` into two assistant turns and made DeepSeek reject call B for missing reasoning (#1477).
416
+ * - 검토한 주요 대안: Disable parallel calls (DeepSeek always enables them); duplicate reasoning per call; reorder each pair; or normalize the complete unambiguous call batch.
417
+ * - 선택한 방식: Treat calls emitted before the first matched result as one batch, emit all calls followed by their matched outputs, and preserve intervening non-tool items immediately after the batch.
418
+ * - 다른 대안 대신 이 방식을 선택한 이유: Batch normalization matches the Responses parallel-call shape without fabricating reasoning, while the provider gate and unique-pair requirement keep the blast radius narrow.
419
+ * - 장점, 단점 및 영향: DeepSeek keeps one reasoning-bearing assistant turn for parallel calls and still accepts hook-interleaved single calls; tolerant providers stay byte/order equivalent, and duplicate, missing, or backwards call/result pairs are not guessed.
420
+ */
421
+ export function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
422
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
423
+ const input = body.input;
424
+ const calls = new Map<string, number[]>();
425
+ const outputs = new Map<string, number[]>();
426
+
427
+ const appendIndex = (map: Map<string, number[]>, key: string, index: number): void => {
428
+ const existing = map.get(key);
429
+ if (existing) existing.push(index);
430
+ else map.set(key, [index]);
431
+ };
432
+
433
+ for (let index = 0; index < input.length; index += 1) {
434
+ const item = input[index];
435
+ if (!isPlainObject(item) || typeof item.call_id !== "string" || item.call_id.length === 0) continue;
436
+ if (item.type === "function_call" || item.type === "local_shell_call") {
437
+ appendIndex(calls, `function:${item.call_id}`, index);
438
+ } else if (item.type === "custom_tool_call") {
439
+ appendIndex(calls, `custom:${item.call_id}`, index);
440
+ } else if (item.type === "function_call_output") {
441
+ appendIndex(outputs, `function:${item.call_id}`, index);
442
+ } else if (item.type === "custom_tool_call_output") {
443
+ appendIndex(outputs, `custom:${item.call_id}`, index);
444
+ }
445
+ }
446
+
447
+ const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
448
+ for (const [key, callIndices] of calls) {
449
+ const outputIndices = outputs.get(key);
450
+ if (!outputIndices) return body;
451
+ if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
452
+ const callIndex = callIndices[0]!;
453
+ const outputIndex = outputIndices[0]!;
454
+ if (outputIndex <= callIndex) return body;
455
+ pairs.push({ callIndex, outputIndex });
456
+ }
457
+ // Reject any collected output that lacks exactly one matching call. A lone or
458
+ // duplicated output is ambiguous, and normalizing on top of it could sever a
459
+ // result from the reasoning-bearing call turn it belongs to.
460
+ for (const [key, outputIndices] of outputs) {
461
+ const callIndices = calls.get(key);
462
+ if (!callIndices || callIndices.length !== 1 || outputIndices.length !== 1) return body;
463
+ }
464
+ pairs.sort((left, right) => left.callIndex - right.callIndex);
465
+
466
+ const movedIndices = new Set<number>();
467
+ const batchAt = new Map<number, unknown[]>();
468
+ for (let cursor = 0; cursor < pairs.length;) {
469
+ const group = [pairs[cursor]!];
470
+ let firstOutputIndex = pairs[cursor]!.outputIndex;
471
+ let next = cursor + 1;
472
+ while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) {
473
+ group.push(pairs[next]!);
474
+ firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex);
475
+ next += 1;
476
+ }
477
+
478
+ // Within one reasoning turn the outputs must appear in the same order as their
479
+ // calls. If they are reversed, normalizing would fabricate a new output order;
480
+ // leave the ambiguous history untouched instead.
481
+ for (let groupIndex = 1; groupIndex < group.length; groupIndex += 1) {
482
+ if (group[groupIndex]!.outputIndex < group[groupIndex - 1]!.outputIndex) return body;
483
+ }
484
+
485
+ const batch = [
486
+ ...group.map(pair => input[pair.callIndex]),
487
+ ...group.map(pair => input[pair.outputIndex]),
488
+ ];
489
+ const anchor = group[0]!.callIndex;
490
+ const alreadyContiguous = batch.every((item, offset) => input[anchor + offset] === item);
491
+ if (!alreadyContiguous) {
492
+ batchAt.set(anchor, batch);
493
+ for (const pair of group) {
494
+ movedIndices.add(pair.callIndex);
495
+ movedIndices.add(pair.outputIndex);
496
+ }
497
+ }
498
+ cursor = next;
499
+ }
500
+ if (batchAt.size === 0) return body;
501
+
502
+ const normalized: unknown[] = [];
503
+ for (let index = 0; index < input.length; index += 1) {
504
+ const batch = batchAt.get(index);
505
+ if (batch) normalized.push(...batch);
506
+ if (!movedIndices.has(index)) normalized.push(input[index]);
507
+ }
508
+ return { ...body, input: normalized };
509
+ }