@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-CsYvvpr3.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-BB0iHG8-.js +0 -86
@@ -1,5 +1,6 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
2
  import type { AdapterEvent } from "../../types";
3
+ import { repairFreeformToolInput } from "../../responses/apply-patch-envelope";
3
4
 
4
5
  const OPEN_TAG = "<tool_call>";
5
6
  const CLOSE_TAG = "</tool_call>";
@@ -27,9 +28,24 @@ export interface SerializedToolCall {
27
28
  end: number;
28
29
  }
29
30
 
31
+ /** A freeform tool's declared identity. Echoes are matched against it, never against the wire alias. */
32
+ export interface FreeformToolIdentity {
33
+ name: string;
34
+ namespace?: string;
35
+ }
36
+
37
+ /** The request's freeform tools keyed by the wire name the provider sees for each of them. */
38
+ export function freeformToolsByWireName<T extends FreeformToolIdentity & { freeform?: boolean }>(
39
+ tools: readonly T[] | undefined,
40
+ wireName: (tool: T) => string,
41
+ ): Map<string, FreeformToolIdentity> {
42
+ return new Map(tools?.filter(tool => tool.freeform).map(tool => [wireName(tool), { name: tool.name, namespace: tool.namespace }] as const) ?? []);
43
+ }
44
+
30
45
  export interface StructuredToolCallReference {
31
46
  names: ReadonlySet<string>;
32
47
  argumentsText: string;
48
+ freeformTool?: FreeformToolIdentity;
33
49
  }
34
50
 
35
51
  const BLOCK_HEADER = /<tool_call>\s*<function=([^>\r\n]+)>/y;
@@ -176,6 +192,29 @@ function contextAfter(text: string, context: TextContext): TextContext {
176
192
  return splitAtPossibleSerializedToolCall(text.replaceAll(OPEN_TAG, "<tool-call>"), context, true).context;
177
193
  }
178
194
 
