@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-EJxiUFMq.js +0 -50
  259. package/gui/dist/assets/index-BmJwNBHL.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -198,6 +198,37 @@ const MOONSHOT_MAX_REF_EXPANSIONS = 512;
198
198
  */
199
199
  const MOONSHOT_MAX_SCHEMA_DEPTH = 64;
200
200
  const MOONSHOT_MAX_SCHEMA_NODES = 4_096;
201
+ const MOONSHOT_MAX_INLINED_SCHEMA_BYTES = 1024 * 1024;
202
+
203
+ /**
204
+ * Measure only as far as the caller's remaining allowance. Keeping this iterative avoids
205
+ * reintroducing the deep-schema stack exhaustion that the normalizer's depth limit prevents.
206
+ */
207
+ function serializedJsonBytesUpTo(value: unknown, limit: number): number {
208
+ const encoder = new TextEncoder();
209
+ const pending: unknown[] = [value];
210
+ let bytes = 0;
211
+ while (pending.length > 0 && bytes <= limit) {
212
+ const item = pending.pop();
213
+ if (Array.isArray(item)) {
214
+ bytes += 2 + Math.max(0, item.length - 1);
215
+ for (const child of item) pending.push(child);
216
+ continue;
217
+ }
218
+ if (isXaiObjectSchema(item)) {
219
+ const entries = Object.entries(item);
220
+ bytes += 2 + Math.max(0, entries.length - 1);
221
+ for (const [key, child] of entries) {
222
+ bytes += encoder.encode(JSON.stringify(key)).byteLength + 1;
223
+ pending.push(child);
224
+ }
225
+ continue;
226
+ }
227
+ const encoded = JSON.stringify(item);
228
+ bytes += encoder.encode(encoded === undefined ? "null" : encoded).byteLength;
229
+ }
230
+ return bytes;
231
+ }
201
232
 
202
233
  /**
203
234
  * Assertion keywords whose meaning under a `$ref` is CONJUNCTION, not replacement. A node
@@ -309,8 +340,19 @@ function composeProperties(
309
340
  return combined;
310
341
  }
311
342
 
343
+ /**
344
+ * The inline-byte allowance for one request. Sharing it across tools matters: a per-tool
345
+ * budget would let a large catalog multiply the cap by its tool count, reintroducing the
346
+ * request amplification this bound exists to prevent.
347
+ */
348
+ interface MoonshotInlineByteBudget {
349
+ remaining: number;
350
+ }
351
+
312
352
  interface MoonshotNormalizeState {
313
353
  activeRefs: Set<string>;
354
+ inlineSizeCache: WeakMap<Record<string, unknown>, number>;
355
+ inlineByteBudget: MoonshotInlineByteBudget;
314
356
  remainingExpansions: number;
315
357
  remainingNodes: number;
316
358
  }
