@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -3,6 +3,7 @@ import { stripBracketedModelSuffix } from "./openai-chat";
3
3
  import { normalizeOpenCodeGoAdditionalTools } from "./opencode-go-additional-tools";
4
4
  import { isXaiResponsesDestination } from "../providers/xai-transport";
5
5
  import { createHash } from "node:crypto";
6
+ import { Buffer } from "node:buffer";
6
7
  import type { IncomingMeta, ProviderAdapter } from "./base";
7
8
  import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
8
9
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
@@ -24,6 +25,8 @@ import type { TranslatorBudget } from "../lib/translator-budget";
24
25
  import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
25
26
  import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
26
27
  import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
28
+ import { preparePlaintextV2AgentMessages } from "../responses/plaintext-v2-agent-messages";
29
+ import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../responses/muse-tool-name-alias";
27
30
  import { openaiResponsesUrl } from "./openai-responses-url";
28
31
  import { normalizeResponsesCodeMode } from "./responses-code-mode";
29
32
  import { stripUnicodePropertyPatterns } from "./responses-tool-schema";
@@ -332,24 +335,6 @@ function scrubOcxCompactionItems(
332
335
  return changed ? { ...body, input } : body;
333
336
  }
334
337
 
335
- /**
336
- * Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
337
- * codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
338
- * backend rejects both. The catalog fix prevents `use_responses_lite` from being set, but this
339
- * is a defense-in-depth guard so stale on-disk catalogs don't break until the user runs `ocx sync`.
340
- */
341
- function stripUnsupportedReasoningParams(body: unknown): unknown {
342
- if (!isPlainObject(body)) return body;
343
- const model = typeof body.model === "string" ? body.model : "";
344
- if (!model.includes("codex-spark")) return body;
345
- if (!isPlainObject(body.reasoning)) return body;
346
- const reasoning = body.reasoning as Record<string, unknown>;
347
- // Spark supports reasoning.effort but rejects context, summary, and generate_summary.
348
- const { context: _ctx, summary: _sum, generate_summary: _gs, ...rest } = reasoning;
349
- if (_ctx === undefined && _sum === undefined && _gs === undefined) return body;
350
- return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
351
- }
352
-
353
338
  /**
354
339
  * GPT-5.6 retired the legacy 24-hour retention field, and the ChatGPT backend 400s the whole
355
340
  * request when that field is present (issue #2092).
@@ -470,157 +455,10 @@ function normalizeConfiguredReasoningSummaryDelivery(
470
455
  };
471
456
  }
472
457
 
473
- /**
474
- * Comprehensive Spark compatibility layer. codex-rs emits five tool types (function,
475
- * namespace, tool_search, web_search, custom) plus extensions (defer_loading,
476
- * parallel_tool_calls, tool_search_call/output items). Spark's serving path only
477
- * supports flat function tools and hosted web_search. This function:
478
- * - Flattens MCP-style namespace tools → promotes inner functions to top level. The reserved
479
- * `functions` group is kept as a group (#3217): Codex 0.147+ sends every ordinary client tool
480
- * inside it on Responses Lite, the backend accepts the group as-is, and flattening it changes
481
- * what the backend answers with — a `custom_tool_call` carrying `namespace: "exec"`, which
482
- * codex-rs concatenates into the unroutable `execexec`. Traced on a live proxy: with the
483
- * group intact the same backend returns the bare `exec` call and the turn completes.
484
- * - Drops unsupported tool types (tool_search, custom)
485
- * - Strips defer_loading from function tools
486
- * - Strips namespace from input items
487
- * - Drops tool_search_call/tool_search_output input items
488
- * - Sets parallel_tool_calls to false
489
- */
490
- function stripSparkCompatibility(body: unknown): unknown {
491
- if (!isPlainObject(body)) return body;
492
- const model = typeof body.model === "string" ? body.model : "";
493
- if (!model.includes("codex-spark")) return body;
494
-
495
- let changed = false;
496
-
497
- const SPARK_SAFE_TOOL_TYPES = new Set(["function", "web_search", "web_search_preview"]);
498
- // Inside the reserved group Codex sends freeform `custom` tools (code-mode `exec`) and the
499
- // backend accepts them there; the top-level "drop custom" rule stays for flattened groups.
500
- const SPARK_SAFE_FUNCTIONS_GROUP_CHILD_TYPES = new Set(["function", "custom"]);
501
- const filterSparkFunctionsGroup = (group: Record<string, unknown>): Record<string, unknown> | undefined => {
502
- if (!Array.isArray(group.tools)) return undefined;
503
- let groupChanged = false;
504
- const children: unknown[] = [];
505
- for (const child of group.tools) {
506
- if (!isPlainObject(child) || typeof child.type !== "string" || !SPARK_SAFE_FUNCTIONS_GROUP_CHILD_TYPES.has(child.type)) {
507
- groupChanged = true;
508
- continue;
509
- }
510
- if (child.type === "function" && "defer_loading" in child) {
511
- const { defer_loading: _, ...rest } = child;
512
- groupChanged = true;
513
- children.push(rest);
514
- continue;
515
- }
516
- children.push(child);
517
- }
518
- if (children.length === 0) return undefined;
519
- return groupChanged ? { ...group, tools: children } : group;
520
- };
521
-
522
- let tools = body.tools;
523
- if (Array.isArray(tools)) {
524
- const flattened: unknown[] = [];
525
- for (const t of tools) {
526
- if (isPlainObject(t) && t.type === "namespace" && t.name === SPARK_RESERVED_FUNCTIONS_NAMESPACE) {
527
- const kept = filterSparkFunctionsGroup(t);
528
- if (kept !== t) changed = true;
529
- if (kept) flattened.push(kept);
530
- } else if (isPlainObject(t) && t.type === "namespace") {
531
- changed = true;
532
- if (Array.isArray(t.tools)) {
533
- for (const inner of t.tools) flattened.push(inner);
534
- }
535
- } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) {
536
- changed = true;
537
- } else {
538
- flattened.push(t);
539
- }
540
- }
541
- // Strip defer_loading from promoted/remaining function tools.
542
- tools = flattened.map(t => {
543
- if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) {
544
- const { defer_loading: _, ...rest } = t;
545
- changed = true;
546
- return rest;
547
- }
548
- return t;
549
- });
550
- }
551
-
552
- // Clean input items: strip namespace, drop tool_search_call/tool_search_output.
553
- const SPARK_UNSUPPORTED_INPUT_TYPES = new Set([
554
- "tool_search_call", "tool_search_output",
555
- "custom_tool_call", "custom_tool_call_output",
556
- ]);
557
- let input = body.input;
558
- if (Array.isArray(input)) {
559
- const cleaned: unknown[] = [];
560
- for (const item of input) {
561
- if (isPlainObject(item) && typeof item.type === "string" && SPARK_UNSUPPORTED_INPUT_TYPES.has(item.type)) {
562
- changed = true;
563
- continue;
564
- }
565
- // Process additional_tools items: filter their inner tools array the same way.
566
- if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
567
- const innerTools = item.tools as unknown[];
568
- const filteredInner: unknown[] = [];
569
- for (const t of innerTools) {
570
- if (isPlainObject(t) && t.type === "namespace" && t.name === SPARK_RESERVED_FUNCTIONS_NAMESPACE) {
571
- const kept = filterSparkFunctionsGroup(t);
572
- if (kept !== t) changed = true;
573
- if (kept) filteredInner.push(kept);
574
- } else if (isPlainObject(t) && t.type === "namespace") {
575
- changed = true;
576
- if (Array.isArray(t.tools)) {
577
- for (const fn of t.tools) filteredInner.push(fn);
578
- }
579
- } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) {
580
- changed = true; // drop custom, tool_search, etc.
581
- } else {
582
- filteredInner.push(t);
583
- }
584
- }
585
- // Strip defer_loading from remaining function tools.
586
- const cleanedInner = filteredInner.map(t => {
587
- if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) {
588
- const { defer_loading: _, ...rest } = t;
589
- changed = true;
590
- return rest;
591
- }
592
- return t;
593
- });
594
- cleaned.push({ ...item, tools: cleanedInner });
595
- continue;
596
- }
597
- if (isPlainObject(item) && "namespace" in item) {
598
- const { namespace: _, ...rest } = item;
599
- changed = true;
600
- cleaned.push(rest);
601
- } else {
602
- cleaned.push(item);
603
- }
604
- }
605
- if (changed) input = cleaned;
606
- }
607
-
608
- // Force parallel_tool_calls off for Spark.
609
- const extraOverrides: Record<string, unknown> = {};
610
- if (body.parallel_tool_calls === true) { extraOverrides.parallel_tool_calls = false; changed = true; }
611
-
612
- return changed
613
- ? { ...body, ...(tools !== body.tools ? { tools } : {}), ...(input !== body.input ? { input } : {}), ...extraOverrides }
614
- : body;
615
- }
616
-
617
458
  function isPlainObject(v: unknown): v is Record<string, unknown> {
618
459
  return !!v && typeof v === "object" && !Array.isArray(v);
619
460
  }