195
+ /**
196
+ * A deferred header/fence can itself be long. While its unresolved portion only grows, a
197
+ * chunk-local check suffices; the first resolving character goes back through the full splitter.
198
+ * Each unbounded phase is scanned once on entry and once on exit, preserving its Markdown rules.
199
+ */
200
+ function deferredContinuationBreak(text: string, context: TextContext): RegExp | undefined {
201
+ if (text.startsWith(OPEN_TAG)) {
202
+ if (/^<tool_call>\s*$/.test(text)) return /\S/;
203
+ if (/^<tool_call>\s*<function=[^>\r\n]*$/.test(text)) return /[>\r\n]/;
204
+ }
205
+ if (!context.inlineTicks && context.lineStart) {
206
+ const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/.exec(text);
207
+ if (fence) {
208
+ if (!context.fence) return /\n/;
209
+ // Closing-fence ticks may grow only before its optional whitespace suffix.
210
+ if (!fence[2]) return fence[1]![0] === "`" ? /[^`]/ : /[^~]/;
211
+ return /[^ \t\r]/;
212
+ }
213
+ }
214
+ if (/^`+$/.test(text)) return /[^`]/;
215
+ return undefined;
216
+ }
217
+
179
218
  /**
180
219
  * Holds possible duplicate text within the shared translator budget until the dispatch outcome is
181
220
  * known. While a block candidate is open, any other event (reasoning) is queued at its position in
@@ -186,11 +225,30 @@ export class SerializedToolCallContentBuffer {
186
225
  private text = "";
187
226
  private bytes = 0;
188
227
  private hasOpenTag = false;
228
+ private continuationBreak: RegExp | undefined;
229
+ private delimiterSuffix = "";
230
+ private sawCloser = false;
231
+ private openAfterCloser = false;
232
+ private trailingProseChars = 0;
189
233
  private context: TextContext = { fence: null, lineStart: true };
190
234
  private queued: { offset: number; event: AdapterEvent }[] = [];
191
235
 
192
236
  constructor(private readonly budget: TranslatorBudget) {}
193
237
 
238
+ /** Measures an append exactly even when the runtime prices isolated UTF-16 surrogates differently. */
239
+ private appendedByteLength(delta: string): number {
240
+ let bytes = Buffer.byteLength(delta);
241
+ if (this.text.length === 0 || delta.length === 0) return bytes;
242
+ const tail = this.text[this.text.length - 1]!;
243
+ const head = delta[0]!;
244
+ const tailCode = tail.charCodeAt(0);
245
+ const headCode = head.charCodeAt(0);
246
+ if (tailCode >= 0xd800 && tailCode <= 0xdbff && headCode >= 0xdc00 && headCode <= 0xdfff) {
247
+ bytes += Buffer.byteLength(tail + head) - Buffer.byteLength(tail) - Buffer.byteLength(head);
248
+ }
249
+ return bytes;
250
+ }
251
+
194
252
  /** Reserves the replacement before releasing the old text, preserving it if the budget rejects growth. */
195
253
  private replace(next: string, hasOpenTag: boolean): void {
196
254
  const nextBytes = Buffer.byteLength(next);
@@ -201,6 +259,12 @@ export class SerializedToolCallContentBuffer {
201
259
  this.text = next;
202
260
  this.bytes = nextBytes;
203
261
  this.hasOpenTag = hasOpenTag;
262
+ this.continuationBreak = undefined;
263
+ this.delimiterSuffix = "";
264
+ this.sawCloser = false;
265
+ this.openAfterCloser = false;
266
+ this.trailingProseChars = 0;
267
+ if (hasOpenTag) this.observeDelimiters(next);
204
268
  } catch (error) {
205
269
  reservation.release();
206
270
  throw error;
@@ -209,21 +273,41 @@ export class SerializedToolCallContentBuffer {
209
273
 
210
274
  /** Charges only the appended bytes, so holding an open block never needs twice its retained size. */
211
275
  private append(delta: string): void {
212
- const deltaBytes = Buffer.byteLength(delta);
276
+ const deltaBytes = this.appendedByteLength(delta);
213
277
  this.budget.reserveTransient(deltaBytes, { kind: "live_transient" }).commitRetained();
278
+ if (this.hasOpenTag) this.observeDelimiters(delta);
214
279
  this.text += delta;
215
280
  this.bytes += deltaBytes;
216
281
  }
217
282
 
283
+ /** Scan new text plus a fixed overlap, never the retained body (which may be a large rope). */
284
+ private observeDelimiters(delta: string): void {
285
+ const scan = this.delimiterSuffix + delta;
286
+ const closer = scan.lastIndexOf(CLOSE_TAG);
287
+ const afterCloser = closer + CLOSE_TAG.length;
288
+ if (closer >= 0) {
289
+ this.sawCloser = true;
290
+ this.openAfterCloser = false;
291
+ this.trailingProseChars = 0;
292
+ }
293
+ if (scan.lastIndexOf(OPEN_TAG) >= (closer >= 0 ? afterCloser : 0)) {
294
+ this.openAfterCloser = true;
295
+ }
296
+ const tail = closer >= 0 ? scan.slice(afterCloser) : delta;
297
+ if (this.sawCloser) this.trailingProseChars += tail.replace(/\s+/g, "").length;
298
+ this.delimiterSuffix = scan.slice(-(Math.max(OPEN_TAG.length, CLOSE_TAG.length) - 1));
299
+ }
300
+
218
301
  /** Returns immediately safe text and retains only the suffix that still needs reconciliation. */
219
302
  ingest(delta: string): string {
220
- if (this.hasOpenTag) {
303
+ if (this.hasOpenTag || (this.continuationBreak && !this.continuationBreak.test(delta))) {
221
304
  this.append(delta);
222
305
  return "";
223
306
  }
224
307
  const split = splitAtPossibleSerializedToolCall(this.text + delta, this.context);
225
308
  this.replace(split.defer, split.hasOpenTag);
226
309
  this.context = split.context;
310
+ if (!split.hasOpenTag) this.continuationBreak = deferredContinuationBreak(split.defer, split.context);
227
311
  return split.emit;
228
312
  }
229
313
 
@@ -234,11 +318,11 @@ export class SerializedToolCallContentBuffer {
234
318
  * context. The size bound is checked before the delta is retained.
235
319
  */
236
320
  ingestStreaming(delta: string): AdapterEvent[] {
237
- const deltaBytes = Buffer.byteLength(delta);
321
+ const deltaBytes = this.appendedByteLength(delta);
238
322
  if (this.hasOpenTag && this.bytes + deltaBytes > MAX_HELD_BYTES) {
239
323
  const released = this.drain([]);
240
324
  // A delta that alone passes the bound is delivered as text rather than retained.
241
- if (deltaBytes > MAX_HELD_BYTES) {
325
+ if (Buffer.byteLength(delta) > MAX_HELD_BYTES) {
242
326
  this.context = contextAfter(delta, this.context);
243
327
  return [...released, ...textEvents(delta)];
244
328
  }
@@ -246,7 +330,7 @@ export class SerializedToolCallContentBuffer {
246
330
  }
247
331
  const text = this.ingest(delta);
248
332
  // Checked after ingest too: one delta can open a block and already carry more than a bound.
249
- if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || proseAfterClosedBlock(this.text) > MAX_TRAILING_CHARS)) {
333
+ if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || (this.sawCloser && !this.openAfterCloser && this.trailingProseChars > MAX_TRAILING_CHARS))) {
250
334
  return [...textEvents(text), ...this.drain([])];
251
335
  }
252
336
  return textEvents(text);
@@ -318,20 +402,30 @@ export class SerializedToolCallContentBuffer {
318
402
  this.text = "";
319
403
  this.bytes = 0;
320
404
  this.hasOpenTag = false;
405
+ this.continuationBreak = undefined;
321
406
  this.queued = [];
322
407
  }
323
408
  }
324
409
 
325
- /** Reads only a string input from a JSON object; other argument shapes cannot prove duplication. */
326
- function inputFromArguments(argumentsText: string): string | undefined {
410
+ /** Reads a wrapped input or raw arguments from a declared freeform tool; neither path rewrites them. */
411
+ function inputFromArguments(
412
+ argumentsText: string,
413
+ freeformTool?: FreeformToolIdentity,
414
+ ): string | undefined {
327
415
  try {
328
416
  const parsed = JSON.parse(argumentsText) as unknown;
329
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
330
- const input = (parsed as Record<string, unknown>).input;
331
- return typeof input === "string" ? input : undefined;
417
+ const input = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
418
+ ? (parsed as Record<string, unknown>).input
419
+ : undefined;
420
+ if (typeof input === "string") return input;
332
421
  } catch {
333
- return undefined;
422
+ // Chat gateways can send custom-tool input as raw text.
334
423
  }
424
+ if (!freeformTool) return undefined;
425
+ // Match raw input only when the Responses bridge dispatches these same bytes. It can unwrap
426
+ // alternate fields or repair freeform input using the declared tool's name and namespace.
427
+ return repairFreeformToolInput(argumentsText, freeformTool.name, freeformTool.namespace) === argumentsText
428
+ ? argumentsText : undefined;
335
429
  }
336
430
 
337
431
  /** One wrapping newline after the function header is template layout, not input (vLLM `_trim_wrapping_newlines`). */
@@ -348,10 +442,20 @@ function agreesWithRepeatedBlock(
348
442
  structured: StructuredToolCallReference,
349
443
  repeated: SerializedToolCall,
350
444
  ): boolean {
351
- const input = structured.names.has(repeated.name) ? inputFromArguments(structured.argumentsText) : undefined;
445
+ const input = structured.names.has(repeated.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
352
446
  return input !== undefined && freeformBody(input) === freeformBody(repeated.body);
353
447
  }
354
448
 
449
+ /** A second call can explain a repeated pair even when its arguments cannot safely be rewritten. */
450
+ function hasDoubledInput(structured: StructuredToolCallReference, repeated: SerializedToolCall): boolean {
451
+ if (!structured.names.has(repeated.name)) return false;
452
+ const input = inputFromArguments(structured.argumentsText, structured.freeformTool);
453
+ if (input === undefined) return false;
454
+ const body = freeformBody(repeated.body);
455
+ const normalized = freeformBody(input);
456
+ return normalized === body + body || normalized === body + "\n" + body;
457
+ }
458
+
355
459
  /** The `[start, end)` ranges of blocks whose function identity and freeform input match a dispatched call. */
356
460
  function duplicatedSerializedToolCallRanges(
357
461
  text: string,
@@ -361,14 +465,16 @@ function duplicatedSerializedToolCallRanges(
361
465
  if (structuredCalls.length === 0) return [];
362
466
  const repeated = repeatedCallIn(text, context);
363
467
  if (repeated) {
364
- // Without a single agreeing call the pair is ambiguous, so no shape of it is suppressed.
468
+ // A doubled call beside an agreeing call leaves the pair ambiguous. If reduction was
469
+ // refused, keep the markup too: otherwise the visible text and executable call disagree.
365
470
  const matching = structuredCalls.filter(structured => agreesWithRepeatedBlock(structured, repeated));
366
- return matching.length === 1 ? [{ start: repeated.start, end: repeated.end }] : [];
471
+ const doubled = structuredCalls.some(structured => structured !== matching[0] && hasDoubledInput(structured, repeated));
472
+ return matching.length === 1 && !doubled ? [{ start: repeated.start, end: repeated.end }] : [];
367
473
  }
368
474
  return callsIn(text, context).filter(call => {
369
475
  const body = freeformBody(call.body);
370
476
  return structuredCalls.some(structured => {
371
- const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText) : undefined;
477
+ const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
372
478
  return input !== undefined && freeformBody(input) === body;
373
479
  });
374
480
  });
@@ -458,16 +564,16 @@ export interface StructuredToolCallInput {
458
564
  wireName: string;
459
565
  restoredName: string;
460
566
  argumentsText: string;
567
+ freeformTool?: FreeformToolIdentity;
461
568
  }
462
569
 
463
570
  /**
464
571
  * Repairs the arguments of every structured call in one response against the visible text that
465
572
  * response carried, and returns them in input order. The per-call prefix repair stands alone,
466
573
  * because the markup it proves is matched against that call's own repaired input. The
467
- * doubled-input reduction is applied only when exactly ONE call in the batch qualifies: it either
468
- * carries the doubled shape or already agrees with the repeated block body. It rewrites executable
469
- * arguments, and a second qualifying call leaves the block ambiguous, so the uniqueness proof has
470
- * to cover the whole batch rather than one call at a time.
574
+ * doubled-input reduction is applied only when exactly one call in the batch explains the repeated
575
+ * pair, including doubled shapes that cannot themselves be rewritten. It rewrites executable
576
+ * arguments, so a second explanatory call leaves the pair ambiguous.
471
577
  */
472
578
  export function reconcileStructuredToolCalls(
473
579
  calls: readonly StructuredToolCallInput[],
@@ -475,7 +581,7 @@ export function reconcileStructuredToolCalls(
475
581
  ): StructuredToolCallReference[] {
476
582
  const references = calls.map(call => {
477
583
  const names = new Set([call.wireName, call.restoredName]);
478
- return { names, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
584
+ return { names, freeformTool: call.freeformTool, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
479
585
  });
480
586
  return reduceUnambiguousDoubledInput(references, serializedText);
481
587
  }
@@ -486,9 +592,8 @@ export function reconcileStructuredToolCalls(
486
592
  * alone. A call whose input already equals the repeated body is a competing explanation, not a
487
593
  * bystander: both readings account for the pair and the response never picks one, so a batch with
488
594
  * two qualifying calls keeps every argument exactly as sent. The reduction then rewrites nothing,
489
- * and the markup is left to the range matcher, which suppresses the pair only when exactly one
490
- * call already agrees. A lone qualifying call is always the doubled one, because a call that
491
- * already agrees leaves nothing to reduce.
595
+ * and the range matcher keeps the markup when a doubled call competes with an agreeing call.
596
+ * An already-agreeing call leaves nothing to reduce.
492
597
  */
493
598
  function reduceUnambiguousDoubledInput(
494
599
  references: readonly StructuredToolCallReference[],
@@ -499,13 +604,14 @@ function reduceUnambiguousDoubledInput(
499
604
  const candidates = references.map(reference => ({
500
605
  reduction: doubledInputReduction(reference.argumentsText, reference.names, repeated),
501
606
  explains: agreesWithRepeatedBlock(reference, repeated),
607
+ doubled: hasDoubledInput(reference, repeated),
502
608
  }));
503
- if (candidates.filter(candidate => candidate.explains || candidate.reduction !== undefined).length !== 1) {
609
+ if (candidates.filter(candidate => candidate.explains || candidate.doubled).length !== 1) {
504
610
  return [...references];
505
611
  }
506
612
  return references.map((reference, index) => {
507
613
  const reduction = candidates[index]!.reduction;
508
- return reduction === undefined ? reference : { names: reference.names, argumentsText: reduction };
614
+ return reduction === undefined ? reference : { ...reference, argumentsText: reduction };
509
615
  });
510
616
  }
511
617
 
@@ -558,12 +664,3 @@ export function reconcileSerializedToolCallEvents(
558
664
  function textEvents(text: string): AdapterEvent[] {
559
665
  return text.length > 0 ? [{ type: "text_delta", text }] : [];
560
666
  }
561
-
562
- /** Non-whitespace characters after the last closed block, or 0 while a later block is still open. */
563
- function proseAfterClosedBlock(text: string): number {
564
- const closer = text.lastIndexOf(CLOSE_TAG);
565
- if (closer < 0) return 0;
566
- const tail = text.slice(closer + CLOSE_TAG.length);
567
- if (tail.includes(OPEN_TAG)) return 0;
568
- return tail.replace(/\s+/g, "").length;
569
- }
@@ -45,7 +45,7 @@ import { messagesToChatFormat } from "./openai-chat/messages";
45
45
  import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
46
46
  import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
47
47
  import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
48
- import { reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content";
48
+ import { freeformToolsByWireName, type FreeformToolIdentity, reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content";
49
49
 
50
50
  export { stripBracketedModelSuffix } from "./openai-chat/wire";
51
51
  export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
@@ -93,6 +93,7 @@ function canSerializeOpenAIChatServiceTier(
93
93
 
94
94
  export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
95
95
  let lastRequestedModelId: string | undefined;
96
+ let freeformTools = new Map<string, FreeformToolIdentity>();
96
97
  return withOpenAIChatToolNames(toolNames => ({
97
98
  name: "openai-chat",
98
99
 
@@ -102,6 +103,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
102
103
  lastRequestedModelId = parsed.modelId;
103
104
  const { url, headers, hasCredential } = openAIChatTransport(provider);
104
105
  const messages = toolNames.messages(parsed, provider.baseUrl, messagesToChatFormat(parsed, provider));
106
+ freeformTools = freeformToolsByWireName(parsed.context.tools, tool => toolNames.registry().alias(tool));
105
107
  const finish = (): AdapterRequest => {
106
108
  const tools = toolsToChatFormatForProvider(parsed, provider, toolNames.registry());
107
109
  const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider, toolNames.registry());
@@ -338,7 +340,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
338
340
  }
339
341
  }
340
342
  // Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
341
- const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args })), toolCallContent.current());
343
+ const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current());
342
344
  calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
343
345
  yield* toolCallContent.drain(references);
344
346
  for (const call of calls) {
@@ -773,7 +775,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
773
775
  const contentEnd = events.length;
774
776
  const answerText = events.slice(contentStart).map(event => (event.type === "text_delta" ? event.text : "")).join("");
775
777
  // Each call holds the delta event it emitted, so the batch repair sets its arguments later.
776
- const structuredCalls: { wireName: string; restoredName: string; argumentsText: string; delta: Extract<AdapterEvent, { type: "tool_call_delta" }> }[] = [];
778
+ const structuredCalls: { wireName: string; restoredName: string; argumentsText: string; freeformTool?: FreeformToolIdentity; delta: Extract<AdapterEvent, { type: "tool_call_delta" }> }[] = [];
777
779
  const rawToolCalls = msg.tool_calls;
778
780
  if (rawToolCalls !== undefined && rawToolCalls !== null) {
779
781
  if (!Array.isArray(rawToolCalls)) {
@@ -797,7 +799,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
797
799
  return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
798
800
  }
799
801
  const delta: Extract<AdapterEvent, { type: "tool_call_delta" }> = { type: "tool_call_delta", arguments: args };
800
- structuredCalls.push({ wireName: name, restoredName: toolNames.restore(name), argumentsText: args, delta });
802
+ structuredCalls.push({ wireName: name, restoredName: toolNames.restore(name), argumentsText: args, freeformTool: freeformTools.get(name), delta });
801
803
  events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" });
802
804
  }
803
805
  }
@@ -474,6 +474,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
474
474
  dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
475
475
  stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
476
476
  dropForeignItemId: parsed._dropForeignReasoningItemIds === true,
477
+ requirePlaintextReasoning: requiresPlaintextReasoningReplay(provider),
477
478
  },
478
479
  ),
479
480
  provider,
@@ -11,6 +11,37 @@ export function dropResponsesReasoningInputItems(body: unknown): unknown {
11
11
  return input.length === body.input.length ? body : { ...body, input };
12
12
  }
13
13
 
14
+ /** Whether the item already carries a non-empty `reasoning_text` content part. */
15
+ function hasPlaintextReasoningContent(content: unknown): boolean {
16
+ return Array.isArray(content) && content.some(part =>
17
+ isPlainObject(part)
18
+ && part.type === "reasoning_text"
19
+ && typeof part.text === "string"
20
+ && part.text.length > 0
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Backfill for a reasoning item that would reach a plaintext-required wire with no
26
+ * `reasoning_text`: its text lived only in ciphertext the provider cannot read and was
27
+ * stripped, so replay the item's `summary_text` as `reasoning_text` — real model output,
28
+ * unlike a fabricated chain — and fall back to the same minimal placeholder the chat
29
+ * adapter emits for `requiresReasoningPlaceholderModels` (#5421). DeepSeek's thinking
30
+ * mode answers a bare emptied item with `The reasoning_text in the thinking mode must
31
+ * be passed back to the API` and aborts the turn.
32
+ */
33
+ function plaintextReasoningBackfill(rec: Record<string, unknown>): unknown[] {
34
+ const fromSummary = Array.isArray(rec.summary)
35
+ ? rec.summary
36
+ .filter(part => isPlainObject(part)
37
+ && part.type === "summary_text"
38
+ && typeof part.text === "string"
39
+ && part.text.length > 0)
40
+ .map(part => ({ type: "reasoning_text", text: part.text }))
41
+ : [];
42
+ return fromSummary.length > 0 ? fromSummary : [{ type: "reasoning_text", text: " " }];
43
+ }
44
+
14
45
  /**
15
46
  * Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
16
47
  * native `encrypted_content` guarantees only that blob value: `status` is always removed;
@@ -40,6 +71,13 @@ export function sanitizeReasoningInputContent(
40
71
  * store this destination cannot read; see `OcxParsedRequest._dropForeignReasoningItemIds`.
41
72
  */
42
73
  dropForeignItemId?: boolean;
74
+ /**
75
+ * The destination documents a strict plaintext-replay contract (DeepSeek's Responses
76
+ * thinking mode): every reasoning input item must carry `reasoning_text` content.
77
+ * An item left with none is backfilled from its summary, or with a minimal
78
+ * placeholder, rather than emitted in the shape the upstream 400s on.
79
+ */
80
+ requirePlaintextReasoning?: boolean;
43
81
  },
44
82
  ): unknown {
45
83
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
@@ -85,9 +123,14 @@ export function sanitizeReasoningInputContent(
85
123
  const blankContent = !dropNullContentChannel
86
124
  && !opts?.preserveRawReasoningContent
87
125
  && (hasRawContent || hasOcxEnvelope);
126
+ // A strict plaintext-replay destination cannot consume a reasoning item emptied of its
127
+ // `reasoning_text` — the resumed-subagent shape (#5421), where the text lived only in
128
+ // ciphertext the provider cannot read. Backfill after every other strip has run.
129
+ const missingPlaintextReasoning = opts?.requirePlaintextReasoning === true
130
+ && !hasPlaintextReasoningContent(rec.content);
88
131
  if (
89
132
  !blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel
90
- && !missingSummary && !dropItemId
133
+ && !missingSummary && !dropItemId && !missingPlaintextReasoning
91
134
  ) {
92
135
  return item;
93
136
  }
@@ -106,6 +149,7 @@ export function sanitizeReasoningInputContent(
106
149
  // `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
107
150
  // continuations after tool calls (issue #875 family).
108
151
  if (blankContent) next.content = [];
152
+ if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
109
153
  return next;
110
154
  });
111
155
 
@@ -49,7 +49,13 @@ import { bridgeToResponsesSSE } from "./sse";
49
49
  export function buildResponseJSON(
50
50
  events: AdapterEvent[],
51
51
  modelId: string,
52
- options?: Parameters<typeof buildResponseJSONWithBudget>[2],
52
+ options?: Parameters<typeof buildResponseJSONWithBudget>[2] & {
53
+ /**
54
+ * False when the body is not what the client receives: a direct client encoder counts its
55
+ * own relayed frames and folds the same events here only for the completion effects.
56
+ */
57
+ recordBufferedDelivery?: boolean;
58
+ },
53
59
  ): Record<string, unknown> {
54
60
  // Default-budget safety net: a caller that omits the budget gets a bounded
55
61
  // default (disposed with the call), never the unbounded append path.
@@ -58,7 +64,9 @@ export function buildResponseJSON(
58
64
  // A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
59
65
  // recorder on the SSE bridge. Without this the attempt would persist adapter events with
60
66
  // zero relayed ones, which is the loss signal -- raised on every non-streaming request.
61
- attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
67
+ if (options.recordBufferedDelivery !== false) {
68
+ attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
69
+ }
62
70
  return body;
63
71
  }
64
72
  const budget = createTranslatorBudget();