@@ -342,10 +384,37 @@ function normalizeMoonshotSchemaNode(
342
384
 
343
385
  const target = lookupLocalJsonPointer(root, ref);
344
386
  if (isXaiObjectSchema(target)) {
387
+ // Charge the referenced value before copying it. Object/node counts do not cover large
388
+ // maps of boolean schemas, which otherwise allow a small input to create hundreds of
389
+ // full copies before the final request is serialized.
390
+ let inlineBytes = state.inlineSizeCache.get(target);
391
+ if (inlineBytes === undefined) {
392
+ inlineBytes = serializedJsonBytesUpTo(target, MOONSHOT_MAX_INLINED_SCHEMA_BYTES);
393
+ state.inlineSizeCache.set(target, inlineBytes);
394
+ }
395
+ if (inlineBytes > state.inlineByteBudget.remaining) return { $ref: ref };
396
+ const bytesBefore = state.inlineByteBudget.remaining;
397
+ const expansionsBefore = state.remainingExpansions;
398
+ const nodesBefore = state.remainingNodes;
399
+ state.inlineByteBudget.remaining -= inlineBytes;
345
400
  state.remainingExpansions -= 1;
346
401
  state.activeRefs.add(ref);
347
402
  const resolvedTarget = normalizeMoonshotSchemaNode(target, root, state, depth + 1);
348
403
  state.activeRefs.delete(ref);
404
+ // Nested copies have already spent from the shared allowance. Charge only
405
+ // growth that their own charges do not cover.
406
+ const nestedCharges = bytesBefore - inlineBytes - state.inlineByteBudget.remaining;
407
+ const normalizedBytes = serializedJsonBytesUpTo(
408
+ resolvedTarget, bytesBefore,
409
+ );
410
+ const growthBytes = Math.max(0, normalizedBytes - inlineBytes - nestedCharges);
411
+ if (growthBytes > state.inlineByteBudget.remaining) {
412
+ state.inlineByteBudget.remaining = bytesBefore;
413
+ state.remainingExpansions = expansionsBefore;
414
+ state.remainingNodes = nodesBefore;
415
+ return { $ref: ref };
416
+ }
417
+ state.inlineByteBudget.remaining -= growthBytes;
349
418
  const merged: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
350
419
  if (isXaiObjectSchema(resolvedTarget)) {
351
420
  for (const [key, value] of Object.entries(resolvedTarget)) merged[key] = value;
@@ -380,6 +449,20 @@ function normalizeMoonshotSchemaNode(
380
449
  }
381
450
  merged[key] = normalized;
382
451
  }
452
+
453
+ // Re-normalize only composed properties that retain a $ref alongside sibling keywords
454
+ if (isXaiObjectSchema(merged.properties)) {
455
+ for (const [propName, propVal] of Object.entries(merged.properties as Record<string, unknown>)) {
456
+ if (isXaiObjectSchema(propVal) && typeof propVal.$ref === "string" && moonshotRefTargetKeys(propVal).length > 0) {
457
+ (merged.properties as Record<string, unknown>)[propName] = normalizeMoonshotSchemaNode(
458
+ propVal,
459
+ root,
460
+ state,
461
+ depth + 1,
462
+ );
463
+ }
464
+ }
465
+ }
383
466
  return merged;
384
467
  }
385
468
 
@@ -397,13 +480,49 @@ function normalizeMoonshotSchemaNode(
397
480
  ? value
398
481
  : normalizeMoonshotSchemaNode(value, root, state, depth + 1);
399
482
  }
483
+
484
+ // Moonshot MFJS requirements:
485
+ // 1. Stamp "object" if properties are present, or if allOf defines object properties/variants,
486
+ // so Moonshot's validator recognizes the schema as a valid termination condition.
487
+ // 2. Infer scalar types for bare const and enum keywords.
488
+ if (out.type === undefined) {
489
+ const isObjectAllOf = Array.isArray(out.allOf) && out.allOf.some(
490
+ variant => isXaiObjectSchema(variant) && (
491
+ variant.type === "object" ||
492
+ variant.properties !== undefined ||
493
+ variant.additionalProperties !== undefined
494
+ ),
495
+ );
496
+ if (out.properties !== undefined || out.additionalProperties !== undefined || isObjectAllOf) {
497
+ out.type = "object";
498
+ } else if (out.const !== undefined) {
499
+ const t = typeof out.const;
500
+ if (t === "string" || t === "number" || t === "boolean") {
501
+ out.type = t;
502
+ }
503
+ } else if (Array.isArray(out.enum) && out.enum.length > 0) {
504
+ if (out.enum.every(x => typeof x === "string")) {
505
+ out.type = "string";
506
+ } else if (out.enum.every(x => typeof x === "number")) {
507
+ out.type = "number";
508
+ } else if (out.enum.every(x => typeof x === "boolean")) {
509
+ out.type = "boolean";
510
+ }
511
+ }
512
+ }
513
+
400
514
  return out;
401
515
  }
402
516
 