620
461
 
621
- /** Codex's reserved client-tool group on Responses Lite; carries no wire prefix. */
622
- const SPARK_RESERVED_FUNCTIONS_NAMESPACE = "functions";
623
-
624
462
  /**
625
463
  * Apply the routed provider's real effort ladder to an existing Responses reasoning field.
626
464
  * Native forward requests keep the server-owned native clamp; unknown third-party ladders stay
@@ -865,6 +703,7 @@ function promoteClientLoadedTools(body: unknown): unknown {
865
703
  }
866
704
 
867
705
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
706
+
868
707
  const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
869
708
  const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
870
709
 
@@ -2307,6 +2146,15 @@ function responsesErrorMessage(payload: unknown): string {
2307
2146
  return "upstream compaction failed";
2308
2147
  }
2309
2148
 
2149
+ /** Count an append without rescanning accumulated text, including split surrogate pairs. */
2150
+ function appendedUtf8Bytes(previousBytes: number, lastCodeUnit: number, fragment: string): number {
2151
+ const first = fragment.charCodeAt(0);
2152
+ // Separate lone surrogates each count as a three-byte replacement character; together
2153
+ // they encode as one four-byte scalar. Empty fragments produce NaN and never pair.
2154
+ const joinsSurrogatePair = lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff && first >= 0xdc00 && first <= 0xdfff;
2155
+ return previousBytes + Buffer.byteLength(fragment, "utf8") - (joinsSurrogatePair ? 2 : 0);
2156
+ }
2157
+
2310
2158
  export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
