@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1265 @@
1
+ import type { AdapterRequest, ProviderAdapter } from "./base";
2
+ import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
3
+ import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
4
+ import { isReasoningEffortRequired, mapReasoningEffort, modelRecordValue, reasoningControlKindFor } from "../reasoning-effort";
5
+ import { debugProviderDiagnostic } from "../lib/debug";
6
+ import { sseFieldValue } from "../lib/sse-decoder";
7
+ import { isDebugEnabled } from "../lib/debug-settings";
8
+ import { isCyberPolicyCode } from "../lib/errors";
9
+ import { redactSecretString } from "../lib/redact";
10
+ import { contentPartsToText } from "./image";
11
+ import { identifyRoutedModel } from "./identity";
12
+ import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
13
+ import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
14
+ import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
15
+ import {
16
+ isTranslatorBudgetExceededError,
17
+ retainTranslatedEventBatch,
18
+ TRANSLATOR_MAX_SSE_EVENT_BYTES,
19
+ type TranslatorBudget,
20
+ } from "../lib/translator-budget";
21
+
22
+ // Providers may opt into stripping one trailing "[...]" group from the wire model id.
23
+ // Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
24
+ // unflagged OpenAI-compatible providers and the Anthropic adapter keep ids verbatim.
25
+ export function stripBracketedModelSuffix(modelId: string): string {
26
+ return modelId.replace(/\[[^\]]*\]\s*$/, "");
27
+ }
28
+
29
+ // 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
30
+ // loop.ts only appends a suffix to "Provider error N" when the adapter exposes
31
+ // formatErrorBody; without it, strict OpenAI-compatible backends (NVIDIA NIM pydantic
32
+ // validation, "This model only supports single tool-calls at once!", etc.) were reduced
33
+ // to a bare status code. JSON-only extraction: recognized string fields are returned,
34
+ // HTML/non-JSON bodies yield "" so raw markup is never echoed to the client.
35
+ export function formatOpenAIChatErrorBody(status: number, _headers: Headers, payloadText: string): string {
36
+ let parsed: unknown;
37
+ try {
38
+ parsed = JSON.parse(payloadText);
39
+ } catch {
40
+ return "";
41
+ }
42
+ const detail = extractErrorDetail(parsed);
43
+ if (!detail) return "";
44
+ return redactSecretString(detail).slice(0, 400);
45
+ }
46
+
47
+ function extractErrorDetail(parsed: unknown): string | undefined {
48
+ if (typeof parsed === "string") return parsed.trim() || undefined;
49
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
50
+ const obj = parsed as Record<string, unknown>;
51
+ // OpenAI shape: { error: { message } } or { error: "..." }
52
+ const err = obj.error;
53
+ if (typeof err === "string" && err.trim()) return err.trim();
54
+ if (err !== null && typeof err === "object" && !Array.isArray(err)) {
55
+ const msg = (err as Record<string, unknown>).message;
56
+ if (typeof msg === "string" && msg.trim()) return msg.trim();
57
+ }
58
+ // FastAPI/pydantic shape (NVIDIA NIM): { detail: "..." } or { detail: [{ msg, loc }, ...] }
59
+ const det = obj.detail;
60
+ if (typeof det === "string" && det.trim()) return det.trim();
61
+ if (Array.isArray(det)) {
62
+ const msgs = det
63
+ .map(item => (item !== null && typeof item === "object" && typeof (item as Record<string, unknown>).msg === "string"
64
+ ? ((item as Record<string, unknown>).msg as string).trim()
65
+ : ""))
66
+ .filter(m => m.length > 0);
67
+ if (msgs.length > 0) return msgs.join("; ");
68
+ }
69
+ // Generic fallbacks: { message } / RFC7807 { title }
70
+ if (typeof obj.message === "string" && obj.message.trim()) return obj.message.trim();
71
+ if (typeof obj.title === "string" && obj.title.trim()) return obj.title.trim();
72
+ return undefined;
73
+ }
74
+
75
+ // ClinePass live responses observed 2026-08-02 wrap non-stream Chat Completions in
76
+ // `{ success, error, data }`; its public Chat Completions docs do not currently describe that
77
+ // envelope. Keep ordinary OpenAI-shaped responses on the direct path.
78
+ function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
79
+ if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
80
+ const data = json.data;
81
+ return data !== null && typeof data === "object" && !Array.isArray(data)
82
+ ? data as Record<string, unknown>
83
+ : json;
84
+ }
85
+
86
+ interface OpenAIChatError {
87
+ message?: unknown;
88
+ code?: unknown;
89
+ type?: unknown;
90
+ status?: unknown;
91
+ metadata?: unknown;
92
+ }
93
+
94
+ function safeUpstreamRequestId(metadata: unknown): string | undefined {
95
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
96
+ const record = metadata as Record<string, unknown>;
97
+ const value = record.request_id ?? record.requestId;
98
+ if (typeof value !== "string") return undefined;
99
+ const requestId = value.trim();
100
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
101
+ && redactSecretString(requestId) === requestId
102
+ ? requestId
103
+ : undefined;
104
+ }
105
+
106
+ function upstreamErrorEvent(
107
+ error: unknown,
108
+ usage?: OcxUsage,
109
+ ): Extract<AdapterEvent, { type: "error" }> {
110
+ const details = error !== null && typeof error === "object" && !Array.isArray(error)
111
+ ? error as OpenAIChatError
112
+ : undefined;
113
+ const rawMessage = typeof error === "string"
114
+ ? error.trim() || "upstream error"
115
+ : typeof details?.message === "string" ? details.message : "upstream error";
116
+ const safeMessage = redactSecretString(rawMessage);
117
+ const requestId = safeUpstreamRequestId(details?.metadata);
118
+ const message = requestId !== undefined && !safeMessage.includes(requestId)
119
+ ? `${safeMessage} (request ID: ${requestId})`
120
+ : safeMessage;
121
+ const code = typeof details?.code === "string"
122
+ ? details.code
123
+ : typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
124
+ ? String(details.code)
125
+ : undefined;
126
+ const errorType = typeof details?.type === "string" ? details.type : undefined;
127
+ const codeStatus = typeof details?.code === "number"
128
+ && Number.isInteger(details.code)
129
+ && details.code >= 100
130
+ && details.code <= 599
131
+ ? details.code
132
+ : undefined;
133
+ const status = isCyberPolicyCode(code)
134
+ ? 400
135
+ : typeof details?.status === "number" && Number.isInteger(details.status)
136
+ ? details.status
137
+ : codeStatus;
138
+ return {
139
+ type: "error",
140
+ message,
141
+ ...(usage !== undefined ? { usage } : {}),
142
+ ...(code !== undefined ? { code } : {}),
143
+ ...(errorType !== undefined ? { errorType } : {}),
144
+ ...(status !== undefined ? { status } : {}),
145
+ };
146
+ }
147
+
148
+ function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
149
+ return finishReason === "length"
150
+ ? "max_tokens"
151
+ : finishReason === "content_filter"
152
+ ? "content_filter"
153
+ : undefined;
154
+ }
155
+
156
+ function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
157
+ return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
158
+ ? record.reasoning_content
159
+ : typeof record.reasoning === "string" && record.reasoning.length > 0
160
+ ? record.reasoning
161
+ : undefined;
162
+ }
163
+
164
+ function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
165
+ return {
166
+ type: "error",
167
+ message: "upstream response contained invalid choices",
168
+ ...(usage !== undefined ? { usage } : {}),
169
+ };
170
+ }
171
+
172
+ function developerSystemText(message: OcxMessage): string | undefined {
173
+ if (message.role !== "developer") return undefined;
174
+ if (typeof message.content === "string") return message.content;
175
+ if (message.content.some(part => part.type === "image")) return undefined;
176
+ return message.content.map(part => (part as OcxTextContent).text).join("");
177
+ }
178
+
179
+ /**
180
+ * Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
181
+ * content is text-only on every chat provider, so these ride in a follow-up user message instead of
182
+ * being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
183
+ * URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
184
+ */
185
+ function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
186
+ if (typeof content === "string") return [];
187
+ const parts: unknown[] = [];
188
+ for (const p of content) {
189
+ // Skip parts without a usable URL (the tool-output parser accepts the empty file_id shape):
190
+ // a {"url":""} part would fail the whole request where the "[image]" marker degrades safely.
191
+ if (p.type !== "image" || !p.imageUrl) continue;
192
+ parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
193
+ }
194
+ return parts;
195
+ }
196
+
197
+ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
198
+ const out: unknown[] = [];
199
+ const { context, options } = parsed;
200
+ // Mirror the bridge's replay-cache scope (issue #950): provider call ids are
201
+ // not globally unique, so reasoning must not cross conversation boundaries.
202
+ const replayCacheScope = parsed._clientThreadId ?? "global";
203
+
204
+ // 260718 dangling tool_calls hardening (devlog/_plan/260718_dangling_toolcall_hardening):
205
+ // strict chat providers (Kimi/Moonshot) 400 when an assistant tool_call is not answered
206
+ // immediately by role:"tool" messages. Repair order: (1) reattach a real result to its
207
+ // original call (barrier messages are DEFERRED until the open tool round closes),
208
+ // (2) synthesize an explicit unavailable-result only when no real result exists,
209
+ // (3) manufacture an orphan assistant call only when no call occurrence matches at all.
210
+ // Occurrences are kept as an ordered list (never a Map) so duplicated ids survive.
211
+ interface PendingToolCall { id: string; name: string }
212
+ let pendingToolCalls: PendingToolCall[] = [];
213
+ let deferredBarrierMessages: unknown[] = [];
214
+ let pendingToolResultImageParts: unknown[] = [];
215
+ let mintedIdSeq = 0;
216
+ const seenWireCallIds = new Set<string>();
217
+
218
+ const mintCallId = (): string => {
219
+ let id = "";
220
+ do {
221
+ id = `call_ocx_minted_${++mintedIdSeq}`;
222
+ } while (seenWireCallIds.has(id));
223
+ seenWireCallIds.add(id);
224
+ return id;
225
+ };
226
+
227
+ const releaseDeferredBarriers = (): void => {
228
+ if (deferredBarrierMessages.length === 0) return;
229
+ out.push(...deferredBarrierMessages);
230
+ deferredBarrierMessages = [];
231
+ };
232
+
233
+ // Tool-result images collected during the open round land in ONE user vision message once the
234
+ // round closes — never inside it, where strict providers (Kimi/Moonshot) 400 on interleaved
235
+ // user messages. Released before deferred barriers so the images stay adjacent to the results
236
+ // they came from (mirrors google.ts sibling inline_data parts and the Kiro carrier images).
237
+ const flushToolResultImages = (): void => {
238
+ if (pendingToolResultImageParts.length === 0) return;
239
+ out.push({
240
+ role: "user",
241
+ content: [
242
+ { type: "text", text: "[ocx] image output from the preceding tool result(s):" },
243
+ ...pendingToolResultImageParts,
244
+ ],
245
+ });
246
+ pendingToolResultImageParts = [];
247
+ };
248
+
249
+ // Close an unresolved tool round with explicit unavailable-result messages. The wording
250
+ // must not claim interruption, success, failure, or user intent: execution status is
251
+ // UNKNOWN, and for user-input tools this must not read as an answer.
252
+ const flushPendingToolCalls = (): void => {
253
+ if (pendingToolCalls.length === 0) return;
254
+ for (const call of pendingToolCalls) {
255
+ out.push({
256
+ role: "tool",
257
+ tool_call_id: call.id,
258
+ content: `[ocx] no tool result was recorded for "${call.name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
259
+ });
260
+ }
261
+ pendingToolCalls = [];
262
+ flushToolResultImages();
263
+ releaseDeferredBarriers();
264
+ };
265
+
266
+ const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
267
+ ? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
268
+ : undefined;
269
+ // Chat templates used by LM Studio, llama.cpp, and other strict OpenAI-compatible
270
+ // backends require every system instruction to precede conversation history. Codex can
271
+ // append developer reminders after user turns, so fold text-only developer messages into
272
+ // the single leading system message instead of emitting role:"system" in place. Developer
273
+ // messages with images cannot be represented as system content and remain user-compatible
274
+ // vision messages at their original position below.
275
+ const developerSystemParts = context.messages
276
+ .map(developerSystemText)
277
+ .filter((part): part is string => part !== undefined && part.length > 0);
278
+ const systemParts = [
279
+ ...(context.systemPrompt ?? []),
280
+ ...developerSystemParts,
281
+ ...(toolCatalogNudge ? [toolCatalogNudge] : []),
282
+ ];
283
+ if (systemParts.length > 0) {
284
+ // Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
285
+ // base_instructions is ignored at request time). Neutralize that one identity line
286
+ // so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI — without
287
+ // leaking the proxy identity into the payload.
288
+ const wireModelId = provider.modelSuffixBracketStrip
289
+ ? stripBracketedModelSuffix(parsed.modelId)
290
+ : parsed.modelId;
291
+ const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
292
+ out.push({ role: "system", content: sys });
293
+ }
294
+
295
+ for (const msg of context.messages) {
296
+ switch (msg.role) {
297
+ case "user":
298
+ case "developer": {
299
+ const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
300
+ const hasImages = parts?.some(p => p.type === "image") ?? false;
301
+ if (msg.role === "developer" && !hasImages) break;
302
+ let chatMsg: Record<string, unknown>;
303
+ if (typeof msg.content === "string") {
304
+ chatMsg = { role: "user", content: msg.content };
305
+ } else {
306
+ if (!hasImages) {
307
+ chatMsg = { role: "user", content: parts!.map(p => (p as OcxTextContent).text).join("") };
308
+ } else {
309
+ // Vision: chat-completions content-parts array. Images are only valid on the user role,
310
+ // and the data URL goes straight into image_url.url (never the token-exploding text path).
311
+ const chatParts = parts!.map(p => p.type === "image"
312
+ ? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
313
+ : { type: "text", text: (p as OcxTextContent).text });
314
+ chatMsg = { role: "user", content: chatParts };
315
+ }
316
+ }
317
+ // A barrier must not split an open tool round: defer it until the round closes
318
+ // (real result arrives) or the round is synthesized shut.
319
+ if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
320
+ else out.push(chatMsg);
321
+ break;
322
+ }
323
+ case "assistant": {
324
+ const aMsg = msg as OcxAssistantMessage;
325
+ const textParts = aMsg.content.filter(p => p.type === "text") as OcxTextContent[];
326
+ const thinkingParts = aMsg.content.filter(p => p.type === "thinking") as OcxThinkingContent[];
327
+ const toolCalls = aMsg.content.filter(p => p.type === "toolCall") as OcxToolCall[];
328
+ const chatMsg: Record<string, unknown> = { role: "assistant" };
329
+ if (textParts.length > 0) {
330
+ chatMsg.content = textParts.map(p => p.text).join("");
331
+ }
332
+ let reasoningContent = thinkingParts.map(p => p.thinking).join("");
333
+ // History transformations (compaction, lost assistant turn, resumed
334
+ // threads) can strip the reasoning item while the tool round survives.
335
+ // Re-attach the reasoning the bridge recorded for these call ids so
336
+ // preserveReasoningContentModels providers (DeepSeek thinking mode)
337
+ // never receive a bare tool-call continuation (issue #950).
338
+ if (
339
+ reasoningContent.length === 0
340
+ && toolCalls.length > 0
341
+ && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
342
+ ) {
343
+ const cached = toolCalls
344
+ .map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
345
+ .filter((text): text is string => typeof text === "string" && text.length > 0);
346
+ // Parallel calls share one preceding reasoning block, which is
347
+ // recorded under every call id — join unique texts only.
348
+ if (cached.length > 0) {
349
+ reasoningContent = [...new Set(cached)].join("\n");
350
+ }
351
+ }
352
+ if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
353
+ chatMsg.reasoning_content = reasoningContent;
354
+ }
355
+ // Skip empty assistant messages: chat APIs like DeepSeek reject an assistant message
356
+ // with neither content, tool calls, nor a provider-supported reasoning_content field.
357
+ if (chatMsg.content === undefined && toolCalls.length === 0 && chatMsg.reasoning_content === undefined) break;
358
+ // A new assistant starts while a previous round is still open: close the previous
359
+ // round synthetically first so its tool_calls are never left dangling.
360
+ flushPendingToolCalls();
361
+ const wireToolCalls = toolCalls.map(tc => {
362
+ let id = tc.id;
363
+ if (!id) id = mintCallId();
364
+ else seenWireCallIds.add(id);
365
+ return { tc, id };
366
+ });
367
+ if (wireToolCalls.length > 0) {
368
+ chatMsg.tool_calls = wireToolCalls.map(({ tc, id }) => ({
369
+ id,
370
+ type: "function",
371
+ function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
372
+ }));
373
+ // "" instead of null: strict validators (xAI: "Each message must have at least one
374
+ // content element", langchain#34140) reject content-less assistant history entries.
375
+ if (!chatMsg.content) chatMsg.content = emptyAssistantContent(provider);
376
+ }
377
+ if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
378
+ chatMsg.content = emptyAssistantContent(provider);
379
+ }
380
+ out.push(chatMsg);
381
+ pendingToolCalls = wireToolCalls.map(({ tc, id }) => ({ id, name: namespacedToolName(tc.namespace, tc.name) }));
382
+ break;
383
+ }
384
+ case "toolResult": {
385
+ let toolCallId = msg.toolCallId;
386
+ const matchIdx = toolCallId ? pendingToolCalls.findIndex(c => c.id === toolCallId) : -1;
387
+ if (matchIdx >= 0 && toolCallId) {
388
+ // Real result reattached to its original call. Barriers were deferred, so the
389
+ // tool message lands immediately inside the open round.
390
+ out.push({
391
+ role: "tool",
392
+ tool_call_id: toolCallId,
393
+ content: contentPartsToText(msg.content),
394
+ });
395
+ pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
396
+ pendingToolCalls.splice(matchIdx, 1);
397
+ if (pendingToolCalls.length === 0) {
398
+ flushToolResultImages();
399
+ releaseDeferredBarriers();
400
+ }
401
+ } else {
402
+ if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
403
+ // No matching call in the open round. Close any unresolved round first so the
404
+ // synthesized orphan pair never splits it, then keep the historical repair:
405
+ // WS turns can arrive with only tool outputs; chat-completions providers reject a bare
406
+ // role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
407
+ flushPendingToolCalls();
408
+ const name = safeToolName(msg.toolName);
409
+ // The orphan repair synthesizes an assistant tool call for a result
410
+ // whose assistant turn was lost; carry the recorded reasoning so the
411
+ // replayed round stays valid for thinking-mode providers (#950).
412
+ const cachedReasoning =
413
+ toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
414
+ ? peekReasoningForCall(toolCallId, replayCacheScope)
415
+ : undefined;
416
+ out.push({
417
+ role: "assistant",
418
+ content: emptyAssistantContent(provider),
419
+ ...(cachedReasoning ? { reasoning_content: cachedReasoning } : {}),
420
+ tool_calls: [{
421
+ id: toolCallId,
422
+ type: "function",
423
+ function: { name, arguments: "{}" },
424
+ }],
425
+ });
426
+ seenWireCallIds.add(toolCallId);
427
+ out.push({
428
+ role: "tool",
429
+ tool_call_id: toolCallId,
430
+ content: contentPartsToText(msg.content),
431
+ });
432
+ pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
433
+ flushToolResultImages();
434
+ }
435
+ break;
436
+ }
437
+ }
438
+ }
439
+
440
+ // Trailing dangle: a turn interrupted after the assistant requested tools leaves the
441
+ // round open; close it synthetically (then release any deferred barriers in order).
442
+ flushPendingToolCalls();
443
+ releaseDeferredBarriers();
444
+ return out;
445
+ }
446
+
447
+ function safeToolName(name: string | undefined): string {
448
+ const raw = name && name.trim().length > 0 ? name : "tool_result";
449
+ const sanitized = raw.replace(/[^A-Za-z0-9_-]/g, "_");
450
+ return sanitized;
451
+ }
452
+
453
+ const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
454
+ const ZEN_DROPPED_SCHEMA_KEYS = new Set(["encrypted"]);
455
+
456
+ function sanitizeZenSchemaMap(value: unknown): unknown {
457
+ if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeZenToolParameters(value);
458
+ const out: Record<string, unknown> = {};
459
+ for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
460
+ out[name] = sanitizeZenToolParameters(child);
461
+ }
462
+ return out;
463
+ }
464
+
465
+ function sanitizeZenToolParameters(value: unknown): unknown {
466
+ if (Array.isArray(value)) return value.map(sanitizeZenToolParameters);
467
+ if (!value || typeof value !== "object") return value;
468
+ const input = value as Record<string, unknown>;
469
+ const out: Record<string, unknown> = {};
470
+ for (const [key, child] of Object.entries(input)) {
471
+ if (ZEN_DROPPED_SCHEMA_KEYS.has(key)) continue;
472
+ if (key === "required" && Array.isArray(child) && child.length === 0) continue;
473
+ if (key === "type" && Array.isArray(child)) {
474
+ const nonNull = child.filter(entry => entry !== "null");
475
+ if (child.includes("null")) out.nullable = true;
476
+ if (nonNull.length > 0) out.type = nonNull[0];
477
+ continue;
478
+ }
479
+ out[key] = ZEN_SCHEMA_MAP_KEYS.has(key) ? sanitizeZenSchemaMap(child) : sanitizeZenToolParameters(child);
480
+ }
481
+ return out;
482
+ }
483
+
484
+ function ensureZenRootObjectSchema(schema: unknown): Record<string, unknown> {
485
+ const obj = schema && typeof schema === "object" && !Array.isArray(schema)
486
+ ? schema as Record<string, unknown>
487
+ : {};
488
+ const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
489
+ const hasComposition = compositionKeys.some(key => Array.isArray(obj[key]));
490
+ const rootType = obj.type;
491
+ const rootObjectType = rootType === "object" || (Array.isArray(rootType) && rootType.includes("object"));
492
+ if (!hasComposition) {
493
+ const base = sanitizeZenToolParameters(obj) as Record<string, unknown>;
494
+ return rootObjectType && base.type === "object" ? base : { ...base, type: "object" };
495
+ }
496
+
497
+ const props: Record<string, unknown> = {};
498
+ const required = new Set<string>();
499
+ if (obj.properties && typeof obj.properties === "object") {
500
+ Object.assign(props, sanitizeZenSchemaMap(obj.properties) as Record<string, unknown>);
501
+ }
502
+ if (Array.isArray(obj.required)) {
503
+ for (const entry of obj.required) if (typeof entry === "string") required.add(entry);
504
+ }
505
+ for (const key of compositionKeys) {
506
+ const variants = obj[key];
507
+ if (!Array.isArray(variants)) continue;
508
+ const mergeRequired = key === "allOf";
509
+ for (const variant of variants) {
510
+ if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
511
+ const rec = variant as Record<string, unknown>;
512
+ if (rec.properties && typeof rec.properties === "object") {
513
+ Object.assign(props, sanitizeZenSchemaMap(rec.properties) as Record<string, unknown>);
514
+ }
515
+ if (mergeRequired && Array.isArray(rec.required)) {
516
+ for (const entry of rec.required) if (typeof entry === "string") required.add(entry);
517
+ }
518
+ }
519
+ }
520
+
521
+ const merged = sanitizeZenToolParameters(obj) as Record<string, unknown>;
522
+ delete merged.oneOf;
523
+ delete merged.anyOf;
524
+ delete merged.allOf;
525
+ merged.type = "object";
526
+ if (Object.keys(props).length > 0) merged.properties = props;
527
+ if (required.size > 0) merged.required = [...required];
528
+ return merged;
529
+ }
530
+
531
+ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
532
+ const baseUrl = provider.baseUrl.replace(/\/+$/, "");
533
+ return baseUrl === "https://opencode.ai/zen/v1"
534
+ || baseUrl === "https://opencode.ai/zen/go/v1";
535
+ }
536
+
537
+ const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
538
+
539
+ function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
540
+ try {
541
+ return XAI_SCHEMA_BASE_URLS.has(new URL(provider.baseUrl).hostname);
542
+ } catch {
543
+ return false;
544
+ }
545
+ }
546
+
547
+ // Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
548
+ // REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
549
+ // `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
550
+ // "", and xAI actively requires it ("Each message must have at least one content element"), so
551
+ // the two contracts are in direct conflict and this cannot be a global change.
552
+ const VOLCENGINE_ARK_HOSTNAMES = new Set([
553
+ "ark.cn-beijing.volces.com",
554
+ "ark.ap-southeast.volces.com",
555
+ ]);
556
+
557
+ function isVolcengineArkTarget(provider: OcxProviderConfig): boolean {
558
+ try {
559
+ return VOLCENGINE_ARK_HOSTNAMES.has(new URL(provider.baseUrl).hostname);
560
+ } catch {
561
+ return false;
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Placeholder content for an assistant history entry carrying only tool calls or reasoning.
567
+ *
568
+ * UNVERIFIED HYPOTHESIS for Ark. The reported error names `input.content.text`, a nested path,
569
+ * which suggests Ark wants the structured content form `[{type:"text",text:""}]` rather than a
570
+ * bare string — no string value, `""` or `" "`, exposes a `content.text` path at all. But Ark's
571
+ * published examples only show array content for MULTIMODAL USER input, never for an assistant
572
+ * history entry, so this shape is inferred from the error message and not confirmed by the docs
573
+ * or by a live request. The empty inner text at least adds no tokens either way.
574
+ *
575
+ * Confirm against a real Ark endpoint before relying on this; #796 records what is still missing.
576
+ *
577
+ * Every other provider keeps the bare `""`, which xAI's validator specifically requires ("Each
578
+ * message must have at least one content element"), so this cannot be applied globally.
579
+ */
580
+ function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "text"; text: string }[] {
581
+ return isVolcengineArkTarget(provider) ? [{ type: "text", text: "" }] : "";
582
+ }
583
+
584
+ /**
585
+ * Providers like Kimi and DeepSeek reject function parameter schemas whose root
586
+ * `type` is missing or `null` — JSON Schema requires `"object"` at the root of
587
+ * function parameters. Add `type: "object"` at the root while preserving
588
+ * `oneOf`, `$defs`, and every other schema key.
589
+ *
590
+ * This mirrors `normalizeFunctionToolSchema` in openai-responses.ts, which
591
+ * applies the same root-only normalization unconditionally on the responses
592
+ * path. Nested schema content is intentionally left untouched.
593
+ */
594
+ function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
595
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
596
+ return { type: "object", properties: {} };
597
+ }
598
+ const obj = parameters as Record<string, unknown>;
599
+ if (obj.type === "object") return obj;
600
+ return { ...obj, type: "object" };
601
+ }
602
+
603
+ function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
604
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
605
+ const obj = schema as Record<string, unknown>;
606
+ const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(obj[key]));
607
+ if (!compositionKey) {
608
+ if (obj.type !== undefined && obj.type !== "object") return undefined;
609
+ return [{ ...obj, type: "object" }];
610
+ }
611
+
612
+ const siblings = Object.fromEntries(Object.entries(obj).filter(([key]) => key !== compositionKey));
613
+ const branches = obj[compositionKey];
614
+ if (!Array.isArray(branches)) return undefined;
615
+ const expanded: Record<string, unknown>[] = [];
616
+ for (const branch of branches) {
617
+ const variants = expandXaiRootObjectSchemas(branch);
618
+ if (!variants) return undefined;
619
+ for (const variant of variants) expanded.push({ ...siblings, ...variant });
620
+ }
621
+ return expanded.length > 0 ? expanded : undefined;
622
+ }
623
+
624
+ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
625
+ const variants = expandXaiRootObjectSchemas(parameters);
626
+ if (!variants) return undefined;
627
+ if (variants.length === 1) return variants[0];
628
+ const root = parameters && typeof parameters === "object" && !Array.isArray(parameters)
629
+ ? parameters as Record<string, unknown>
630
+ : {};
631
+ const metadata = Object.fromEntries(Object.entries(root).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
632
+ return { ...metadata, oneOf: variants };
633
+ }
634
+
635
+ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
636
+ if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
637
+ const allowed = isAllowedToolChoice(parsed.options.toolChoice)
638
+ ? new Set(parsed.options.toolChoice.allowedTools)
639
+ : undefined;
640
+ const tools = allowed
641
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
642
+ : parsed.context.tools;
643
+ if (tools.length === 0) return undefined;
644
+ const xaiTarget = isXaiSchemaTarget(provider);
645
+ const formatted = tools.flatMap(t => {
646
+ const parameters = xaiTarget
647
+ ? normalizeXaiToolParameters(t.parameters)
648
+ : ensureRootObjectType(t.parameters);
649
+
650
+ if (parameters === undefined) return [];
651
+ return [{
652
+ type: "function",
653
+ function: {
654
+ name: namespacedToolName(t.namespace, t.name),
655
+ description: t.description,
656
+ parameters,
657
+ ...(t.strict !== undefined ? { strict: t.strict } : {}),
658
+ },
659
+ }];
660
+ });
661
+ return formatted.length > 0 ? formatted : undefined;
662
+ }
663
+
664
+ function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
665
+ const base = toolsToChatFormat(parsed, provider);
666
+ if (!base || !shouldSanitizeZenToolParameters(provider)) return base;
667
+ return base.map(tool => {
668
+ if (!tool || typeof tool !== "object") return tool;
669
+ const functionDef = (tool as { function?: Record<string, unknown> }).function;
670
+ if (!functionDef || typeof functionDef !== "object") return tool;
671
+ return {
672
+ ...tool,
673
+ function: {
674
+ ...functionDef,
675
+ parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}),
676
+ },
677
+ };
678
+ });
679
+ }
680
+
681
+ function toolChoiceToChatFormat(tc: OcxParsedRequest["options"]["toolChoice"], tools: OcxParsedRequest["context"]["tools"]): unknown {
682
+ if (!tc) return undefined;
683
+ if (isAllowedToolChoice(tc)) return tc.mode === "required" ? "required" : "auto";
684
+ if (tc === "auto" || tc === "none" || tc === "required") return tc;
685
+ if ("name" in tc) return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.name) } };
686
+ return undefined;
687
+ }
688
+
689
+ function usageFromOpenAIChat(usage: Record<string, unknown> | undefined): OcxUsage | undefined {
690
+ if (!usage) return undefined;
691
+ const promptDetails = usage.prompt_tokens_details as Record<string, number> | undefined;
692
+ const completionDetails = usage.completion_tokens_details as Record<string, number> | undefined;
693
+ return {
694
+ inputTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
695
+ outputTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
696
+ ...(promptDetails?.cached_tokens !== undefined ? { cachedInputTokens: promptDetails.cached_tokens } : {}),
697
+ ...(completionDetails?.reasoning_tokens !== undefined ? { reasoningOutputTokens: completionDetails.reasoning_tokens } : {}),
698
+ };
699
+ }
700
+
701
+ function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
702
+ return parsed.options.maxOutputTokens
703
+ ?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
704
+ ?? provider.defaultMaxOutputTokens;
705
+ }
706
+
707
+ function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string, maxOutputTokens?: number): number | undefined {
708
+ if (parsed.options.reasoning === "minimal") return 0;
709
+ const maxBudget = maxOutputTokens ?? 32768;
710
+ const fractions: Record<string, number> = {
711
+ low: 0.20,
712
+ medium: 0.50,
713
+ high: 0.75,
714
+ xhigh: 0.90,
715
+ max: 1.0,
716
+ };
717
+ const fraction = fractions[reasoningEffort];
718
+ return fraction === undefined ? undefined : Math.max(1, Math.floor(maxBudget * fraction));
719
+ }
720
+
721
+ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
722
+ return {
723
+ name: "openai-chat",
724
+
725
+ formatErrorBody: formatOpenAIChatErrorBody,
726
+
727
+ buildRequest(parsed: OcxParsedRequest) {
728
+ const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
729
+ if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
730
+ throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
731
+ }
732
+
733
+ const messages = messagesToChatFormat(parsed, provider);
734
+ const tools = toolsToChatFormatForProvider(parsed, provider);
735
+ const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
736
+
737
+ const body: Record<string, unknown> = {
738
+ model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
739
+ messages,
740
+ stream: parsed.stream,
741
+ };
742
+ if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
743
+ const maxTokens = resolveMaxTokens(provider, parsed);
744
+ const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
745
+ if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
746
+ if (tools) body.tools = tools;
747
+ if (tools && toolChoice !== undefined) {
748
+ body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
749
+ ? (toolChoice === "none" ? "none" : "auto")
750
+ : toolChoice;
751
+ }
752
+ if (maxTokens !== undefined) body.max_tokens = maxTokens;
753
+ if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
754
+ body.temperature = parsed.options.temperature;
755
+ }
756
+ if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
757
+ body.top_p = parsed.options.topP;
758
+ }
759
+ if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
760
+ const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
761
+ const reasoningRequired = isReasoningEffortRequired(provider, parsed.modelId);
762
+ const reasoningControl = reasoningControlKindFor(provider, parsed.modelId);
763
+ const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
764
+ let reasoningLog: AdapterRequest["reasoningLog"];
765
+ // ClinePass live requests observed 2026-08-02 require this gateway-specific object; the
766
+ // public API docs do not currently specify its request shape.
767
+ if (
768
+ !reasoningDisabled
769
+ && provider.reasoningWireFormat === "gateway-object"
770
+ && reasoningControl === "toggle"
771
+ && typeof parsed.options.reasoning === "string"
772
+ ) {
773
+ const enabled = parsed.options.reasoning !== "none";
774
+ body.reasoning = { enabled };
775
+ reasoningLog = {
776
+ effectiveEffort: enabled ? "on" : "none",
777
+ wireField: "reasoning.enabled",
778
+ wireValue: enabled,
779
+ };
780
+ } else if (
781
+ !reasoningDisabled
782
+ && !reasoningRequired
783
+ && provider.reasoningWireFormat === "gateway-object"
784
+ && parsed.options.reasoning === "none"
785
+ ) {
786
+ body.reasoning = { enabled: false };
787
+ reasoningLog = {
788
+ effectiveEffort: "none",
789
+ wireField: "reasoning.enabled",
790
+ wireValue: false,
791
+ };
792
+ } else if (reasoningEffort !== undefined) {
793
+ if (provider.reasoningWireFormat === "gateway-object") {
794
+ body.reasoning = { enabled: true, effort: reasoningEffort };
795
+ reasoningLog = {
796
+ effectiveEffort: reasoningEffort,
797
+ wireField: "reasoning.effort",
798
+ wireValue: reasoningEffort,
799
+ };
800
+ } else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
801
+ const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
802
+ if (budget !== undefined) {
803
+ body.thinking_budget = budget;
804
+ reasoningLog = {
805
+ effectiveEffort: parsed.options.reasoning === "minimal" ? "minimal" : reasoningEffort,
806
+ wireField: "thinking_budget",
807
+ wireValue: budget,
808
+ };
809
+ }
810
+ } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
811
+ // Vendor thinking-toggle wire: the mapped value is sent as `thinking: {type}` because
812
+ // these models ignore/reject reasoning_effort. Most use enabled/disabled; MiniMax-M3
813
+ // uses adaptive/disabled.
814
+ if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
815
+ body.thinking = { type: reasoningEffort };
816
+ reasoningLog = {
817
+ effectiveEffort: reasoningEffort,
818
+ wireField: "thinking.type",
819
+ wireValue: reasoningEffort,
820
+ };
821
+ }
822
+ } else {
823
+ body.reasoning_effort = reasoningEffort;
824
+ reasoningLog = {
825
+ effectiveEffort: reasoningEffort,
826
+ wireField: "reasoning_effort",
827
+ wireValue: reasoningEffort,
828
+ };
829
+ }
830
+ }
831
+ if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
832
+ body.presence_penalty = parsed.options.presencePenalty;
833
+ }
834
+ if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
835
+ body.frequency_penalty = parsed.options.frequencyPenalty;
836
+ }
837
+ // prompt_cache_key is an OpenAI-specific chat extension; strict backends (Groq,
838
+ // Cerebras, etc.) reject unknown fields. Only forward when the provider opts in.
839
+ if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
840
+ body.prompt_cache_key = parsed.options.promptCacheKey;
841
+ }
842
+ // Responses `text.format` -> chat `response_format`. json_object maps 1:1; json_schema
843
+ // re-nests the flattened Responses fields under `json_schema` — the exact inverse of
844
+ // responseFormatToText in src/chat/inbound.ts. Forwarded unconditionally (like `stop`):
845
+ // response_format is a first-class Chat Completions field, it is only present when the
846
+ // caller explicitly asked for structured output, and a backend that rejects it should
847
+ // fail loud rather than silently return prose the caller will try to JSON.parse.
848
+ const textFormat = parsed.options.textFormat;
849
+ if (textFormat?.type === "json_object") {
850
+ body.response_format = { type: "json_object" };
851
+ } else if (textFormat?.type === "json_schema") {
852
+ body.response_format = {
853
+ type: "json_schema",
854
+ json_schema: {
855
+ name: textFormat.name ?? "response",
856
+ ...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
857
+ ...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
858
+ ...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
859
+ },
860
+ };
861
+ }
862
+
863
+ if (tools) {
864
+ // Default-ON for chat-completions providers (user decision 260709): the buffered
865
+ // parser assembles multi-call streams safely, so `parallelToolCalls: false` is the
866
+ // only per-provider opt-out; Codex's request bit can still force false per request.
867
+ // Rationale + provider evidence: devlog/_plan/260709_parallel_tool_calls.
868
+ body.parallel_tool_calls = provider.parallelToolCalls === false
869
+ ? false
870
+ : parsed.options.parallelToolCalls !== false;
871
+ }
872
+ if (parsed.stream) {
873
+ body.stream_options = { include_usage: true };
874
+ }
875
+
876
+ const url = `${provider.baseUrl}/chat/completions`;
877
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
878
+ // Precedence preserved from pre-#128 behavior: apiKey Authorization first, then
879
+ // provider.headers may override (user/registry-configured headers win). Registry
880
+ // staticHeaders (e.g. opencode-free x-opencode-client) flow in via derive.ts and
881
+ // never carry Authorization, so keyless providers are unaffected.
882
+ if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
883
+ if (provider.headers) Object.assign(headers, provider.headers);
884
+
885
+ const bodyJson = JSON.stringify(body);
886
+ // Never log pathname/query — tenant-scoped hosts (e.g. Cloudflare
887
+ // /accounts/<account_id>/ai/v1) would otherwise leak account identifiers (#452).
888
+ if (isDebugEnabled()) {
889
+ let host = "upstream";
890
+ try { host = new URL(url).host; } catch { /* keep fallback */ }
891
+ debugProviderDiagnostic("openai-chat", "request", {
892
+ host,
893
+ model: body.model,
894
+ stream: parsed.stream,
895
+ messageCount: Array.isArray(messages) ? messages.length : 0,
896
+ toolCount: Array.isArray(tools) ? tools.length : 0,
897
+ hasCredential,
898
+ bodyBytes: new TextEncoder().encode(bodyJson).length,
899
+ });
900
+ }
901
+
902
+ return {
903
+ url,
904
+ method: "POST",
905
+ headers,
906
+ body: bodyJson,
907
+ ...(reasoningLog ? { reasoningLog } : {}),
908
+ };
909
+ },
910
+
911
+ async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
912
+ if (!response.body) {
913
+ yield { type: "error", message: "No response body" };
914
+ return;
915
+ }
916
+
917
+ const reader = response.body.getReader();
918
+ const decoder = new TextDecoder();
919
+ const budgetEncoder = new TextEncoder();
920
+ let buffer = "";
921
+ let bufferBytes = 0;
922
+ // Streamed tool calls are BUFFERED until a terminal signal, then flushed as atomic
923
+ // start/delta/end sequences. The bridge treats text/reasoning deltas as barriers that
924
+ // close an open tool-call item (bridge.ts closeCurrentToolCall on text_delta), so
925
+ // emitting calls incrementally would orphan later argument deltas whenever a provider
926
+ // interleaves content — and parallel tool calls (multiple ids, index-keyed continuation
927
+ // chunks, whole-chunk calls) cannot be represented live without overlapping sequences.
928
+ // Keyed by `index` (OpenAI wire standard), falling back to `id`, falling back to the
929
+ // last-seen call for providers that omit both on continuation chunks.
930
+ interface PendingToolCall { key: string; id: string; name: string; args: string; argsBytes: number }
931
+ const pendingToolCalls: PendingToolCall[] = [];
932
+ let toolCallSeq = 0;
933
+ const closeToolCalls = (): PendingToolCall[] => {
934
+ const calls = [...pendingToolCalls];
935
+ for (const call of calls) budget.closeCall(call.key);
936
+ pendingToolCalls.length = 0;
937
+ return calls;
938
+ };
939
+ const flushToolCalls = function* (): Generator<AdapterEvent> {
940
+ // Do not treat flushed tool calls as user-facing output for the finish-less EOF
941
+ // fallback — incomplete tool args must stay on the truncation path.
942
+ for (const call of closeToolCalls()) {
943
+ if (!call.id) call.id = `call_${++toolCallSeq}`;
944
+ yield { type: "tool_call_start", id: call.id, name: call.name };
945
+ if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
946
+ yield { type: "tool_call_end" };
947
+ }
948
+ };
949
+ const terminateWithError = function* (
950
+ event: Extract<AdapterEvent, { type: "error" }>,
951
+ ): Generator<AdapterEvent, "terminate"> {
952
+ closeToolCalls();
953
+ yield event;
954
+ return "terminate";
955
+ };
956
+ let pendingUsage: OcxUsage | undefined;
957
+ // Track terminal signals so a socket EOF without any terminator can fail closed instead of
958
+ // being reported as a clean completion (silent truncation). A graceful close is either an
959
+ // explicit `[DONE]` sentinel OR a chunk carrying a non-null `finish_reason` (some
960
+ // OpenAI-compatible providers omit `[DONE]` but do send finish_reason).
961
+ let finishReason: string | undefined;
962
+ // Only answer text enables the finish-less EOF fallback. Reasoning-only streams can be
963
+ // suppressed by hideThinkingSummary and must not complete as empty successful turns.
964
+ let sawUserFacingOutput = false;
965
+
966
+ // Single per-line handler shared by the streaming loop and the EOF residual-frame flush, so
967
+ // a final frame is parsed identically wherever it lands (no duplicated, drift-prone parsing).
968
+ // Yields adapter events and returns "terminate" for a terminal frame ([DONE] / error) that
969
+ // must end the stream, or "continue" otherwise. Mutates the closure's terminal-signal state.
970
+ const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
971
+ const rawPayload = sseFieldValue(line, "data");
972
+ if (rawPayload === null) return "continue";
973
+ const payload = rawPayload.trim();
974
+ // A bare `data:` line carries nothing (heartbeat-style keep-alive on some gateways);
975
+ // it is not a malformed frame, just nothing to parse.
976
+ if (payload.length === 0) return "continue";
977
+ if (payload === "[DONE]") {
978
+ yield* flushToolCalls();
979
+ const stopReason = stopReasonFor(finishReason);
980
+ yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
981
+ return "terminate";
982
+ }
983
+
984
+ let parsed: unknown;
985
+ try {
986
+ parsed = JSON.parse(payload);
987
+ } catch {
988
+ yield { type: "error", message: "malformed upstream SSE data frame" };
989
+ return "terminate";
990
+ }
991
+ // Validate the shape instead of asserting it. `JSON.parse` yields a value, not necessarily
992
+ // an object — `JSON.parse("null")` returns null without throwing, so the catch above never
993
+ // sees it and the `chunk.error` read below crashed the stream mid-flight.
994
+ //
995
+ // Skip rather than terminate: `data: null` is emitted as a benign padding frame BETWEEN
996
+ // content deltas by real OpenAI-compatible routes (issue #1219), so failing here would
997
+ // discard the finish_reason chunk and [DONE] still in flight and turn a healthy response
998
+ // into a failed turn. Skipping cannot mask a genuinely broken stream — a stream carrying
999
+ // only such frames still sets neither finishReason nor sawUserFacingOutput and so trips
1000
+ // the EOF truncation guard below. An unparseable frame stays terminal.
1001
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1002
+ return "continue";
1003
+ }
1004
+ const chunk = parsed as Record<string, unknown>;
1005
+
1006
+ // A 200/OK chat-completions stream may carry an inline provider error envelope
1007
+ // instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
1008
+ // classified response.failed (bridge case "error") — never a truncated completion.
1009
+ if (chunk.error !== undefined && chunk.error !== null) {
1010
+ const event = upstreamErrorEvent(chunk.error, pendingUsage);
1011
+ debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
1012
+ return yield* terminateWithError(event);
1013
+ }
1014
+
1015
+ if (chunk.usage) {
1016
+ // Record usage but keep parsing: some providers send usage and the final content
1017
+ // delta in the SAME chunk; a bail here would drop that content. The choices
1018
+ // guard below no-ops a usage-only chunk.
1019
+ pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
1020
+ }
1021
+
1022
+ const choices = chunk.choices;
1023
+ if (choices === undefined) return "continue";
1024
+ if (!Array.isArray(choices)) {
1025
+ return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
1026
+ }
1027
+ if (choices.length === 0) return "continue";
1028
+ const rawChoice = choices[0];
1029
+ if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
1030
+ return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
1031
+ }
1032
+ const choice = rawChoice as {
1033
+ delta?: Record<string, unknown>;
1034
+ finish_reason?: string;
1035
+ error?: unknown;
1036
+ };
1037
+ if (choice.finish_reason === "error") {
1038
+ const event = upstreamErrorEvent(choice.error, pendingUsage);
1039
+ debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
1040
+ return yield* terminateWithError(event);
1041
+ }
1042
+ // Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
1043
+ // no delta) is a graceful close and must record finishReason even though we skip it below.
1044
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
1045
+ finishReason = choice.finish_reason;
1046
+ }
1047
+ const delta = choice.delta;
1048
+ if (delta) {
1049
+ const reasoningText = reasoningTextFrom(delta);
1050
+ if (reasoningText !== undefined) {
1051
+ yield { type: "reasoning_raw_delta", text: reasoningText };
1052
+ }
1053
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1054
+ sawUserFacingOutput = true;
1055
+ yield { type: "text_delta", text: delta.content };
1056
+ }
1057
+
1058
+ const toolCalls = delta.tool_calls as { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined;
1059
+ if (toolCalls) {
1060
+ for (const tc of toolCalls) {
1061
+ const key = typeof tc.index === "number"
1062
+ ? `i:${tc.index}`
1063
+ : tc.id
1064
+ ? `id:${tc.id}`
1065
+ : pendingToolCalls[pendingToolCalls.length - 1]?.key;
1066
+ let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
1067
+ // Mixed keying rescue: a call opened under an index key must still absorb an
1068
+ // id-only continuation for the same provider id (and vice versa) instead of
1069
+ // splitting into two calls that share one call_id downstream.
1070
+ if (!call && tc.id) call = pendingToolCalls.find(c => c.id === tc.id);
1071
+ if (!call) {
1072
+ call = { key: key ?? `seq:${pendingToolCalls.length}`, id: "", name: "", args: "", argsBytes: 0 };
1073
+ pendingToolCalls.push(call);
1074
+ budget.openCall(call.key);
1075
+ }
1076
+ if (tc.id && !call.id) call.id = tc.id;
1077
+ if (tc.function?.name && !call.name) call.name = tc.function.name;
1078
+ if (tc.function?.arguments) {
1079
+ const previousBytes = call.argsBytes;
1080
+ const nextBytes = previousBytes + budgetEncoder.encode(tc.function.arguments).byteLength;
1081
+ const scope = { kind: "tool_args" as const, callId: call.key };
1082
+ const reservation = budget.reserveTransient(nextBytes, scope);
1083
+ try {
1084
+ call.args += tc.function.arguments;
1085
+ reservation.commitRetained();
1086
+ budget.releaseRetained(previousBytes, scope);
1087
+ call.argsBytes = nextBytes;
1088
+ } catch (error) {
1089
+ reservation.release();
1090
+ throw error;
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // Any non-empty finish_reason ends the generation: flush assembled tool calls as
1098
+ // atomic sequences (covers "tool_calls" AND providers that close tool turns with "stop").
1099
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
1100
+ yield* flushToolCalls();
1101
+ }
1102
+ return "continue";
1103
+ };
1104
+
1105
+ try {
1106
+ while (true) {
1107
+ const { done, value } = await reader.read();
1108
+ if (done) break;
1109
+ const decoded = decoder.decode(value, { stream: true });
1110
+ const nextBufferBytes = bufferBytes + budgetEncoder.encode(decoded).byteLength;
1111
+ if (nextBufferBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
1112
+ throw new Error(`translation SSE event exceeded ${TRANSLATOR_MAX_SSE_EVENT_BYTES} bytes`, {
1113
+ cause: { code: "translation_buffer_limit" },
1114
+ });
1115
+ }
1116
+ const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
1117
+ try {
1118
+ buffer += decoded;
1119
+ appendReservation.commitRetained();
1120
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1121
+ } catch (error) {
1122
+ appendReservation.release();
1123
+ throw error;
1124
+ }
1125
+ bufferBytes = nextBufferBytes;
1126
+
1127
+ const lines = buffer.split("\n");
1128
+ buffer = lines.pop() ?? "";
1129
+ const residualBytes = budgetEncoder.encode(buffer).byteLength;
1130
+ const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
1131
+ residualReservation.commitRetained();
1132
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1133
+ bufferBytes = residualBytes;
1134
+
1135
+ for (const line of lines) {
1136
+ if ((yield* handleDataLine(line)) === "terminate") return;
1137
+ }
1138
+ }
1139
+
1140
+ // Some providers send the terminal `data:` frame (carrying the final delta, finish_reason,
1141
+ // and/or usage) WITHOUT a trailing newline before closing the socket, so it never crosses
1142
+ // the split("\n") boundary and stays in `buffer`. Run it through the SAME handler so its
1143
+ // content/tool-calls are emitted and its terminal signal observed — otherwise a genuinely
1144
+ // complete stream loses its last frame and may be falsely failed below.
1145
+ if (buffer.length > 0) {
1146
+ if ((yield* handleDataLine(buffer)) === "terminate") return;
1147
+ }
1148
+ // Reader EOF. Prefer failing closed before flushing pending tool calls so the bridge
1149
+ // never sees a fabricated tool_call_end on a truncated mid-assembly stream.
1150
+ //
1151
+ // Checked BEFORE flushToolCalls(), because that helper emits tool_call_end and there is no
1152
+ // taking it back: a half-assembled argument string would reach the client as a completed
1153
+ // call. Tool calls are buffered here (unlike the Anthropic adapter, which forwards
1154
+ // fragments live), so this adapter can still decide.
1155
+ const sawFinish = finishReason !== undefined;
1156
+ if (!sawFinish && pendingToolCalls.length > 0) {
1157
+ debugProviderDiagnostic("openai-chat", "stream-truncated", {
1158
+ finishReason: null,
1159
+ hadUsage: pendingUsage !== undefined,
1160
+ pendingToolCalls: pendingToolCalls.length,
1161
+ });
1162
+ yield { type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" };
1163
+ return;
1164
+ }
1165
+ // Finish-less EOF is only safe when answer text was emitted. Reasoning-only / usage-only
1166
+ // truncations must stay on the error path (hideThinkingSummary can suppress reasoning).
1167
+ // Trailing usage alone is not a terminal signal for this adapter (#735 / restore #773).
1168
+ if (!sawFinish && !sawUserFacingOutput) {
1169
+ debugProviderDiagnostic("openai-chat", "stream-truncated", {
1170
+ finishReason: finishReason ?? null,
1171
+ hadUsage: pendingUsage !== undefined,
1172
+ });
1173
+ yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
1174
+ return;
1175
+ }
1176
+ yield* flushToolCalls();
1177
+ // Graceful close that omitted [DONE] but delivered finish_reason and/or answer text.
1178
+ const stopReason = stopReasonFor(finishReason);
1179
+ yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
1180
+ } catch (error) {
1181
+ if (isTranslatorBudgetExceededError(error)
1182
+ || (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit")) {
1183
+ yield {
1184
+ type: "error",
1185
+ status: 502,
1186
+ errorType: "upstream_error",
1187
+ code: "translation_buffer_limit",
1188
+ message: "upstream translation buffer exceeded the safe limit",
1189
+ };
1190
+ try { await reader.cancel(error); } catch { /* already closed */ }
1191
+ return;
1192
+ }
1193
+ throw error;
1194
+ } finally {
1195
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1196
+ closeToolCalls();
1197
+ reader.releaseLock();
1198
+ }
1199
+ },
1200
+
1201
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1202
+ const json = await response.json() as Record<string, unknown>;
1203
+ const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
1204
+ budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
1205
+ try {
1206
+ const payload = unwrapChatCompletionPayload(json);
1207
+ const usage = usageFromOpenAIChat(payload.usage as Record<string, unknown> | undefined);
1208
+ if (json.success === false && payload.error === undefined) {
1209
+ return [{
1210
+ type: "error",
1211
+ message: "upstream reported failure without an error payload",
1212
+ ...(usage ? { usage } : {}),
1213
+ }];
1214
+ }
1215
+ if (payload.error !== undefined && payload.error !== null) {
1216
+ return [upstreamErrorEvent(payload.error, usage)];
1217
+ }
1218
+
1219
+ const events: AdapterEvent[] = [];
1220
+ const choices = payload.choices as {
1221
+ message?: Record<string, unknown>;
1222
+ finish_reason?: unknown;
1223
+ error?: OpenAIChatError;
1224
+ }[] | undefined;
1225
+ if (!Array.isArray(choices) || choices.length === 0) {
1226
+ return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
1227
+ }
1228
+ const rawChoice = choices[0];
1229
+ if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
1230
+ return [invalidChoicesEvent(usage)];
1231
+ }
1232
+ const choice = rawChoice;
1233
+ if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
1234
+ if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
1235
+
1236
+ const msg = choice.message;
1237
+ const reasoningText = reasoningTextFrom(msg);
1238
+ if (reasoningText !== undefined) {
1239
+ events.push({ type: "reasoning_raw_delta", text: reasoningText });
1240
+ }
1241
+ if (typeof msg.content === "string") {
1242
+ events.push({ type: "text_delta", text: msg.content });
1243
+ }
1244
+ const toolCalls = msg.tool_calls as { id: string; function: { name: string; arguments: string } }[] | undefined;
1245
+ if (toolCalls) {
1246
+ for (const tc of toolCalls) {
1247
+ events.push({ type: "tool_call_start", id: tc.id, name: tc.function.name });
1248
+ events.push({ type: "tool_call_delta", arguments: tc.function.arguments });
1249
+ events.push({ type: "tool_call_end" });
1250
+ }
1251
+ }
1252
+ const stopReason = stopReasonFor(choice.finish_reason);
1253
+ events.push({
1254
+ type: "done",
1255
+ usage,
1256
+ ...(stopReason ? { stopReason } : {}),
1257
+ });
1258
+ retainTranslatedEventBatch(events, budget);
1259
+ return events;
1260
+ } finally {
1261
+ budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
1262
+ }
1263
+ },
1264
+ };
1265
+ }