403
- function normalizeMoonshotToolParameters(parameters: unknown): Record<string, unknown> {
517
+ function normalizeMoonshotToolParameters(
518
+ parameters: unknown,
519
+ inlineByteBudget: MoonshotInlineByteBudget,
520
+ ): Record<string, unknown> {
404
521
  const rooted = ensureRootObjectType(parameters);
405
522
  const normalized = normalizeMoonshotSchemaNode(rooted, rooted, {
406
523
  activeRefs: new Set<string>(),
524
+ inlineSizeCache: new WeakMap<Record<string, unknown>, number>(),
525
+ inlineByteBudget,
407
526
  remainingExpansions: MOONSHOT_MAX_REF_EXPANSIONS,
408
527
  remainingNodes: MOONSHOT_MAX_SCHEMA_NODES,
409
528
  });
@@ -420,11 +539,14 @@ export function toolsToChatFormat(
420
539
  if (tools.length === 0) return undefined;
421
540
  const xaiTarget = isXaiSchemaTarget(provider);
422
541
  const moonshotTarget = !xaiTarget && isMoonshotSchemaTarget(provider);
542
+ const moonshotInlineByteBudget: MoonshotInlineByteBudget = {
543
+ remaining: MOONSHOT_MAX_INLINED_SCHEMA_BYTES,
544
+ };
423
545
  const formatted = tools.flatMap(t => {
424
546
  const normalized = xaiTarget
425
547
  ? normalizeXaiToolParameters(t.parameters)
426
548
  : moonshotTarget
427
- ? normalizeMoonshotToolParameters(t.parameters)
549
+ ? normalizeMoonshotToolParameters(t.parameters, moonshotInlineByteBudget)
428
550
  : ensureRootObjectType(t.parameters);
429
551
  const parameters = stripUnicodePropertyPatterns(stripResponsesOnlyEncryptedMarker(normalized));
430
552
 
@@ -4,6 +4,7 @@ import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wi
4
4
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
5
5
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
6
6
  import { modelInList } from "../types";
7
+ import { createInlineThinkContentSplitter, splitInlineThinkContent } from "./inline-think-tags";
7
8
  import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
8
9
  import { debugProviderDiagnostic } from "../lib/debug";
9
10
  import { sseFieldValue } from "../lib/sse-decoder";
@@ -355,6 +356,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
355
356
  // Gate on the routed model, not list length: a mixed openai-chat provider
356
357
  // can list MiniMax ids without putting every sibling on MiniMax semantics.
357
358
  const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? "");
359
+ // A gateway with no server-side reasoning parser leaves thinking inline in `content` as
360
+ // <think> blocks, which would otherwise render as the answer. Passthrough unless opted in.
361
+ const inlineThink = createInlineThinkContentSplitter(provider.inlineThinkTagModels, lastRequestedModelId, budget);
362
+ const emitContent = function* (events: AdapterEvent[]): Generator<AdapterEvent> {
363
+ for (const event of events) { if (event.type === "text_delta") sawUserFacingOutput = true; yield event; }
364
+ };
358
365
 
359
366
  const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
360
367
  const rawPayload = sseFieldValue(line, "data");
@@ -362,6 +369,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
362
369
  const payload = rawPayload.trim();
363
370
  if (payload.length === 0) return "continue";
364
371
  if (payload === "[DONE]") {
372
+ yield* emitContent(inlineThink.flush());
365
373
  if ((yield* flushToolCalls()) === "terminate") return "terminate";
366
374
  const stopReason = stopReasonFor(finishReason);
367
375
  yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
@@ -422,8 +430,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
422
430
  if (reasoningText !== undefined) yield { type: "reasoning_raw_delta", text: reasoningText };
423
431
  }
424
432
  if (typeof delta.content === "string" && delta.content.length > 0) {
425
- sawUserFacingOutput = true;
426
- yield { type: "text_delta", text: delta.content };
433
+ yield* emitContent(inlineThink.feed(delta.content));
427
434
  }
428
435
 
429
436
  const rawToolCalls = delta.tool_calls;
@@ -562,6 +569,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
562
569
  }
563
570
 
564
571
  if (typeof choice.finish_reason === "string" && choice.finish_reason) {
572
+ yield* emitContent(inlineThink.flush());
565
573
  if ((yield* flushToolCalls()) === "terminate") return "terminate";
566
574
  }
567
575
  return "continue";
@@ -605,6 +613,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
605
613
  if (buffer.length > 0) {
606
614
  if ((yield* handleDataLine(buffer)) === "terminate") return;
607
615
  }
616
+ yield* emitContent(inlineThink.flush());
608
617
  const sawFinish = finishReason !== undefined;
609
618
  if (!sawFinish && pendingToolCalls.length > 0) {
610
619
  // Some OpenAI-compatible gateways close immediately after a complete function-call
@@ -652,6 +661,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
652
661
  } finally {
653
662
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
654
663
  reasoningDetailTracker.release();
664
+ inlineThink.dispose();
655
665
  closeToolCalls();
656
666
  reader.releaseLock();
657
667
  }
@@ -738,7 +748,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
738
748
  if (segments.length > 0) reasoningText = segments.map(s => s.text).join("");
739
749
  }
740
750
  if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
741
- if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });
751
+ if (typeof msg.content === "string") events.push(...splitInlineThinkContent(provider.inlineThinkTagModels, lastRequestedModelId, budget, msg.content));
742
752
  const rawToolCalls = msg.tool_calls;
