@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
@@ -1,5 +1,5 @@
1
1
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
- import { debugDroppedFrame } from "../lib/debug";
2
+ import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
3
3
  import { createToolCallIdAllocator } from "./tool-call-id";
4
4
  import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
5
5
  import type {
@@ -15,13 +15,16 @@ import type {
15
15
  OcxUsage,
16
16
  } from "../types";
17
17
  import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
18
+ import type { OcxTool } from "../types";
18
19
  import { contentPartsToText, parseDataUrl } from "./image";
19
20
  import { getVertexAccessToken } from "../lib/gcp-adc";
20
21
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
21
22
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
22
23
  import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
23
- import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
24
+ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionAnchor, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
25
+ import { summarizeGoogleWireShape } from "./google-wire-shape";
24
26
  import { compileGoogleWireBody } from "./google-wire-compiler";
27
+ import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema";
25
28
  import { identifyRoutedModel } from "./identity";
26
29
  import {
27
30
  antigravityUsesReplayCache,
@@ -344,6 +347,13 @@ function messagesToGeminiFormat(
344
347
  parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
345
348
  continue;
346
349
  }
350
+ if (p.type === "document") {
351
+ // Gemini takes document bytes through the same inline_data part as images and
352
+ // video. The marker on the part is the fallback for wires without one, not this
353
+ // wire's best effort (#5212).
354
+ parts.push({ inline_data: { mime_type: p.mediaType, data: p.data } });
355
+ continue;
356
+ }
347
357
  // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
348
358
  const textPart = geminiTextPart(p.text);
349
359
  if (textPart) parts.push(textPart);
@@ -463,9 +473,7 @@ function messagesToGeminiFormat(
463
473
 
464
474
  function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
465
475
  if (!parsed.context.tools?.length) return undefined;
466
- const tools = isAllowedToolChoice(parsed.options.toolChoice)
467
- ? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
468
- : parsed.context.tools;
476
+ const tools = advertisedGeminiTools(parsed);
469
477
  if (tools.length === 0) return undefined;
470
478
  return [{
471
479
  functionDeclarations: tools.map(t => ({
@@ -476,19 +484,37 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
476
484
  }];
477
485
  }
478
486
 
487
+ /** The declarations this request actually advertises, after any allowed-tools filter. */
488
+ function advertisedGeminiTools(parsed: OcxParsedRequest): readonly OcxTool[] {
489
+ const declared = parsed.context.tools ?? [];
490
+ return isAllowedToolChoice(parsed.options.toolChoice)
491
+ ? declared.filter(toolChoiceToolPredicate(parsed.options.toolChoice, declared))
492
+ : declared;
493
+ }
494
+
479
495
  /**
480
496
  * Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
481
497
  * prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
482
498
  * so the common case is byte-identical. The allowedTools variant already filters the
483
499
  * declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
500
+ *
501
+ * A caller that declares strict tools is asking for its argument schemas to be enforced, and
502
+ * Gemini expresses that as VALIDATED. The mode existed and was plumbed end to end, but was only
503
+ * ever reachable by matching a model name, so a strict declaration arrived as an ordinary
504
+ * unvalidated AUTO turn and the response looked the same either way (#5210). VALIDATED replaces
505
+ * AUTO only: ANY and NONE are stronger constraints the caller asked for explicitly, and
506
+ * overwriting either of them would lose the choice this function exists to enforce.
484
507
  */
485
508
  function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
486
509
  const choice = parsed.options.toolChoice;
487
- if (!choice || choice === "auto") return undefined;
510
+ const validated = advertisedGeminiTools(parsed).some(t => t.strict === true)
511
+ ? { functionCallingConfig: { mode: "VALIDATED" } }
512
+ : undefined;
513
+ if (!choice || choice === "auto") return validated;
488
514
  if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
489
515
  if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
490
516
  if (isAllowedToolChoice(choice)) {
491
- return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
517
+ return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : validated;
492
518
  }
493
519
  return {
494
520
  functionCallingConfig: {
@@ -737,6 +763,14 @@ function invalidGoogleShapeEvent(
737
763
  }
738
764
 
739
765
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
766
+ const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
767
+ const toolSchemaProfile = {
768
+ endpointClass: provider.googleMode ?? "ai-studio",
769
+ } satisfies GoogleToolSchemaProfile;
770
+ const reportToolSchemaLoss = (report: GoogleToolSchemaLossReport): void => {
771
+ if (!report.lossy && report.uncertainComparisons === 0) return;
772
+ debugProviderDiagnosticLazy("google", "google-tool-schema-loss", () => ({ ...report }));
773
+ };
740
774
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
741
775
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
742
776
  let antigravityModel: string | undefined;
@@ -799,7 +833,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
799
833
  ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
800
834
  ? {
801
835
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
802
- (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
836
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
837
+ request,
838
+ ctx,
839
+ { toolSchemaProfile, toolSchemaPolicy },
840
+ ),
803
841
  formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
804
842
  (provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
805
843
  }
@@ -970,7 +1008,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
970
1008
  const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
971
1009
  draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
972
1010
  }
973
- const compiled = compileGoogleWireBody(draftRequest);
1011
+ const compiled = compileGoogleWireBody(draftRequest, toolSchemaProfile, toolSchemaPolicy);
1012
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
974
1013
  const request = compiled.body;
975
1014
  restoreGoogleToolName = compiled.restoreToolName;
976
1015
  // Compile names before replay: signatures are keyed by the exact provider-visible name.
@@ -989,6 +1028,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
989
1028
  // Vertex and AI Studio share one decision. A second check here would append a
990
1029
  // duplicate nudge whenever signature sanitization reshapes the tail afterwards.
991
1030
  }
1031
+ // Opt-in structural description of the request that is about to leave (#5008).
1032
+ //
1033
+ // Passed as a BUILDER, not a value: the lazy form gates before invoking it, so a session
1034
+ // with provider debug off never pays the walk, and it evaluates the projection inside its
1035
+ // own try/catch, so a throw in here cannot turn a built request into a rejected one. The
1036
+ // projection only reads `request`, so the bytes below are the same either way.
1037
+ debugProviderDiagnosticLazy("google", "antigravity-wire-shape", () => summarizeGoogleWireShape(request, {
1038
+ sessionAnchor: antigravitySessionAnchor(parsed),
1039
+ // Signed at translation time, from client history or the durable store. The
1040
+ // Antigravity session cache signs afterwards, inside applyAntigravityReplay, and the
1041
+ // projection attributes that remainder to the cache rather than to this count.
1042
+ historySignedCalls: replayedCallIds.length,
1043
+ replayScopeBound: parsed._reasoningReplayScope !== undefined,
1044
+ }));
992
1045
  const envelope = {
993
1046
  model: wireModelId,
994
1047
  // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
@@ -1006,7 +1059,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1006
1059
  }
1007
1060
 
1008
1061
  if (provider.googleMode === "vertex") {
1009
- const compiled = compileGoogleWireBody(body);
1062
+ const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1063
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
1010
1064
  restoreGoogleToolName = compiled.restoreToolName;
1011
1065
  const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
1012
1066
  const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
@@ -1052,7 +1106,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1052
1106
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
1053
1107
  headers["x-goog-api-key"] = apiKey;
1054
1108
 
1055
- const compiled = compileGoogleWireBody(body);
1109
+ const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1110
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
1056
1111
  restoreGoogleToolName = compiled.restoreToolName;
1057
1112
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1058
1113
  },
@@ -18,6 +18,9 @@ export function parseDataUrl(url: string): { mediaType: string; base64: string }
18
18
  */
19
19
  export function contentPartsToText(content: string | OcxContentPart[]): string {
20
20
  if (typeof content === "string") return content;
21
- const text = content.map(p => p.type === "text" ? p.text : p.type === "image" ? "[image]" : "[video]").join("");
21
+ // A document carries its own marker, so this wire states the attachment instead of
22
+ // mislabelling it as a video.
23
+ const text = content.map(p =>
24
+ p.type === "text" || p.type === "document" ? p.text : p.type === "image" ? "[image]" : "[video]").join("");
22
25
  return text || "[image]";
23
26
  }
@@ -1,31 +1,45 @@
1
1
  import type { ProviderAdapter } from "./base";
2
2
  import { untranslatedInputMediaMessage, untranslatedResponsesInputMedia } from "../responses/input-media";
3
+ import { unrepresentableDeclaration } from "./declaration-carrier";
4
+ import type { AdapterWire } from "./registry";
5
+ import type { OcxParsedRequest } from "../types";
3
6
 
4
7
  /**
5
8
  * Refuse unrepresentable input at the final translated-adapter boundary. The registry
6
9
  * applies this after wire resolution; Responses passthrough (including Azure) opts
7
10
  * out because it uses the original body rather than the lossy normalized content.
11
+ *
12
+ * Two of the three checks are wire-scoped, and that is the point. A constraint the normalized
13
+ * request CAN carry — a caller restriction on a tool, an attached document's bytes — still has
14
+ * to reach a wire that can express it. Leaving that to each adapter means an adapter that never
15
+ * learned about the carrier rebuilds without it and answers normally, so the allowlist in
16
+ * `declaration-carrier.ts` is default-deny and this is the one place every registered adapter
17
+ * passes through.
8
18
  */
9
- export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T): T {
19
+ export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T, wire: AdapterWire): T {
20
+ const refusal = (parsed: OcxParsedRequest): string | undefined => {
21
+ const kind = untranslatedResponsesInputMedia(parsed._rawBody);
22
+ return kind ? untranslatedInputMediaMessage(kind) : unrepresentableDeclaration(parsed, wire);
23
+ };
10
24
  const build = adapter.buildRequest.bind(adapter);
11
25
  adapter.buildRequest = (parsed, incoming) => {
12
- const kind = untranslatedResponsesInputMedia(parsed._rawBody);
13
- if (kind) throw new Error(untranslatedInputMediaMessage(kind));
26
+ const message = refusal(parsed);
27
+ if (message) throw new Error(message);
14
28
  return build(parsed, incoming);
15
29
  };
16
30
 
17
31
  const runTurn = adapter.runTurn?.bind(adapter);
18
32
  if (runTurn) {
19
33
  adapter.runTurn = async (parsed, incoming, emit) => {
20
- const kind = untranslatedResponsesInputMedia(parsed._rawBody);
21
- if (kind) {
34
+ const message = refusal(parsed);
35
+ if (message) {
22
36
  emit({
23
37
  type: "error",
24
38
  status: 400,
25
39
  errorType: "invalid_request_error",
26
40
  code: "unsupported_input_modality",
27
41
  retryable: false,
28
- message: untranslatedInputMediaMessage(kind),
42
+ message,
29
43
  });
30
44
  return;
31
45
  }
@@ -37,9 +51,7 @@ export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T): T {
37
51
  if (localTerminal) {
38
52
  // This hook is outside the builder's error catch. Decline its success shortcut;
39
53
  // the ordinary buildRequest path then returns the established client-safe 400.
40
- adapter.localTerminal = parsed => untranslatedResponsesInputMedia(parsed._rawBody)
41
- ? undefined
42
- : localTerminal(parsed);
54
+ adapter.localTerminal = parsed => refusal(parsed) ? undefined : localTerminal(parsed);
43
55
  }
44
56
  return adapter;
45
57
  }
@@ -12,14 +12,15 @@ import type { KiroHistoryEntry } from "./wire";
12
12
 
13
13
  export function userContentText(content: string | OcxContentPart[]): string {
14
14
  if (typeof content === "string") return content;
15
- return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
15
+ // A document carries its own marker: dropping it built an empty user turn that Kiro rejects.
16
+ return content.map(p => (p.type === "text" || p.type === "document" ? p.text : "")).filter(Boolean).join("\n");
16
17
  }
17
18
 
18
19
  export function usageContentText(content: string | OcxContentPart[]): string {
19
20
  if (typeof content === "string") return content;
20
21
  return content
21
22
  .map(p => {
22
- if (p.type === "text") return p.text;
23
+ if (p.type === "text" || p.type === "document") return p.text;
23
24
  if (p.type === "image") return `[image:${p.detail ?? "auto"}]`;
24
25
  return "";
25
26
  })
@@ -9,7 +9,7 @@ function contentText(content: string | OcxContentPart[]): string {
9
9
  if (typeof content === "string") return content;
10
10
  return content
11
11
  .map(part => {
12
- if (part.type === "text") return part.text;
12
+ if (part.type === "text" || part.type === "document") return part.text;
13
13
  if (part.type === "image") return `[image:${part.detail ?? "auto"}]`;
14
14
  return "";
15
15
  })
@@ -262,6 +262,12 @@ function contentToNative(
262
262
  text += part.text;
263
263
  continue;
264
264
  }
265
+ // No Ollama document carrier: keep the marker rather than falling through to the image
266
+ // branch below, which would read a nonexistent imageUrl.
267
+ if (part.type === "document") {
268
+ text += part.text;
269
+ continue;
270
+ }
265
271
  // Ollama's native /api/chat message shape carries `images: string[]` and has no video
266
272
  // counterpart, so a video part is refused rather than silently dropped or mis-sent as an image.
267
273
  if (part.type === "video") throw new Error(`ollama-native cannot send video content in ${label}`);
@@ -0,0 +1,61 @@
1
+ import type { OcxProviderConfig } from "../../types";
2
+
3
+ /**
4
+ * The role a `developer` message carries on the Chat wire, decided in one place for both the
5
+ * translated adapter and the native passthrough.
6
+ *
7
+ * `developer` belongs to the Chat Completions role set, but not every OpenAI-compatible gateway
8
+ * accepts it: one that does not answers `400 role 'developer' is not allowed` and the turn never
9
+ * starts. #5213 removed a hostname test that decided the role, which was right — a gateway
10
+ * proxying OpenAI accepts the role and the hostname cannot say so.
11
+ *
12
+ * `foldDeveloperRoleToSystem` is tri-state and only two of its states say anything about the
13
+ * destination: `true` records an upstream known to reject the role, `false` one known to accept
14
+ * it, and absent means nobody has recorded either. Returning `undefined` for the absent state
15
+ * instead of a role keeps the recorded fact separate from the default a route applies to
16
+ * silence, which is what lets the native route honour the first without inheriting the second.
17
+ *
18
+ * This decides the role and only the role. Which slot the message occupies is the caller's
19
+ * decision and must not change with this value.
20
+ */
21
+ export function explicitChatDeveloperWireRole(
22
+ provider: OcxProviderConfig,
23
+ ): "developer" | "system" | undefined {
24
+ if (provider.foldDeveloperRoleToSystem === undefined) return undefined;
25
+ return provider.foldDeveloperRoleToSystem ? "system" : "developer";
26
+ }
27
+
28
+ /**
29
+ * The translated route's role: the recorded one, or `system` when nothing is recorded.
30
+ *
31
+ * The unrecorded state folds because the two mistakes are not symmetrical. Sending `developer`
32
+ * to a destination that rejects it fails the request outside this repository, where no test here
33
+ * can reach it; sending `system` to one that would have accepted `developer` costs the role name
34
+ * and nothing else.
35
+ */
36
+ export function translatedChatDeveloperWireRole(provider: OcxProviderConfig): "developer" | "system" {
37
+ return explicitChatDeveloperWireRole(provider) ?? "system";
38
+ }
39
+
40
+ /**
41
+ * Apply a recorded role to a caller-supplied `messages` array, leaving every other field of
42
+ * every message, and the order of all of them, exactly as they arrived.
43
+ *
44
+ * The native route forwards the caller's messages verbatim, so an operator who recorded that a
45
+ * destination rejects the role still sent `developer` there and the turn failed upstream with a
46
+ * 400. Only an explicit record changes anything here: with the key unset, or set to the role the
47
+ * message already carries, the same array reference is returned and the wire is byte-identical
48
+ * to the one the caller sent.
49
+ */
50
+ export function applyExplicitChatDeveloperRole(messages: unknown, provider: OcxProviderConfig): unknown {
51
+ const role = explicitChatDeveloperWireRole(provider);
52
+ if (role === undefined || role === "developer" || !Array.isArray(messages)) return messages;
53
+ let rewritten = false;
54
+ const applied = messages.map(message => {
55
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
56
+ if ((message as { role?: unknown }).role !== "developer") return message;
57
+ rewritten = true;
58
+ return { ...(message as Record<string, unknown>), role };
59
+ });
60
+ return rewritten ? applied : messages;
61
+ }
@@ -1,13 +1,14 @@
1
1
  import { isNativeOpenAIChatTarget, stripBracketedModelSuffix } from "./wire";
2
2
  import { reasoningDetailSegmentForWire } from "./response-events";
3
+ import { translatedChatDeveloperWireRole } from "./developer-role";
3
4
  import { isVolcengineArkPaygChatTarget } from "./tool-schema";
4
5
  import { contentPartsToText } from "../image";
5
6
  import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
6
7
  import { identifyRoutedModel } from "../identity";
7
8
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
8
- import { registryEntryForProviderDestination } from "../../providers/registry";
9
9
  import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
10
- import type { OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
10
+ import { inlineDocumentDataUrl } from "../../responses/inline-document";
11
+ import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
11
12
  import { modelInList, namespacedToolName } from "../../types";
12
13
 
13
14
  /**
@@ -21,13 +22,6 @@ import { modelInList, namespacedToolName } from "../../types";
21
22
  */
22
23
  const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has no video mapping]";
23
24
 
24
- export function developerSystemText(message: OcxMessage): string | undefined {
25
- if (message.role !== "developer") return undefined;
26
- if (typeof message.content === "string") return message.content;
27
- if (message.content.some(part => part.type === "image")) return undefined;
28
- return message.content.map(part => (part as OcxTextContent).text).join("");
29
- }
30
-
31
25
  /**
32
26
  * Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
33
27
  * content is text-only on every chat provider, so these ride in a follow-up user message instead of
@@ -121,21 +115,26 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
121
115
  };
122
116
 
123
117
  const nativeOpenAI = isNativeOpenAIChatTarget(provider);
124
- // Hoisting a newly appended reminder rewrites the reusable prompt prefix.
125
- // Keep this compatibility exception on the destination/model tested with OCG.
126
- const chronologicalSystem = parsed.modelId === "deepseek-v4.1-flash"
127
- && registryEntryForProviderDestination(provider)?.id === "opencode-go";
118
+ // Which role a developer message carries, and why the unrecorded state folds, is stated once
119
+ // in ./developer-role.ts and read from there by the native passthrough as well. Either way the
120
+ // message keeps the slot it arrived in — only the role changes, never the position.
121
+ const developerWireRole = translatedChatDeveloperWireRole(provider);
122
+ // A developer message keeps the slot it arrived in. Hoisting its text into the leading
123
+ // system block moved a mid-conversation instruction ahead of every turn it was written to
124
+ // follow, and the caller saw an ordinary answer either way (#5213). The Claude inbound mints
125
+ // chronological developer items for exactly this reason (#4161), so the two halves of the
126
+ // route were working against each other on every host but api.openai.com. Placement is now
127
+ // uniform; which ROLE that slot carries is decided separately below.
128
+ //
129
+ // One destination already had the chronological behaviour, keyed to a model and a registry
130
+ // id, because hoisting a newly appended reminder rewrites the reusable prompt prefix. That is
131
+ // a property of prompt-prefix caching rather than of that destination, and it is now what
132
+ // every destination gets.
128
133
  const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
129
134
  ? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
130
135
  : undefined;
131
- const developerSystemParts = nativeOpenAI || chronologicalSystem
132
- ? []
133
- : context.messages
134
- .map(developerSystemText)
135
- .filter((part): part is string => part !== undefined && part.length > 0);
136
136
  const systemParts = [
137
137
  ...(context.systemPrompt ?? []),
138
- ...developerSystemParts,
139
138
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
140
139
  ];
141
140
  if (systemParts.length > 0) {
@@ -152,20 +151,23 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
152
151
  case "developer": {
153
152
  const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
154
153
  const hasImages = parts?.some(p => p.type === "image") ?? false;
154
+ // A document has a structured counterpart on this wire, so it needs the parts array for
155
+ // the same reason an image does: flattening it to a string would drop the bytes.
156
+ const hasStructured = hasImages || (parts?.some(p => p.type === "document") ?? false);
155
157
  let chatMsg: Record<string, unknown>;
156
- if (msg.role === "developer" && !hasImages) {
157
- if (!nativeOpenAI && !chronologicalSystem) break;
158
+ if (msg.role === "developer" && !hasStructured) {
158
159
  const text = typeof msg.content === "string"
159
160
  ? msg.content
160
161
  : parts!.map(p => (p as OcxTextContent).text).join("");
161
- // A non-text timeline part (video, for example) serializes to nothing here.
162
- // The generic path drops such a message; the chronological exception must not
163
- // turn it into an empty system message that some upstreams reject.
162
+ // A non-text timeline part (video, for example) serializes to nothing here. The
163
+ // generic user path drops such a message, and emitting a content-free system
164
+ // message instead is rejected by some upstreams. Native OpenAI keeps its existing
165
+ // empty-developer wire, which is a separate question from placement.
164
166
  if (!nativeOpenAI && text.length === 0) break;
165
- chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text };
167
+ chatMsg = { role: developerWireRole, content: text };
166
168
  } else if (typeof msg.content === "string") {
167
169
  chatMsg = { role: "user", content: msg.content };
168
- } else if (!hasImages) {
170
+ } else if (!hasStructured) {
169
171
  // A video part has no `text`, so joining it produced "" and the whole message
170
172
  // was dropped: a video-only or text-plus-video turn vanished silently. OpenAI's
171
173
  // Chat Completions wire has no video content part, so state the omission
@@ -183,13 +185,30 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
183
185
  if (p.type === "image") {
184
186
  return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
185
187
  }
188
+ // Chat Completions carries an attached document as a file part with inline bytes,
189
+ // the direct counterpart of the Anthropic document block the caller sent.
190
+ if (p.type === "document") {
191
+ return {
192
+ type: "file",
193
+ file: {
194
+ file_data: inlineDocumentDataUrl(p),
195
+ ...(p.filename !== undefined ? { filename: p.filename } : {}),
196
+ },
197
+ };
198
+ }
186
199
  // Previously this produced { type: "text", text: undefined } for a video
187
200
  // part — a malformed part, worse than a drop because it can fail upstream
188
201
  // schema validation.
189
202
  if (p.type === "video") return { type: "text", text: VIDEO_UNSUPPORTED_MARKER };
190
203
  return { type: "text", text: (p as OcxTextContent).text };
191
204
  });
192
- chatMsg = { role: "user", content: chatParts };
205
+ // A developer message with images keeps the user-compatible shape it has always had on
206
+ // this wire. One carrying only a document has no such precedent, and demoting it would
207
+ // undo the role this adapter just finished preserving.
208
+ chatMsg = {
209
+ role: msg.role === "developer" && !hasImages ? developerWireRole : "user",
210
+ content: chatParts,
211
+ };
193
212
  }
194
213
  if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
195
214
  else out.push(chatMsg);
@@ -0,0 +1,32 @@
1
+ import type { OcxProviderConfig } from "../../types";
2
+
3
+ /**
4
+ * The `parallel_tool_calls` value for a translated Chat request, or `undefined` to omit the key.
5
+ *
6
+ * A provider has three states here, not two, and the third is the default for every provider that
7
+ * never configured the knob. While the call site only branched on the two configured states, a
8
+ * caller's own explicit `parallel_tool_calls: false` reached the parser, was carried on
9
+ * `options.parallelToolCalls`, and was then dropped on the way to the wire — the upstream stayed
10
+ * free to emit concurrent calls and the response looked entirely normal (#5211).
11
+ *
12
+ * Only an explicit request-side `false` is forwarded in the unset state. `true` is already the
13
+ * upstream default, so emitting it would introduce the knob to strict OpenAI-compatible hosts
14
+ * that have never had to accept it, which is the reason the configured opt-out below omits the
15
+ * key rather than sending `false`.
16
+ */
17
+ export function chatParallelToolCallsWireValue(
18
+ provider: OcxProviderConfig,
19
+ requested: boolean | undefined,
20
+ ): boolean | undefined {
21
+ if (provider.parallelToolCalls === false) {
22
+ // NIM documents the Boolean defaulting to false and kimi rejects true; pin the wire bit so
23
+ // Codex cannot opt in via request.options. Other opted-out providers omit the field, but a
24
+ // self-hosted gateway that DOES honor it and keeps emitting parallel calls without it can opt
25
+ // in via pinParallelToolCallsFalse.
26
+ const pinned = provider.baseUrl === "https://integrate.api.nvidia.com/v1"
27
+ || provider.pinParallelToolCallsFalse === true;
28
+ return pinned ? false : undefined;
29
+ }
30
+ if (provider.parallelToolCalls === true) return requested !== false;
31
+ return requested === false ? false : undefined;
32
+ }
@@ -1,6 +1,7 @@
1
1
  import { openAIChatTransport, stripBracketedModelSuffix } from "./wire";
2
2
  import type { AdapterRequest } from "../base";
3
3
  import { frameAgentRouterMessages } from "../agentrouter";
4
+ import { applyExplicitChatDeveloperRole } from "./developer-role";
4
5
  import { openRouterProviderPayload, resolveOpenRouterRouting } from "../../providers/openrouter-routing";
5
6
  import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../../providers/vercel-gateway-routing";
6
7
  import { fastPolicyForModel } from "../../providers/service-tier";
@@ -9,6 +10,8 @@ import { debugProviderDiagnostic } from "../../lib/debug";
9
10
  import { isDebugEnabled } from "../../lib/debug-settings";
10
11
  import { modelRecordValue } from "../../reasoning-effort";
11
12
  import { modelInList, type OcxProviderConfig } from "../../types";
13
+ import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
14
+ import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
12
15
 
13
16
  const CHAT_PASSTHROUGH_FIELDS = [
14
17
  "audio",
@@ -54,16 +57,37 @@ export function buildOpenAIChatPassthroughRequest(
54
57
 
55
58
  const body: Record<string, unknown> = {
56
59
  model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
57
- messages: frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
60
+ // The caller's messages are forwarded as they arrived, with one exception: an operator who
61
+ // recorded that this destination rejects the `developer` role gets that role converted in
62
+ // place. Verbatim was not neutral there — it sent the role anyway and the turn failed
63
+ // upstream with a 400 before the model saw it. An unrecorded destination is still verbatim,
64
+ // and the conversion changes the role of those messages and nothing else, so the position
65
+ // of every message and every other field survive unchanged.
66
+ messages: applyExplicitChatDeveloperRole(
67
+ frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
68
+ provider,
69
+ ),
58
70
  stream,
59
71
  };
60
72
  for (const field of CHAT_PASSTHROUGH_FIELDS) {
61
73
  if (rawBody[field] !== undefined) body[field] = rawBody[field];
62
74
  }
63
75
  const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
64
- if (modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0) {
76
+ const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
77
+ if (reasoningDisabled) {
65
78
  delete body.reasoning_effort;
66
79
  }
80
+ const hasTools = Array.isArray(rawBody.tools) && rawBody.tools.length > 0;
81
+ const requestedEffort = typeof body.reasoning_effort === "string" ? body.reasoning_effort : undefined;
82
+ applyExplicitChatReasoningWirePolicy({
83
+ provider,
84
+ modelId,
85
+ hasTools,
86
+ requestedEffort,
87
+ wireEffort: requestedEffort,
88
+ reasoningDisabled,
89
+ body,
90
+ });
67
91
 
68
92
  const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
69
93
  if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
@@ -107,13 +131,13 @@ export function buildOpenAIChatPassthroughRequest(
107
131
  if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
108
132
  body.prompt_cache_key = rawBody.prompt_cache_key;
109
133
  }
110
- if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
111
- if (provider.parallelToolCalls === true) {
112
- body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
113
- } else if (provider.parallelToolCalls === false
114
- && (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
115
- body.parallel_tool_calls = false;
116
- }
134
+ if (hasTools) {
135
+ // Same three provider states as the translated path, and the same defect in the unset one:
136
+ // a caller's explicit false was dropped here too (#5211). The native route reads the bit off
137
+ // the raw request rather than the parsed options, since nothing projects this body.
138
+ const requested = typeof rawBody.parallel_tool_calls === "boolean" ? rawBody.parallel_tool_calls : undefined;
139
+ const parallelToolCalls = chatParallelToolCallsWireValue(provider, requested);
140
+ if (parallelToolCalls !== undefined) body.parallel_tool_calls = parallelToolCalls;
117
141
  }
118
142
  if (stream) {
119
143
  const callerOptions = rawBody.stream_options !== null