2311
2159
  return {
2312
2160
  name: "openai-responses",
@@ -2370,6 +2218,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2370
2218
  let routedCustomToolRepairNames: Set<string> | undefined;
2371
2219
  let convertedRoutedToolSearchNames: Set<string> | undefined;
2372
2220
  let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
2221
+ let plaintextV2AgentMessageToolNames: ReadonlySet<string> | undefined;
2222
+ let plaintextV2AgentMessageAliasedToolNames: ReadonlySet<string> | undefined;
2223
+ let convertedMuseToolNameAliases: Map<string, string> | undefined;
2373
2224
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
2374
2225
  let outBody = stripPreviousResponseId(
2375
2226
  parsed._rawBody,
@@ -2463,6 +2314,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2463
2314
  outBody = stripOpenAiOnlyWebSearchFields(outBody);
2464
2315
  }
2465
2316
  outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url);
2317
+ // Host-only: api.meta.ai rejects function names over 64 chars on every Muse model,
2318
+ // including default muse-spark-1.3. Do not reuse the contributor/Zen web_search
2319
+ // predicates. Namespace flattening has already produced the public wire names.
2320
+ if (isMetaAiResponsesDestination(url)) {
2321
+ const rewritten = rewriteMuseToolNamesForUpstream(outBody);
2322
+ outBody = rewritten.body;
2323
+ convertedMuseToolNameAliases = rewritten.aliases;
2324
+ }
2466
2325
  // Last, so promoted namespace children are also cleared of Codex-private fields.
2467
2326
  outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
2468
2327
  }
@@ -2479,34 +2338,38 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2479
2338
  // Run after routed compaction so nested input_image parts are replaced before a malformed
2480
2339
  // tool output is flattened to text and can no longer be inspected structurally.
2481
2340
  outBody = repairUnidentifiedToolOutputItems(outBody);