743
753
  if (rawToolCalls !== undefined && rawToolCalls !== null) {
744
754
  if (!Array.isArray(rawToolCalls)) {
@@ -15,7 +15,7 @@ import {
15
15
  isOpenAiOperatedResponsesDestination,
16
16
  } from "../../providers/openai-tiers";
17
17
  import type { TranslatorBudget } from "../../lib/translator-budget";
18
- import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat";
18
+ import { rewriteRoutedCustomToolsForUpstream, validateFinalCustomToolCompatibility } from "../../responses/custom-tool-compat";
19
19
  import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat";
20
20
  import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat";
21
21
  import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
@@ -295,7 +295,15 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
295
295
  }
296
296
  const synthesizeMissingCallOutputs = !forward && (stateless || pairedToolResults);
297
297
  if (forward || stateless || pairedToolResults) {
298
- outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
298
+ // A stateful destination can resolve an output-only delta against the call stored behind
299
+ // an unexpanded previous_response_id. All other shapes have no hidden call to preserve.
300
+ const repairOrphanOutputs = forward || stateless || !unexpandedMiss;
301
+ outBody = repairOrphanedInputItems(
302
+ outBody,
303
+ unexpandedMiss,
304
+ synthesizeMissingCallOutputs,
305
+ repairOrphanOutputs,
306
+ );
299
307
  }
300
308
  if (provider.dropResponsesReasoningItems === true) {
301
309
  outBody = dropResponsesReasoningInputItems(outBody);
@@ -450,11 +458,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
450
458
  preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
451
459
  dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
452
460
  stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
461
+ dropForeignItemId: parsed._dropForeignReasoningItemIds === true,
453
462
  },
454
463
  ),
455
464
  provider,
456
465
  ),
457
466
  ),
467
+ isXaiResponsesDestination(provider),
458
468
  ),
459
469
  isXaiSchemaTarget(provider),
460
470
  );
@@ -503,6 +513,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
503
513
  // HTTP and the WebSocket outbound, because the WS path transports this same request
504
514
  // instead of rebuilding it.
505
515
  observeOutbound(parsed._rawBody, finalBody, headers);
516
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
517
+ validateFinalCustomToolCompatibility(finalBody, provider.supportsResponsesCustomTools);
518
+ }
506
519
  const body = JSON.stringify(finalBody);
