@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -0,0 +1,279 @@
1
+ // Bounded classification of a PARTIAL freeform tool-call wrapper.
2
+ //
3
+ // `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
4
+ // what order an object's properties arrive in or how their names are spelled. The streaming side
5
+ // has to reach the same answer from a prefix, and it used to do that by comparing the buffer
6
+ // against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
7
+ // wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
8
+ // `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
9
+ // completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
10
+ // same disagreement; reordering and escaping are the two the literal matcher could never see,
11
+ // because RFC 8259 objects are unordered and their names are strings with escapes.
12
+ //
13
+ // So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
14
+ // which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
15
+ //
16
+ // `input` the canonical key is present with a string value, whatever preceded it and however
17
+ // its name was spelled. Its value can be decoded progressively, because
18
+ // `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
19
+ // `raw` no wrapper can apply, because the text is not an object or because it is one that
20
+ // `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
21
+ // `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
22
+ // is published until the object parses and completion's own rule decides.
23
+ //
24
+ // The bound matters as much as the classification. A scan that walks the whole buffer on every
25
+ // delta is quadratic in the argument size, so classification gives up after
26
+ // `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
27
+ // buffer is still resolved by the authoritative parse at completion.
28
+
29
+ /** The insignificant whitespace `JSON.parse` accepts between tokens. */
30
+ export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
31
+
32
+ /** The two-character escapes JSON defines, and nothing else. */
33
+ export const JSON_ESCAPES = new Map<string, string>([
34
+ ['"', '"'], ["\\", "\\"], ["/", "/"],
35
+ ["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
36
+ ]);
37
+
38
+ /**
39
+ * How far a classification scan walks before it gives up and holds.
40
+ *
41
+ * A wrapper's own structure — its property names and the small scalar siblings a model puts
42
+ * beside the body — lives at the front of the object. A buffer still undecided after this much
43
+ * scanning is one whose preceding values are large, and holding it is what the routed
44
+ * restoration path already does for every unrecognized object.
45
+ */
46
+ export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
47
+
48
+ const CANONICAL_KEY = "input";
49
+
50
+ /** The buffer ends inside a token: what follows can still change the answer. */
51
+ const HOLD = -1;
52
+ /** `JSON.parse` will reject this text no matter what is appended to it. */
53
+ const NEVER = -2;
54
+
55
+ export type FreeformWrapperScan =
56
+ | { kind: "hold"; parse: boolean }
57
+ | { kind: "raw" }
58
+ | { kind: "input"; valueStart: number };
59
+
60
+ /** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
61
+ function skipWhitespace(text: string, from: number): number {
62
+ let i = from;
63
+ while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
64
+ return i < text.length ? i : HOLD;
65
+ }
66
+
67
+ /**
68
+ * Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
69
+ *
70
+ * This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
71
+ * fail on a truncated string and the other has to return what it decoded so far. They answer
72
+ * different questions about the same bytes and share one escape table so they cannot disagree
73
+ * about which escapes exist.
74
+ */
75
+ function scanString(text: string, from: number): number {
76
+ let i = from + 1;
77
+ while (i < text.length) {
78
+ const c = text[i]!;
79
+ if (c === '"') return i + 1;
80
+ if (c === "\\") {
81
+ const n = text[i + 1];
82
+ if (n === undefined) return HOLD;
83
+ if (n === "u") {
84
+ const hex = text.slice(i + 2, i + 6);
85
+ if (hex.length < 4) return HOLD;
86
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
87
+ i += 6;
88
+ continue;
89
+ }
90
+ if (!JSON_ESCAPES.has(n)) return NEVER;
91
+ i += 2;
92
+ continue;
93
+ }
94
+ // A literal control character is not legal inside a JSON string.
95
+ if (c.charCodeAt(0) <= 0x1f) return NEVER;
96
+ i++;
97
+ }
98
+ return HOLD;
99
+ }
100
+
101
+ const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
102
+ const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
103
+
104
+ /** Index just past one complete scalar at `from`, or HOLD / NEVER. */
105
+ function scanScalar(text: string, from: number): number {
106
+ const c = text[from]!;
107
+ if (c === '"') return scanString(text, from);
108
+ if (c === "t" || c === "f" || c === "n") {
109
+ const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
110
+ if (text.startsWith(literal, from)) return from + literal.length;
111
+ return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
112
+ }
113
+ let end = from;
114
+ while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
115
+ if (end === from) return NEVER;
116
+ // A number running to the end of the buffer can still grow another digit or exponent.
117
+ if (end >= text.length) return HOLD;
118
+ return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
119
+ }
120
+
121
+ /** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
122
+ function scanMemberKey(text: string, from: number): number {
123
+ if (text[from] !== '"') return NEVER;
124
+ const nameEnd = scanString(text, from);
125
+ if (nameEnd < 0) return nameEnd;
126
+ const colon = skipWhitespace(text, nameEnd);
127
+ if (colon === HOLD) return HOLD;
128
+ if (text[colon] !== ":") return NEVER;
129
+ return skipWhitespace(text, colon + 1);
130
+ }
131
+
132
+ /**
133
+ * Index just past one complete JSON value at `from`, or HOLD / NEVER.
134
+ *
135
+ * Containers are walked with an explicit stack rather than recursion: the value being skipped
136
+ * is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
137
+ */
138
+ function scanValue(text: string, from: number): number {
139
+ const closers: string[] = [];
140
+ let i = from;
141
+ for (;;) {
142
+ const c = text[i]!;
143
+ if (c === "{" || c === "[") {
144
+ const closer = c === "{" ? "}" : "]";
145
+ closers.push(closer);
146
+ const first = skipWhitespace(text, i + 1);
147
+ if (first === HOLD) return HOLD;
148
+ if (text[first] === closer) {
149
+ closers.pop();
150
+ i = first + 1;
151
+ } else if (closer === "}") {
152
+ const value = scanMemberKey(text, first);
153
+ if (value < 0) return value;
154
+ i = value;
155
+ continue;
156
+ } else {
157
+ i = first;
158
+ continue;
159
+ }
160
+ } else {
161
+ const end = scanScalar(text, i);
162
+ if (end < 0) return end;
163
+ i = end;
164
+ }
165
+
166
+ // One value is complete: close whatever it finished and find the next value, if any.
167
+ for (;;) {
168
+ if (closers.length === 0) return i;
169
+ const at = skipWhitespace(text, i);
170
+ if (at === HOLD) return HOLD;
171
+ const closer = closers[closers.length - 1]!;
172
+ if (text[at] === closer) {
173
+ closers.pop();
174
+ i = at + 1;
175
+ continue;
176
+ }
177
+ if (text[at] !== ",") return NEVER;
178
+ const next = skipWhitespace(text, at + 1);
179
+ if (next === HOLD) return HOLD;
180
+ if (closer === "}") {
181
+ const value = scanMemberKey(text, next);
182
+ if (value < 0) return value;
183
+ i = value;
184
+ } else {
185
+ i = next;
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Which wrapper the completed text will unwrap to, as far as this prefix can say.
194
+ *
195
+ * Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
196
+ * carries a string, and a second one can still arrive, so no prefix decides them — which makes
197
+ * them indistinguishable from any other undecided object and lets one HOLD cover both.
198
+ */
199
+ export function scanFreeformWrapper(text: string): FreeformWrapperScan {
200
+ // One clamp rather than a budget threaded through every helper. Every helper already holds
201
+ // when it runs off the end of what it can see, so a buffer whose classification needs more
202
+ // than this holds for exactly the right reason, and no scan can cost more than this many
203
+ // characters however large the arguments grow. Indices into the clamp are indices into the
204
+ // full text, because the clamp is a prefix of it.
205
+ const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
206
+ ? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
207
+ : text;
208
+ // `parse` is true only where this scan actually SAW the object close. Every other hold ran
209
+ // out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
210
+ // possible payoff: the first is provably incomplete, and the second would re-read a growing
211
+ // buffer on every delta that happens to end in a brace — repeated braces inside a long
212
+ // unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
213
+ // costs nothing that matters, because the value it would release arrives in the same instant
214
+ // as the authoritative completion that follows it.
215
+ const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
216
+ const open = skipWhitespace(bounded, 0);
217
+ if (open === HOLD) return hold();
218
+ // Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
219
+ // complete as themselves.
220
+ if (bounded[open] !== "{") return { kind: "raw" };
221
+
222
+ let i = open + 1;
223
+ for (;;) {
224
+ const at = skipWhitespace(bounded, i);
225
+ if (at === HOLD) return hold();
226
+ if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
227
+ if (bounded[at] !== '"') return { kind: "raw" };
228
+
229
+ const nameEnd = scanString(bounded, at);
230
+ if (nameEnd === HOLD) return hold();
231
+ if (nameEnd === NEVER) return { kind: "raw" };
232
+ let name: string;
233
+ try {
234
+ // The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
235
+ // key completion sees. Decoding it by hand here is the second implementation that would
236
+ // drift from `JSON.parse`, which is how this defect existed in the first place.
237
+ name = JSON.parse(bounded.slice(at, nameEnd)) as string;
238
+ } catch {
239
+ return { kind: "raw" };
240
+ }
241
+
242
+ const colon = skipWhitespace(bounded, nameEnd);
243
+ if (colon === HOLD) return hold();
244
+ if (bounded[colon] !== ":") return { kind: "raw" };
245
+ const valueAt = skipWhitespace(bounded, colon + 1);
246
+ if (valueAt === HOLD) return hold();
247
+
248
+ if (name === CANONICAL_KEY) {
249
+ // An own `input` wins over everything else in the object, so this is decidable now. A
250
+ // non-string value is decidable too, in the other direction: completion hands back the
251
+ // argument text unchanged rather than unwrapping a value it cannot use.
252
+ return bounded[valueAt] === '"'
253
+ ? { kind: "input", valueStart: valueAt + 1 }
254
+ : { kind: "raw" };
255
+ }
256
+
257
+ const valueEnd = scanValue(bounded, valueAt);
258
+ if (valueEnd === HOLD) return hold();
259
+ if (valueEnd === NEVER) return { kind: "raw" };
260
+
261
+ const next = skipWhitespace(bounded, valueEnd);
262
+ if (next === HOLD) return hold();
263
+ if (bounded[next] === ",") {
264
+ i = next + 1;
265
+ continue;
266
+ }
267
+ if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
268
+ return { kind: "raw" };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The object closed without a canonical key. Only whitespace may follow one that parses, so
274
+ * anything else makes the text raw; otherwise the completed parse decides between a fallback
275
+ * wrapper and no wrapper at all.
276
+ */
277
+ function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
278
+ return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
279
+ }
@@ -33,6 +33,38 @@ function namespaceOf(value: unknown): string | undefined {
33
33
  return typeof value === "string" && value !== "functions" ? value : undefined;
34
34
  }
35
35
 
36
+ /**
37
+ * Repair one provider spelling drift for Codex's flat shell bridge.
38
+ *
39
+ * This is schema-bound rather than a general alias: `input` is also a legitimate
40
+ * argument name for arbitrary caller and MCP tools. Only the exact bare
41
+ * `exec_command` declaration can establish that its sole string input has one faithful
42
+ * reading as the required string `cmd` member.
43
+ */
44
+ function repairExecCommandInput(
45
+ argumentsText: string,
46
+ schema: FunctionCallRepairSchema,
47
+ ): string {
48
+ if (schema.namespace !== undefined || schema.name !== "exec_command") return argumentsText;
49
+ const parameters = schema.parameters;
50
+ if (!isObject(parameters) || parameters.type !== "object"
51
+ || !Array.isArray(parameters.required) || !parameters.required.includes("cmd")
52
+ || !isObject(parameters.properties) || !isObject(parameters.properties.cmd)
53
+ || parameters.properties.cmd.type !== "string") return argumentsText;
54
+ let parsed: unknown;
55
+ try {
56
+ parsed = JSON.parse(argumentsText);
57
+ } catch {
58
+ return argumentsText;
59
+ }
60
+ if (!isObject(parsed)) return argumentsText;
61
+ const keys = Object.keys(parsed);
62
+ if (keys.length !== 1 || keys[0] !== "input" || typeof parsed.input !== "string") {
63
+ return argumentsText;
64
+ }
65
+ return JSON.stringify({ cmd: parsed.input });
66
+ }
67
+
36
68
  function selectorAllows(
37
69
  selector: unknown,
38
70
  lowered: unknown,
@@ -131,7 +163,12 @@ function repairItem(item: unknown, schemas: FunctionCallRepairSchemas, completed
131
163
  if (unsafe) return item;
132
164
  } catch { return item; }
133
165
  }
134
- const argumentsText = coerceIntegerToolArguments(raw || "{}", schema.parameters, schema.namespace ? undefined : schema.name);
166
+ const integerRepaired = coerceIntegerToolArguments(
167
+ raw || "{}",
168
+ schema.parameters,
169
+ schema.namespace ? undefined : schema.name,
170
+ );
171
+ const argumentsText = repairExecCommandInput(integerRepaired, schema);
135
172
  return argumentsText === raw ? item : { ...item, arguments: argumentsText };
136
173
  }
137
174
 
@@ -0,0 +1,65 @@
1
+ import type { OcxDocumentContent } from "../types";
2
+
3
+ const DATA_URL = /^data:([^;,]+)((?:;[^;,]*)*),(.*)$/s;
4
+ const BASE64_PAYLOAD = /^[A-Za-z0-9+/]+={0,2}$/;
5
+
6
+ function usableBase64(payload: string): boolean {
7
+ if (!BASE64_PAYLOAD.test(payload)) return false;
8
+ // Preserve valid unpadded encodings, but a single sextet cannot represent any byte.
9
+ // Padding, when present, must complete a four-character quantum. No payload allocation.
10
+ return payload.endsWith("=") ? payload.length % 4 === 0 : payload.length % 4 !== 1;
11
+ }
12
+
13
+ /** The one marker vocabulary for an attached document, derived from what the part knows. */
14
+ export function inlineDocumentMarker(filename: string | undefined): string {
15
+ return filename !== undefined && filename.length > 0 ? `[document: ${filename}]` : "[document]";
16
+ }
17
+
18
+ /**
19
+ * An inline document part from a `data:` URL, or `undefined` when there are no usable bytes.
20
+ *
21
+ * A reference with no payload — a `file_id`, a bare filename — is deliberately not a document:
22
+ * there is nothing to carry, and minting a part for it would claim an attachment the request
23
+ * never contained. Those keep the marker path they already had.
24
+ */
25
+ export function inlineDocumentFromDataUrl(
26
+ fileData: string | undefined,
27
+ filename: string | undefined,
28
+ ): OcxDocumentContent | undefined {
29
+ if (typeof fileData !== "string" || fileData.length === 0) return undefined;
30
+ const match = DATA_URL.exec(fileData);
31
+ if (!match) return undefined;
32
+ const mediaType = match[1]!;
33
+ const payload = match[3]!;
34
+ if (!hasBase64Parameter(match[2] ?? "") || !usableBase64(payload)) return undefined;
35
+ return {
36
+ type: "document",
37
+ text: inlineDocumentMarker(filename),
38
+ mediaType,
39
+ data: payload,
40
+ ...(filename !== undefined && filename.length > 0 ? { filename } : {}),
41
+ };
42
+ }
43
+
44
+ /** The `data:` URL spelling of a document part, for wires whose counterpart takes one. */
45
+ export function inlineDocumentDataUrl(part: OcxDocumentContent): string {
46
+ return `data:${part.mediaType};base64,${part.data}`;
47
+ }
48
+
49
+ /**
50
+ * Whether this string is a `data:` URL this module would actually decode.
51
+ *
52
+ * The media-type scanner and this parser have to agree exactly. A looser scanner would exempt an
53
+ * attachment from the untranslated-media refusal that the parser then reduces to a marker, which
54
+ * is the silent drop the refusal exists to prevent. `;base64` must be a whole parameter token,
55
+ * not a substring of one: `;notbase64,` and `;x=base64,` are not base64 payloads.
56
+ */
57
+ export function isInlineDocumentDataUrl(value: unknown): boolean {
58
+ if (typeof value !== "string" || value.length === 0) return false;
59
+ const match = DATA_URL.exec(value);
60
+ return match !== null && hasBase64Parameter(match[2] ?? "") && usableBase64(match[3]!);
61
+ }
62
+
63
+ function hasBase64Parameter(parameters: string): boolean {
64
+ return parameters.split(";").includes("base64");
65
+ }
@@ -1,3 +1,5 @@
1
+ import { isInlineDocumentDataUrl } from "./inline-document";
2
+
1
3
  /** Input kinds for which the normalized request has no lossless content carrier. */
2
4
  export type UntranslatedInputMedia = "audio" | "file";
3
5
 
@@ -7,20 +9,47 @@ function isRecord(value: unknown): value is RecordValue {
7
9
  return value !== null && typeof value === "object" && !Array.isArray(value);
8
10
  }
9
11
 
10
- function mediaKind(value: unknown): UntranslatedInputMedia | undefined {
12
+ /**
13
+ * Whether the position being scanned has a converter that builds a document part.
14
+ *
15
+ * Only user content does. A tool output, a system or assistant message, and a Chat `developer`
16
+ * message are all flattened to text by their converters, so exempting an attachment there would
17
+ * turn today's explicit refusal into the silent drop this scanner exists to prevent.
18
+ */
19
+ type CarrierPosition = "user-content" | "flattened";
20
+
21
+ function mediaKind(value: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
11
22
  if (!isRecord(value)) return undefined;
12
23
  if (value.type === "input_audio" || value.type === "audio") return "audio";
13
- if (value.type === "input_file" || value.type === "file" || value.type === "document") return "file";
24
+ if (value.type === "input_file" || value.type === "file" || value.type === "document") {
25
+ // A document that carries its own bytes has a lossless carrier in user content, so refusing
26
+ // it there would reject the very request #5212 exists to preserve. A reference with no
27
+ // payload has none anywhere: translated adapters cannot dereference a file_id or a remote
28
+ // source.
29
+ return position === "user-content" && carriesInlineDocumentBytes(value) ? undefined : "file";
30
+ }
14
31
  // A file-id-only image is not pixels: translated adapters cannot dereference it.
15
32
  if (value.type === "input_image" && typeof value.file_id === "string" && value.file_id.length > 0
16
33
  && !(typeof value.image_url === "string" && value.image_url.length > 0)) return "file";
17
34
  return undefined;
18
35
  }
19
36
 
20
- function contentMedia(content: unknown): UntranslatedInputMedia | undefined {
37
+ /** Presence of a base64 payload only. No payload is read, decoded, copied or returned. */
38
+ function carriesInlineDocumentBytes(value: RecordValue): boolean {
39
+ if (isInlineDocumentDataUrl(value.file_data)) return true;
40
+ // Chat Completions nests the payload under `file`.
41
+ if (isRecord(value.file) && isInlineDocumentDataUrl(value.file.file_data)) return true;
42
+ // Anthropic nests it under a base64 `source`.
43
+ return isRecord(value.source)
44
+ && value.source.type === "base64"
45
+ && typeof value.source.data === "string"
46
+ && value.source.data.length > 0;
47
+ }
48
+
49
+ function contentMedia(content: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
21
50
  if (!Array.isArray(content)) return undefined;
22
51
  for (const part of content) {
23
- const kind = mediaKind(part);
52
+ const kind = mediaKind(part, position);
24
53
  if (kind) return kind;
25
54
  }
26
55
  return undefined;
@@ -35,13 +64,16 @@ export function untranslatedResponsesInputMedia(body: unknown): UntranslatedInpu
35
64
  if (!isRecord(body) || !Array.isArray(body.input)) return undefined;
36
65
  for (const item of body.input) {
37
66
  if (!isRecord(item)) continue;
38
- const direct = mediaKind(item);
67
+ const direct = mediaKind(item, "flattened");
39
68
  if (direct) return direct;
40
69
  if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
41
- const kind = contentMedia(item.output);
70
+ const kind = contentMedia(item.output, "flattened");
42
71
  if (kind) return kind;
43
72
  } else if (item.type === "message" || item.type === undefined) {
44
- const kind = contentMedia(item.content);
73
+ // `inputContentParts` runs for user and developer messages only; a system message is
74
+ // flattened to text by the parser.
75
+ const role = item.role;
76
+ const kind = contentMedia(item.content, role === "user" || role === "developer" ? "user-content" : "flattened");
45
77
  if (kind) return kind;
46
78
  }
47
79
  }
@@ -53,7 +85,9 @@ export function untranslatedChatInputMedia(body: unknown): UntranslatedInputMedi
53
85
  if (!isRecord(body) || !Array.isArray(body.messages)) return undefined;
54
86
  for (const message of body.messages) {
55
87
  if (!isRecord(message)) continue;
56
- const kind = contentMedia(message.content);
88
+ // Only the `user` branch of the Chat projection builds content blocks. `system`,
89
+ // `developer`, `assistant` and `tool` all reduce their content to a string.
90
+ const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened");
57
91
  if (kind) return kind;
58
92
  }
59
93
  return undefined;
@@ -0,0 +1,134 @@
1
+ import { collectResponsesToolGroups } from "./tool-groups";
2
+ import { isSchemaValidResponsesToolName, wireToolInnerName } from "./tool-name-aliases";
3
+ import { CODE_MODE_HELPER_WIRE_NAMES, dottedToolName, namespacedToolName } from "../types";
4
+
5
+ /**
6
+ * The one invented namespace this proxy is known to have emitted. Routed providers flatten a
7
+ * namespaced call as `ns.name`, and a provider that invents the grouping picks `default`; #3402,
8
+ * #4176 and #4412 all repaired that spelling on the way IN, each on its own path.
9
+ *
10
+ * This is deliberately one literal prefix rather than a rule. "Strip everything before the first
11
+ * dot" would silently rewrite a legitimate tool name that happens to contain a dot in some other
12
+ * provider's vocabulary, and a replayed history item is the worst possible place to guess: it names
13
+ * a call that already happened.
14
+ */
15
+ const LEGACY_INVENTED_NAMESPACE_PREFIX = "default.";
16
+
17
+ /** Item types whose `name` the upstream validates against the tool-name pattern. */
18
+ const NAMED_CALL_ITEM_TYPES = new Set(["function_call", "custom_tool_call"]);
19
+
20
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
21
+ return !!value && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+
24
+ type DeclarationView = {
25
+ /** Wire spellings the caller's own catalog declares, including flattened namespace forms. */
26
+ readonly spellings: ReadonlySet<string>;
27
+ /** How many declared identities answer to a given inner name. */
28
+ readonly ownersByInnerName: ReadonlyMap<string, number>;
29
+ };
30
+
31
+ /**
32
+ * Read the request's own catalog. Namespace children contribute their inner name and both
33
+ * flattened spellings, which is what makes a declared `default` namespace distinguishable from an
34
+ * invented one.
35
+ */
36
+ function readDeclarations(body: unknown): DeclarationView {
37
+ const spellings = new Set<string>();
38
+ const owners = new Map<string, number>();
39
+ const identities = new Map<string, Set<string>>();
40
+ const claim = (innerName: string, identity: string): void => {
41
+ const seen = identities.get(innerName);
42
+ if (seen === undefined) identities.set(innerName, new Set([identity]));
43
+ else seen.add(identity);
44
+ };
45
+ for (const group of collectResponsesToolGroups(body)) {
46
+ for (const spec of group) {
47
+ if (!isPlainObject(spec)) continue;
48
+ if (spec.type === "namespace" && Array.isArray(spec.tools)) {
49
+ const namespace = typeof spec.name === "string" ? spec.name : undefined;
50
+ if (namespace === undefined) continue;
51
+ for (const child of spec.tools) {
52
+ const inner = wireToolInnerName(child);
53
+ if (!inner) continue;
54
+ spellings.add(namespacedToolName(namespace, inner));
55
+ spellings.add(dottedToolName(namespace, inner));
56
+ claim(inner, JSON.stringify([namespace, inner]));
57
+ }
58
+ continue;
59
+ }
60
+ const name = wireToolInnerName(spec);
61
+ if (!name) continue;
62
+ spellings.add(name);
63
+ claim(name, JSON.stringify([null, name]));
64
+ }
65
+ }
66
+ for (const [inner, owning] of identities) owners.set(inner, owning.size);
67
+ return { spellings, ownersByInnerName: owners };
68
+ }
69
+
70
+ /**
71
+ * Canonical name a legacy dotted alias should be replayed under, or undefined to leave it alone.
72
+ *
73
+ * Fail-closed in both directions that matter. A dotted spelling the caller's catalog actually
74
+ * declares is a real tool identity, so it is never stripped — rewriting it would change which tool
75
+ * the history says was called. A suffix claimed by two declared identities is ambiguous and is
76
+ * left as it is. Only two things resolve: a suffix that names exactly one declared tool, and the
77
+ * bounded set of code-mode helper spellings, which a code-mode catalog never declares because they
78
+ * exist only as nested `tools.*` helpers inside `exec`.
79
+ */
80
+ function repairedCallName(name: string, declarations: DeclarationView): string | undefined {
81
+ if (isSchemaValidResponsesToolName(name)) return undefined;
82
+ if (!name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)) return undefined;
83
+ if (declarations.spellings.has(name)) return undefined;
84
+ const suffix = name.slice(LEGACY_INVENTED_NAMESPACE_PREFIX.length);
85
+ if (!isSchemaValidResponsesToolName(suffix)) return undefined;
86
+ const owners = declarations.ownersByInnerName.get(suffix);
87
+ if (owners === 1) return suffix;
88
+ if (owners !== undefined) return undefined;
89
+ return CODE_MODE_HELPER_WIRE_NAMES.has(suffix) ? suffix : undefined;
90
+ }
91
+
92
+ function repairedItem(item: unknown, declarations: DeclarationView): unknown {
93
+ if (!isPlainObject(item)) return item;
94
+ if (typeof item.type !== "string" || !NAMED_CALL_ITEM_TYPES.has(item.type)) return item;
95
+ // An item carrying an explicit namespace is a namespaced identity whose `name` is the inner
96
+ // name; it is not the flattened shape this repairs, and its own field is the authority.
97
+ if (item.namespace !== undefined) return item;
98
+ if (typeof item.name !== "string") return item;
99
+ const repaired = repairedCallName(item.name, declarations);
100
+ return repaired === undefined ? item : { ...item, name: repaired };
101
+ }
102
+
103
+ /**
104
+ * Make an already-damaged conversation replayable.
105
+ *
106
+ * A `function_call` whose name violates `^[a-zA-Z0-9_-]+$` is not a one-turn failure: Codex stores
107
+ * the item, and the upstream then refuses EVERY later request that replays it
108
+ * (`Invalid 'input[N].name': ...`) — including the automatic compaction a long task or a side chat
109
+ * triggers. The emit-side fix stops new items from being created; without this the conversations
110
+ * that already contain one stay permanently unusable, which is the part of #5095 that actually
111
+ * hurts.
112
+ *
113
+ * Only replayed input items are touched. The caller's tool catalog is never rewritten, and a body
114
+ * with nothing to repair is returned by reference.
115
+ */
116
+ export function repairLegacyDottedToolCallNames(body: unknown): unknown {
117
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
118
+ if (!body.input.some(item =>
119
+ isPlainObject(item)
120
+ && typeof item.type === "string"
121
+ && NAMED_CALL_ITEM_TYPES.has(item.type)
122
+ && typeof item.name === "string"
123
+ && item.name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)
124
+ )) return body;
125
+
126
+ const declarations = readDeclarations(body);
127
+ let changed = false;
128
+ const input = body.input.map(item => {
129
+ const repaired = repairedItem(item, declarations);
130
+ if (repaired !== item) changed = true;
131
+ return repaired;
132
+ });
133
+ return changed ? { ...body, input } : body;
134
+ }
@@ -8,6 +8,25 @@ const MUSE_SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
8
8
 
9
9
  export type MuseToolNameAliases = ReadonlyMap<string, string>;
10
10
 
11
+ /**
12
+ * Final wire name for an identity before the Muse length rewrite.
13
+ *
14
+ * The alias map is request-local and already narrowed by `tool_choice`. Returning undefined for
15
+ * a contradictory map keeps downstream identity checks fail-closed instead of choosing one alias.
16
+ */
17
+ export function museWireNameForOriginal(
18
+ originalName: string,
19
+ aliases: MuseToolNameAliases,
20
+ ): string | undefined {
21
+ let wireName: string | undefined;
22
+ for (const [candidate, original] of aliases) {
23
+ if (original !== originalName) continue;
24
+ if (wireName !== undefined && wireName !== candidate) return undefined;
25
+ wireName = candidate;
26
+ }
27
+ return wireName ?? originalName;
28
+ }
29
+
11
30
  function isPlainObject(value: unknown): value is Record<string, unknown> {
12
31
  return !!value && typeof value === "object" && !Array.isArray(value);
13
32
  }
@@ -1,4 +1,5 @@
1
1
  import type { OcxContentPart, OcxTextContent } from "../types";
2
+ import { inlineDocumentFromDataUrl } from "./inline-document";
2
3
 
3
4
  export function isObj(v: unknown): v is Record<string, unknown> {
4
5
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -90,8 +91,13 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
90
91
  if (fileId) {
91
92
  parts.push({ type: "text", text: `[file: ${fileId}]` });
92
93
  } else if (fileData) {
93
- // Inline file_data is often large base64. Preserve only its presence and name, never bytes.
94
- parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
94
+ // Inline bytes used to be reduced to a name here, which meant no adapter could forward
95
+ // the attachment even to a target that has a representation for it, and a title-only
96
+ // forward came back as a confident answer about a document the model never saw (#5212).
97
+ // The marker survives on the part for every wire that still cannot carry one. Bytes with
98
+ // no declared media type are not decodable into a document and keep the marker they had.
99
+ const document = inlineDocumentFromDataUrl(fileData, filename);
100
+ parts.push(document ?? { type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
95
101
  }
96
102
  // A bare filename is not a file resource in the Responses schema, so omit it rather than
97
103
  // fabricating a "[file: ...]" marker for an attachment that was never sent.