2341
+ if (parsed._plaintextV2AgentMessages === true && isCanonicalOpenAiForwardProvider(provider)) {
2342
+ const prepared = preparePlaintextV2AgentMessages(outBody);
2343
+ outBody = prepared.body;
2344
+ if (prepared.namespaceAliased) {
2345
+ plaintextV2AgentMessageToolNames = prepared.toolNames;
2346
+ plaintextV2AgentMessageAliasedToolNames = prepared.aliasedAgentMessageToolNames;
2347
+ }
2348
+ }
2482
2349
  const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
2483
2350
  const sanitizedBody = normalizeToolSchemas(
2484
- stripSparkCompatibility(
2485
- stripUnsupportedReasoningParams(
2486
- stripItemIdsWhenUnstored(
2487
- stripInvalidItemIds(
2488
- stripUnsupportedHostedTools(
2489
- sanitizeReasoningInputContent(
2490
- scrubOcxCompactionItems(
2491
- outBody,
2492
- destinationDecodesNativeCompactionBlob(provider),
2493
- threadServingIdentityChanged,
2494
- ),
2495
- {
2496
- preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
2497
- dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
2498
- stripEncryptedContent: threadServingIdentityChanged,
2499
- },
2500
- ),
2501
- provider,
2351
+ stripItemIdsWhenUnstored(
2352
+ stripInvalidItemIds(
2353
+ stripUnsupportedHostedTools(
2354
+ sanitizeReasoningInputContent(
2355
+ scrubOcxCompactionItems(
2356
+ outBody,
2357
+ destinationDecodesNativeCompactionBlob(provider),
2358
+ threadServingIdentityChanged,
2502
2359
  ),
2360
+ {
2361
+ preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
2362
+ dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
2363
+ stripEncryptedContent: threadServingIdentityChanged,
2364
+ },
2503
2365
  ),
2366
+ provider,
2504
2367
  ),
2505
2368
  ),
2506
2369
  ),
2507
2370
  isXaiSchemaTarget(provider),
2508
2371
  );
2509
- const finalBody = stripDisabledVerbosity(
2372
+ const unnormalizedBody = stripDisabledVerbosity(
2510
2373
  stripDisabledReasoningSummaries(
2511
2374
  normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
2512
2375
  provider,
@@ -2515,14 +2378,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2515
2378
  provider,
2516
2379
  parsed.modelId,
2517
2380
  );
2381
+ // Normalize the wire model before deriving model-dependent transport metadata.
2382
+ const finalBody =
2383
+ provider.modelSuffixBracketStrip
2384
+ && unnormalizedBody !== null
2385
+ && typeof unnormalizedBody === "object"
2386
+ && !Array.isArray(unnormalizedBody)
2387
+ && typeof (unnormalizedBody as { model?: unknown }).model === "string"
2388
+ ? { ...(unnormalizedBody as Record<string, unknown>), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) }
2389
+ : unnormalizedBody;
2518
2390
  if (isCanonicalOpenAiForwardProvider(provider)) {
2519
- // Spark closes Responses Lite streams before a terminal completion. Select compatibility
2520
- // from the final wire model so aliases cannot leave the caller or a static header enabled.
2521
- if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") {
2522
- for (const name of Object.keys(headers)) {
2523
- if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name];
2524
- }
2525
- }
2526
2391
  const routingHeaders = new Headers(headers);
2527
2392
  applyCodexRoutingHint(routingHeaders, finalBody);
2528
2393
  // Static headers may use mixed casing. Remove every stale spelling
@@ -2548,18 +2413,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2548
2413
  // here, on the serialized body, not on the parsed selector. One place covers both the
2549
2414
  // HTTP and the WebSocket outbound, because the WS path transports this same request
2550
2415
  // instead of rebuilding it.
2551
- const body = JSON.stringify(
2552
- provider.modelSuffixBracketStrip
2553
- && finalBody !== null
2554
- && typeof finalBody === "object"
2555
- && !Array.isArray(finalBody)
2556
- && typeof (finalBody as { model?: unknown }).model === "string"
2557
- ? { ...(finalBody as Record<string, unknown>), model: stripBracketedModelSuffix((finalBody as { model: string }).model) }
2558
- : finalBody,
2559
- );
2416
+ const body = JSON.stringify(finalBody);
2560
2417
  const releaseBodyObservation = translatorBudget.observeExternallyCapped(
2561
2418
  "passthrough_serialization",
2562
- new TextEncoder().encode(body).byteLength,
2419
+ Buffer.byteLength(body, "utf8"),
2563
2420
  );
2564
2421
  return {
2565
2422
  url,
@@ -2571,6 +2428,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2571
2428
  ...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
2572
2429
  ...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
2573
2430
  ...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
2431
+ ...(plaintextV2AgentMessageToolNames ? { plaintextV2AgentMessageToolNames } : {}),
2432
+ ...(plaintextV2AgentMessageAliasedToolNames ? { plaintextV2AgentMessageAliasedToolNames } : {}),
2433
+ ...(convertedMuseToolNameAliases ? { convertedMuseToolNameAliases } : {}),
2574
2434
  ...(tierLog ? { tierLog } : {}),
2575
2435
  };
2576
2436
  },
@@ -2583,12 +2443,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2583
2443
  yield { type: "error", message: "passthrough adapter received no response body" };
2584
2444
  return;
2585
2445
  }
2586
- const budgetEncoder = new TextEncoder();
2587
2446
  let deltas = "";
2447
+ let deltasBytes = 0;
2448
+ let deltasLastCodeUnit = 0;
2588
2449
  let doneText = "";
2450
+ let doneTextBytes = 0;
2451
+ let doneTextLastCodeUnit = 0;
2589
2452
  let snapshot = "";
2453
+ let snapshotBytes = 0;
2590
2454
  let usage: OcxUsage | undefined;
2455
+ let usageRawBytes = 0;
2591
2456
  let compactionEncryptedContent: string | undefined;
2457
+ let compactionEncryptedContentBytes = 0;
2592
2458
  let completedSeen = false;
2593
2459
  for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
2594
2460
  let payload: unknown;
@@ -2598,21 +2464,25 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2598
2464
  case "response.output_text.delta":
2599
2465
  if (typeof payload.delta === "string") {
2600
2466
  const next = deltas + payload.delta;
2601
- const previousBytes = budgetEncoder.encode(deltas).byteLength;
2602
- const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
2467
+ const nextBytes = appendedUtf8Bytes(deltasBytes, deltasLastCodeUnit, payload.delta);
2468
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
2603
2469
  deltas = next;
2604
2470
  reservation.commitRetained();
2605
- budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
2471
+ budget.releaseRetained(deltasBytes, { kind: "retained_collectors" });
2472
+ deltasBytes = nextBytes;
2473
+ if (payload.delta.length > 0) deltasLastCodeUnit = payload.delta.charCodeAt(payload.delta.length - 1);
2606
2474
  }
2607
2475
  break;
2608
2476
  case "response.output_text.done":
2609
2477
  if (typeof payload.text === "string") {
2610
2478
  const next = doneText + payload.text;
2611
- const previousBytes = budgetEncoder.encode(doneText).byteLength;
2612
- const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
2479
+ const nextBytes = appendedUtf8Bytes(doneTextBytes, doneTextLastCodeUnit, payload.text);
2480
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
2613
2481
  doneText = next;
2614
2482
  reservation.commitRetained();
2615
- budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
2483
+ budget.releaseRetained(doneTextBytes, { kind: "retained_collectors" });
2484
+ doneTextBytes = nextBytes;
2485
+ if (payload.text.length > 0) doneTextLastCodeUnit = payload.text.charCodeAt(payload.text.length - 1);
2616
2486
  }
2617
2487
  break;
2618
2488
  case "response.failed":
@@ -2630,28 +2500,28 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2630
2500
  const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
2631
2501
  if (isPlainObject(compaction) && typeof compaction.encrypted_content === "string") {
2632
2502
  const nextEncryptedContent = compaction.encrypted_content;
2633
- const previousBytes = budgetEncoder.encode(compactionEncryptedContent ?? "").byteLength;
2634
- const reservation = budget.reserveTransient(budgetEncoder.encode(nextEncryptedContent).byteLength, { kind: "retained_collectors" });
2503
+ const nextEncryptedContentBytes = Buffer.byteLength(nextEncryptedContent, "utf8");
2504
+ const reservation = budget.reserveTransient(nextEncryptedContentBytes, { kind: "retained_collectors" });
2635
2505
  compactionEncryptedContent = nextEncryptedContent;
2636
2506
  reservation.commitRetained();
2637
- budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
2507
+ budget.releaseRetained(compactionEncryptedContentBytes, { kind: "retained_collectors" });
2508
+ compactionEncryptedContentBytes = nextEncryptedContentBytes;
2638
2509
  }
2639
2510
  const next = responsesPayloadText(payload.response);
2640
- const previousBytes = budgetEncoder.encode(snapshot).byteLength;
2641
- const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
2511
+ const nextBytes = Buffer.byteLength(next, "utf8");
2512
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
2642
2513
  snapshot = next;
2643
2514
  reservation.commitRetained();
2644
- budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
2515
+ budget.releaseRetained(snapshotBytes, { kind: "retained_collectors" });
2516
+ snapshotBytes = nextBytes;
2645
2517
  }
2646
2518
  {
2647
2519
  const nextUsage = usageFromResponsesPayload(payload.response);
2648
2520
  // The attached raw usage object can be event-sized (unknown keys carry arbitrary
2649
2521
  // values); it stays reachable until the terminal yields, so charge it like the
2650
2522
  // adjacent retained collectors or it would defeat the per-request memory cap.
2651
- const previousRawBytes = usage?.rawUsage === undefined ? 0
2652
- : budgetEncoder.encode(JSON.stringify(usage.rawUsage)).byteLength;
2653
2523
  const nextRawBytes = nextUsage?.rawUsage === undefined ? 0
2654
- : budgetEncoder.encode(JSON.stringify(nextUsage.rawUsage)).byteLength;
2524
+ : Buffer.byteLength(JSON.stringify(nextUsage.rawUsage), "utf8");
2655
2525
  if (nextRawBytes > 0) {
2656
2526
  const reservation = budget.reserveTransient(nextRawBytes, { kind: "retained_collectors" });
2657
2527
  usage = nextUsage;
@@ -2659,9 +2529,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2659
2529
  } else {
2660
2530
  usage = nextUsage;
2661
2531
  }
2662
- if (previousRawBytes > 0) {
2663
- budget.releaseRetained(previousRawBytes, { kind: "retained_collectors" });
2532
+ if (usageRawBytes > 0) {
2533
+ budget.releaseRetained(usageRawBytes, { kind: "retained_collectors" });
2664
2534
  }
2535
+ usageRawBytes = nextRawBytes;
2665
2536
  }
2666
2537
  break;
2667
2538
  }
@@ -2683,10 +2554,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2683
2554
  const text = snapshot || doneText || deltas;
2684
2555
  if (text) yield { type: "text_delta", text };
2685
2556
  budget.releaseRetained(
2686
- budgetEncoder.encode(deltas).byteLength
2687
- + budgetEncoder.encode(doneText).byteLength
2688
- + budgetEncoder.encode(snapshot).byteLength
2689
- + (usage?.rawUsage === undefined ? 0 : budgetEncoder.encode(JSON.stringify(usage.rawUsage)).byteLength),
2557
+ deltasBytes + doneTextBytes + snapshotBytes + usageRawBytes,
2690
2558
  { kind: "retained_collectors" },
2691
2559
  );
2692
2560
  yield {
@@ -2701,7 +2569,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
2701
2569
  try { payload = await response.json(); } catch {
2702
2570
  return [{ type: "error", message: "malformed upstream compaction response" }];
2703
2571
  }
2704
- budget.chargeRetained(new TextEncoder().encode(JSON.stringify(payload)).byteLength, { kind: "retained_collectors" });
2572
+ budget.chargeRetained(Buffer.byteLength(JSON.stringify(payload), "utf8"), { kind: "retained_collectors" });
2705
2573
  if (!isPlainObject(payload)) {
2706
2574
  return [{ type: "error", message: "malformed upstream compaction response" }];
2707
2575
  }
@@ -6,7 +6,6 @@ import { createCodeBuddyAdapter } from "./codebuddy/adapter";
6
6
  import { createQoderAdapter } from "./qoder/adapter";
7
7
  import { createCommandCodeAdapter } from "./command-code";
8
8
  import { createCursorAdapter } from "./cursor";
9
- import { createDevinCliAdapter } from "./devin-cli/adapter";
10
9
  import { createDevinAdapter } from "./devin";
11
10
  import { createGoogleAdapter } from "./google";
12
11
  import { createKiroAdapter } from "./kiro";
@@ -43,7 +42,6 @@ export type AdapterWire =
43
42
  | "google"
44
43
  | "kiro"
45
44
  | "cursor"
46
- | "devin-cli"
47
45
  | "devin";
48
46
 
49
47
  export type AdapterMutationContract =
@@ -126,11 +124,6 @@ export const ADAPTER_REGISTRY = {
126
124
  mutation: "codex-owned-with-gated-native-fallback",
127
125
  create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCursorAdapter(provider),
128
126
  },
129
- "devin-cli": {
130
- wire: "devin-cli",
131
- mutation: "codex-owned",
132
- create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createDevinCliAdapter(provider),
133
- },
134
127
  devin: {
135
128
  wire: "devin",
136
129
  mutation: "codex-owned",
@@ -12,7 +12,13 @@ export const PREFLIGHT_HEARTBEAT_RETAIN_LIMIT = 16;
12
12
  export const COALESCE_MAX_CHUNK_LENGTH = 64 * 1024;
13
13
 
14
14
  export interface AdapterEventQueue {
15
- push(event: AdapterEvent): void;
15
+ /**
16
+ * Returns true when the event was merged into the buffered tail instead of
17
+ * becoming its own retained item. A caller that charges a memory budget for
18
+ * what the queue holds needs that distinction: a merged delta costs only its
19
+ * appended payload, while a new item costs a whole serialized event.
20
+ */
21
+ push(event: AdapterEvent): boolean;
16
22
  close(): void;
17
23
  stream(): AsyncIterable<AdapterEvent>;
18
24
  collect(): Promise<AdapterEvent[]>;
@@ -98,21 +104,22 @@ export function createAdapterEventQueue(opts?: {
98
104
  return false;
99
105
  };
100
106
 
101
- const push = (event: AdapterEvent): void => {
102
- if (closed) return;
107
+ const push = (event: AdapterEvent): boolean => {
108
+ if (closed) return false;
103
109
  const reader = readers.shift();
104
110
  if (reader) {
105
111
  reader({ done: false, value: event });
106
- return;
112
+ return false;
107
113
  }
108
- if (coalesceIntoTail(event)) return;
114
+ if (coalesceIntoTail(event)) return true;
109
115
  if (queued.length >= maxBacklog) {
110
116
  opts?.onBacklogExceeded?.();
111
117
  queued.push({ type: "error", message: "consumer stalled: adapter event backlog exceeded — turn aborted" });
112
118
  close();
113
- return;
119
+ return false;
114
120
  }
115
121
  queued.push(event);
122
+ return false;
116
123
  };
117
124
 
118
125
  const close = (): void => {
package/src/bridge.ts CHANGED
@@ -663,16 +663,13 @@ export function bridgeToResponsesSSE(
663
663
  const closeCurrentRawReasoning = () => {
664
664
  if (!currentRawReasoning) return;
665
665
  rawReasoningForNextToolCall = currentRawReasoning.text;
666
- emit("response.reasoning_summary_text.done", {
667
- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text,
668
- });
669
- emit("response.reasoning_summary_part.done", {
670
- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0,
671
- part: { type: "summary_text", text: currentRawReasoning.text },
666
+ emit("response.reasoning_text.done", {
667
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text,
672
668
  });
673
669
  const item = {
674
670
  type: "reasoning", id: currentRawReasoning.itemId,
675
- summary: [{ type: "summary_text", text: currentRawReasoning.text }],
671
+ summary: [] as never[],
672
+ content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
676
673
  };
677
674
  emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
678
675
  retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
@@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE(
1111
1108
  const itemId = `rs_${uuid()}`;
1112
1109
  const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
1113
1110
  emit("response.output_item.added", { output_index: outputIndex, item });
1114
- emit("response.reasoning_summary_part.added", {
1115
- item_id: itemId, output_index: outputIndex, summary_index: 0,
1116
- part: { type: "summary_text", text: "" },
1117
- });
1118
1111
  currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
1119
1112
  }
1120
1113
  ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
@@ -1123,9 +1116,12 @@ export function bridgeToResponsesSSE(
1123
1116
  event.text,
1124
1117
  "reasoning",
1125
1118
  ));
1126
- emit("response.reasoning_summary_text.delta", {
1119
+ // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT
1120
+ // channel. Clients control raw-reasoning display; this text is not a
1121
+ // provider-authored summary.
1122
+ emit("response.reasoning_text.delta", {
1127
1123
  item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
1128
- summary_index: 0, delta: event.text,
1124
+ content_index: 0, delta: event.text,
1129
1125
  });
1130
1126
  break;
1131
1127
  }
@@ -1297,7 +1293,9 @@ export function bridgeToResponsesSSE(
1297
1293
  if (isTruncatedStopReason(event.stopReason)) failCurrentToolCall();
1298
1294
  else closeCurrentToolCall();
1299
1295
  }
1300
- if (currentWebSearch) closeCurrentWebSearch("completed", []);
1296
+ // A search still in flight when upstream truncates never returned results, so it
1297
+ // takes the same "failed" status as the error/incomplete terminals below.
1298
+ if (currentWebSearch) closeCurrentWebSearch(isTruncatedStopReason(event.stopReason) ? "failed" : "completed", []);
1301
1299
  releasePendingWebSources();
1302
1300
  // Redacted-only turns (or hidden thinking without a trailing signature event) still
1303
1301
  // need their envelope-only reasoning item so the blocks replay next turn.
@@ -1783,7 +1781,8 @@ function buildResponseJSONWithBudget(
1783
1781
  }
1784
1782
  pushOutput({
1785
1783
  type: "reasoning", id: `rs_${uuid()}`,
1786
- summary: [{ type: "summary_text", text: currentRawReasoning }],
1784
+ summary: [],
1785
+ content: [{ type: "reasoning_text", text: currentRawReasoning }],
1787
1786
  }, currentRawReasoningBytes, "reasoning");
1788
1787
  currentRawReasoning = "";
1789
1788
  currentRawReasoningBytes = 0;
@@ -46,10 +46,35 @@ function contentToText(content: unknown): string {
46
46
  }
47
47
 
48
48
  function imageUrlFromPart(part: Rec): string | null {
49
- if (part.type !== "image_url") return null;
50
- const imageUrl = part.image_url;
51
- if (typeof imageUrl === "string" && imageUrl.length > 0) return imageUrl;
52
- if (isRec(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.length > 0) return imageUrl.url;
49
+ if (part.type === "image_url") {
50
+ const imageUrl = part.image_url;
51
+ if (typeof imageUrl === "string" && imageUrl.length > 0) return imageUrl;
52
+ if (isRec(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.length > 0) return imageUrl.url;
53
+ return null;
54
+ }
55
+ // Agent clients whose native wire shape is not OpenAI's still send images over
56
+ // Chat Completions: Pi/MCP-style parts carry {type:"image", data, mimeType}
57
+ // (Aside read_file tool results), Anthropic-shaped clients carry a source
58
+ // object. Dropping either silently blinds a vision model, so normalize both
59
+ // to the URL/data-URI form the Responses pipeline already understands.
60
+ if (part.type === "image") {
61
+ const data = part.data;
62
+ if (typeof data === "string" && data.length > 0) {
63
+ if (data.startsWith("data:")) return data;
64
+ const media = typeof part.mimeType === "string" && part.mimeType.length > 0 ? part.mimeType
65
+ : typeof part.mediaType === "string" && part.mediaType.length > 0 ? part.mediaType
66
+ : "image/png";
67
+ return "data:" + media + ";base64," + data;
68
+ }
69
+ const source = part.source;
70
+ if (isRec(source)) {
71
+ if (source.type === "base64" && typeof source.data === "string" && source.data.length > 0) {
72
+ const media = typeof source.media_type === "string" && source.media_type.length > 0 ? source.media_type : "image/png";
73
+ return "data:" + media + ";base64," + source.data;
74
+ }
75
+ if (source.type === "url" && typeof source.url === "string" && source.url.length > 0) return source.url;
76
+ }
77
+ }
53
78
  return null;
54
79
  }
55
80