507
520
  const releaseBodyObservation = translatorBudget.observeExternallyCapped(
508
521
  "passthrough_serialization",
@@ -35,6 +35,11 @@ export function sanitizeReasoningInputContent(
35
35
  preserveRawReasoningContent?: boolean;
36
36
  dropNullContentChannel?: boolean;
37
37
  stripEncryptedContent?: boolean;
38
+ /**
39
+ * Remove `id` from every reasoning item, with or without a blob, because the ids name items in a
40
+ * store this destination cannot read; see `OcxParsedRequest._dropForeignReasoningItemIds`.
41
+ */
42
+ dropForeignItemId?: boolean;
38
43
  },
39
44
  ): unknown {
40
45
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
@@ -55,6 +60,10 @@ export function sanitizeReasoningInputContent(
55
60
  const missingSummary = !Object.prototype.hasOwnProperty.call(rec, "summary");
56
61
  const stripEncryptedContent = hasOcxEnvelope
57
62
  || (opts?.stripEncryptedContent === true && hasEncryptedContent);
63
+ // An id-only item is as foreign as one with a blob: a stateful destination still resolves it
64
+ // against its own store.
65
+ const dropItemId = opts?.dropForeignItemId === true
66
+ && Object.prototype.hasOwnProperty.call(rec, "id");
58
67
  // Codex serializes an absent reasoning content channel as `"content": null`. The field is
59
68
  // optional and null carries nothing, but a strict gateway rejects the item on its declared type
60
69
  // — xAI answers `Could not decode the compaction blob`, naming the sibling `encrypted_content`
@@ -76,7 +85,10 @@ export function sanitizeReasoningInputContent(
76
85
  const blankContent = !dropNullContentChannel
77
86
  && !opts?.preserveRawReasoningContent
78
87
  && (hasRawContent || hasOcxEnvelope);
79
- if (!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel && !missingSummary) {
88
+ if (
89
+ !blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel
90
+ && !missingSummary && !dropItemId
91
+ ) {
80
92
  return item;
81
93
  }
82
94
  changed = true;
@@ -85,6 +97,7 @@ export function sanitizeReasoningInputContent(
85
97
  if (dropNullContentChannel) delete next.content;
86
98
  if (stripOutputStatus) delete next.status;
87
99
  if (stripEncryptedContent) delete next.encrypted_content;
100
+ if (dropItemId) delete next.id;
88
101
  // Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
89
102
  // native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
90
103
  // `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
@@ -1,4 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
3
+ import { debugProviderDiagnostic } from "../../lib/debug";
2
4
  import { isPlainObject } from "./internal";
3
5
  import { activateDeferredTool } from "./tool-schema";
4
6
  import { stripOpenAiOnlyWebSearchFields } from "./web-search";
@@ -169,13 +171,37 @@ export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
169
171
  * exist, producing a 404. Strip all item IDs in this case — `call_id` pairing is unaffected.
170
172
  * Matches codex-rs behavior (core/src/client.rs:918-925).
171
173
  */
172
- export function stripItemIdsWhenUnstored(body: unknown): unknown {
173
- if (!isPlainObject(body) || body.store !== false) return body;
174
+ export function stripItemIdsWhenUnstored(body: unknown, requireCustomCallIds = false): unknown {
175
+ const repairCustomCallIds = requireCustomCallIds === true;
176
+ if (!isPlainObject(body) || (body.store !== false && !repairCustomCallIds)) return body;
174
177
  if (!Array.isArray(body.input)) return body;
175
178
 
176
179
  let changed = false;
177
180
  const input = body.input.map(item => {
178
- if (!isPlainObject(item) || !("id" in item)) return item;
181
+ if (!isPlainObject(item)) return item;
182
+ if (repairCustomCallIds && item.type === "custom_tool_call") {
183
+ try {
184
+ if (typeof item.id === "string" && item.id.startsWith("ctc_")) return item;
185
+ if (
186
+ typeof item.call_id !== "string"
187
+ || typeof item.name !== "string"
188
+ || typeof item.input !== "string"
189
+ ) return item;
190
+ const digest = createHash("sha256")
191
+ .update(JSON.stringify([item.call_id, item.name, item.input]))
192
+ .digest("hex")
193
+ .slice(0, 40);
194
+ changed = true;
195
+ debugProviderDiagnostic("openai-responses", "xai-custom-tool-call-id-repaired", {
196
+ hadId: typeof item.id === "string",
197
+ });
198
+ return { ...item, id: `ctc_${digest}` };
199
+ } catch {
200
+ debugProviderDiagnostic("openai-responses", "xai-custom-tool-call-id-unrepaired", {});
201
+ return item;
202
+ }
203
+ }
204
+ if (body.store !== false || !("id" in item)) return item;
179
205
  changed = true;
180
206
  const next = { ...item };
181
207
  delete next.id;
@@ -194,7 +194,8 @@ export function repairUnidentifiedToolOutputItems(body: unknown): unknown {
194
194
  * reasoning-bearing assistant turn (#1477). Gated on
195
195
  * `synthesizeMissingCallOutputs` (stateless AND non-forward wires); forward replay keeps
196
196
  * fail-closed behavior.
197
- * - `function_call_output`/`custom_tool_call_output` without their paired call item
197
+ * - `function_call_output`/`custom_tool_call_output` without their paired call item, when
198
+ * `repairOrphanOutputs` is enabled
198
199
  * ("No tool call found for function call output with call_id ..."). Converted to user
199
200
  * messages so the result text survives. `function_call_output` also pairs with
200
201
  * `local_shell_call` (codex-rs emits shell outputs as function_call_output).
@@ -342,7 +343,12 @@ export function restoreBridgedWebSearchCalls(body: unknown, destinationScope: st
342
343
  return changed ? { ...body, input: restored } : body;
343
344
  }
344
345
 
345
- export function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
346
+ export function repairOrphanedInputItems(
347
+ body: unknown,
348
+ dropReasoning: boolean,
349
+ synthesizeMissingCallOutputs = false,
350
+ repairOrphanOutputs = true,
351
+ ): unknown {
346
352
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
347
353
  const input = body.input;
348
354
 
@@ -381,7 +387,7 @@ export function repairOrphanedInputItems(body: unknown, dropReasoning: boolean,
381
387
  // incomplete. With no call id and no output, preserve the invalid item so validation fails
382
388
  // closed rather than pretending any tool result exists.
383
389
  const knownNullOutput = callId.length > 0 && item.output == null;
384
- if (!paired && (knownNullOutput || usableOutput)) {
390
+ if (repairOrphanOutputs && !paired && (knownNullOutput || usableOutput)) {
385
391
  changed = true;
386
392
  repaired.push({
387
393
  type: "message",
@@ -8,8 +8,10 @@ import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessa
8
8
 
9
9
  export type QoderAdapterDeps = CodingAgentDeps;
10
10
 
11
- export function buildQoderChildEnv(profile: QoderProfile, apiKey: string): Record<string, string> {
12
- return { ...baseScopedEnv(), NO_COLOR: "1", [profile.tokenEnv]: apiKey };
11
+ export function buildQoderChildEnv(profile: QoderProfile, apiKey: string, systemPrompt?: string): Record<string, string> {
12
+ const promptEnv = profile.region === "cn" ? "QODERCN_APPEND_SYSTEM_PROMPT" : "QODER_APPEND_SYSTEM_PROMPT";
13
+ return { ...baseScopedEnv(), NO_COLOR: "1", [profile.tokenEnv]: apiKey,
14
+ ...(systemPrompt ? { [promptEnv]: systemPrompt } : {}) };
13
15
  }
14
16
 
15
17
  /** Single-shot, tools-disabled Qoder CLI invocation; Codex remains the tool owner. */
@@ -27,8 +29,6 @@ export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderCo
27
29
  ];
28
30
  const effort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
29
31
  if (effort) args.push("--reasoning-effort", effort);
30
- const system = buildSystemPrompt(parsed);
31
- if (system) args.push("--append-system-prompt", system);
32
32
  return args;
33
33
  }
34
34
 
@@ -123,15 +123,18 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
123
123
  });
124
124
  return;
125
125
  }
126
+ // Qoder documents QODER_APPEND_SYSTEM_PROMPT for scoped child configuration.
127
+ // Keep the folded prompt out of argv and never inherit an ambient vendor prompt.
128
+ const system = buildSystemPrompt(parsed);
126
129
  await runCodingAgentTurn({
127
- profiles: QODER_PROFILES,
128
- provider,
129
- parsed,
130
- incoming,
131
- emit: guardQoderScaffolding(emit),
132
- buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov),
133
- buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey),
134
- deps,
130
+ profiles: QODER_PROFILES,
131
+ provider,
132
+ parsed,
133
+ incoming,
134
+ emit: guardQoderScaffolding(emit),
135
+ buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov),
136
+ buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey, system),
137
+ deps,
135
138
  });
136
139
  },
137
140
  };
@@ -54,11 +54,44 @@ const MAX_MARKER_LENGTH = Math.max(...ALL_MARKERS.map(marker => marker.length));
54
54
  * refuse the turn. A stem running to the end of the buffer still counts: more text may be
55
55
  * arriving, and reading it as prose is the one reading that could release the block body.
56
56
  */
57
- function reminderOpensHere(lowered: string, at: number): boolean {
58
- const after = lowered[at + REMINDER_OPEN.length];
57
+ function reminderOpensHere(text: string, at: number): boolean {
58
+ const after = text[at + REMINDER_OPEN.length];
59
59
  return after === undefined || /[\s/>]/.test(after);
60
60
  }
61
61
 
62
+ /**
63
+ * Fold one UTF-16 code unit the way `toLowerCase()` does, when that yields one code unit.
64
+ *
65
+ * This keeps every match the lowercased scan used to make. U+212A KELVIN SIGN lowercases to an
66
+ * ASCII `k`, so `<invo\u212Ae>` was treated as tool markup; an ASCII-only fold would release it.
67
+ * A character whose lowercase form is longer (such as U+0130) is left as-is.
68
+ */
69
+ function foldCodeUnit(code: number): number {
70
+ if (code >= 65 && code <= 90) return code + 32;
71
+ if (code < 128) return code;
72
+ const lowered = String.fromCharCode(code).toLowerCase();
73
+ return lowered.length === 1 ? lowered.charCodeAt(0) : code;
74
+ }
75
+
76
+ /**
77
+ * Find a lowercase ASCII marker without transforming `text`.
78
+ *
79
+ * Marker offsets must remain offsets into the original string. Unicode lowercasing can expand
80
+ * one code unit into several (for example, `İ` becomes `i` plus a combining dot), so an index
81
+ * obtained from `text.toLowerCase()` is unsafe to reuse with `text.slice()`. Folding one code
82
+ * unit at a time keeps the offsets and the matches.
83
+ */
84
+ function indexOfMarker(text: string, marker: string, from = 0): number {
85
+ const last = text.length - marker.length;
86
+ outer: for (let at = Math.max(0, from); at <= last; at++) {
87
+ for (let offset = 0; offset < marker.length; offset++) {
88
+ if (foldCodeUnit(text.charCodeAt(at + offset)) !== marker.charCodeAt(offset)) continue outer;
89
+ }
90
+ return at;
91
+ }
92
+ return -1;
93
+ }
94
+
62
95
  /**
63
96
  * Ceiling on a suppressed block before it is treated as unterminated.
64
97
  *
@@ -80,9 +113,10 @@ export interface ScaffoldFilterResult {
80
113
  function heldSuffixLength(text: string): number {
81
114
  const limit = Math.min(MAX_MARKER_LENGTH - 1, text.length);
82
115
  for (let length = limit; length > 0; length--) {
83
- const suffix = text.slice(text.length - length).toLowerCase();
84
116
  for (const marker of ALL_MARKERS) {
85
- if (marker.length > length && marker.startsWith(suffix)) return length;
117
+ if (marker.length > length && indexOfMarker(text, marker.slice(0, length), text.length - length) >= 0) {
118
+ return length;
119
+ }
86
120
  }
87
121
  }
88
122
  return 0;
@@ -114,7 +148,6 @@ export class QoderScaffoldFilter {
114
148
  for (;;) {
115
149
  if (this.mode === "suppress") {
116
150
  const scan = this.suppressedTail + buffer;
117
- const scanned = scan.toLowerCase();
118
151
  // Unwind nesting rather than ending at the first closer. A reminder containing another
119
152
  // reminder would otherwise hand the outer block's remaining body — the MCP server list
120
153
  // in the reported leak — to the client as the model's answer, with a successful
@@ -122,11 +155,11 @@ export class QoderScaffoldFilter {
122
155
  let cursor = 0;
123
156
  let close = -1;
124
157
  for (;;) {
125
- const nextClose = scanned.indexOf(REMINDER_CLOSE, cursor);
158
+ const nextClose = indexOfMarker(scan, REMINDER_CLOSE, cursor);
126
159
  if (nextClose < 0) break;
127
- let nextOpen = scanned.indexOf(REMINDER_OPEN, cursor);
128
- while (nextOpen >= 0 && !reminderOpensHere(scanned, nextOpen)) {
129
- nextOpen = scanned.indexOf(REMINDER_OPEN, nextOpen + 1);
160
+ let nextOpen = indexOfMarker(scan, REMINDER_OPEN, cursor);
161
+ while (nextOpen >= 0 && !reminderOpensHere(scan, nextOpen)) {
162
+ nextOpen = indexOfMarker(scan, REMINDER_OPEN, nextOpen + 1);
130
163
  }
131
164
  if (nextOpen >= 0 && nextOpen < nextClose) {
132
165
  this.suppressDepth += 1;
@@ -159,11 +192,10 @@ export class QoderScaffoldFilter {
159
192
 
160
193
  let earliest = -1;
161
194
  let found = "";
162
- const lowered = buffer.toLowerCase();
163
195
  for (const marker of ALL_MARKERS) {
164
- let at = lowered.indexOf(marker);
165
- while (at >= 0 && marker === REMINDER_OPEN && !reminderOpensHere(lowered, at)) {
166
- at = lowered.indexOf(marker, at + 1);
196
+ let at = indexOfMarker(buffer, marker);
197
+ while (at >= 0 && marker === REMINDER_OPEN && !reminderOpensHere(buffer, at)) {
198
+ at = indexOfMarker(buffer, marker, at + 1);
167
199
  }
168
200
  if (at < 0) continue;
169
201
  // A closer sitting exactly where an opener starts cannot happen, so ties are impossible.
@@ -114,6 +114,26 @@ export function toolCallArgumentsUsable(args: string): boolean {
114
114
  }
115
115
  }
116
116
 
117
+ /**
118
+ * Whether an in-progress function-call argument buffer could still become valid JSON.
119
+ * The first non-whitespace byte must be one that can begin a JSON value. A stream that
120
+ * already lost its leading `{"` (observed from coding-agent CLIs as `code":"…}`) can only
121
+ * fail `toolCallArgumentsUsable` at completion, so streaming those fragments publishes
122
+ * bytes a failed item cannot take back — the same #765 rule that refuses completion.
123
+ */
124
+ export function toolCallArgumentsCouldBeJson(args: string): boolean {
125
+ const first = args.trimStart().charAt(0);
126
+ if (first === "") return true;
127
+ return first === "{"
128
+ || first === "["
129
+ || first === "\""
130
+ || first === "-"
131
+ || (first >= "0" && first <= "9")
132
+ || first === "t"
133
+ || first === "f"
134
+ || first === "n";
135
+ }
136
+
117
137
  export function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
118
138
  const message = redactSecretString(event.message);
119
139
  if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
package/src/bridge/sse.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
25
25
  import { encodeCompactionSummary } from "../responses/compaction";
26
26
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
27
+ import { mayBecomeCodeModeShellInput } from "../responses/code-mode-shell-input";
27
28
  import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
28
29
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "../responses/reasoning-envelope";
29
30
  import { rememberReasoningForCall } from "../responses/reasoning-replay-cache";
@@ -47,7 +48,7 @@ import {
47
48
  type TranslatorBudget,
48
49
  type TranslatorBufferKind,
49
50
  } from "../lib/translator-budget";
50
- import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
51
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
51
52
  import type { OutputItem, StringChunks } from "./internal";
52
53
 
53
54
  function sseEvent(name: string, data: Record<string, unknown>): string {
@@ -1055,10 +1056,17 @@ export function bridgeToResponsesSSE(
1055
1056
  currentToolCall.callId,
1056
1057
  ));
1057
1058
  if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
1058
- emit("response.function_call_arguments.delta", {
1059
- item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1060
- delta: event.arguments,
1061
- });
1059
+ // Hold fragments whose accumulated buffer can never parse as JSON. Fragments
1060
+ // already streamed are retained by the client as history even when the item
1061
+ // fails at completion (the poisoned-replay loop behind inbound "non-JSON
1062
+ // arguments" warnings); holding costs nothing for healthy streams because the
1063
+ // completed item still carries the full arguments.
1064
+ if (toolCallArgumentsCouldBeJson(currentToolCall.args)) {
1065
+ emit("response.function_call_arguments.delta", {
1066
+ item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1067
+ delta: event.arguments,
1068
+ });
1069
+ }
1062
1070
  }
1063
1071
  if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
1064
1072
  // `progressiveFreeformInput` holds while the buffer is still an ambiguous prefix
@@ -1089,6 +1097,7 @@ export function bridgeToResponsesSSE(
1089
1097
  // replaced by the normalized ones.
1090
1098
  const mayNormalize = ownsFreeformGrammar && currentToolCall.name === "apply_patch";
1091
1099
  if (!((mayCompile || mayNormalize) && mayBecomePatchEnvelope(full))
1100
+ && !(mayCompile && mayBecomeCodeModeShellInput(currentToolCall.args, full))
1092
1101
  && full.startsWith(emitted) && full.length > emitted.length) {
1093
1102
  emit("response.custom_tool_call_input.delta", {
1094
1103
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,