@cjhyy/code-shell-core 0.5.0-rc.1 → 0.6.0-rc.1

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 (528) hide show
  1. package/README.md +13 -10
  2. package/dist/agent/agent-definition-registry.d.ts +21 -3
  3. package/dist/agent/agent-definition-registry.js +49 -18
  4. package/dist/agent/agent-definition.d.ts +32 -0
  5. package/dist/agent/agent-definition.js +49 -4
  6. package/dist/arena/arena.js +11 -12
  7. package/dist/arena/context/context-tools.js +47 -6
  8. package/dist/arena/context/within-root.d.ts +7 -0
  9. package/dist/arena/context/within-root.js +15 -0
  10. package/dist/arena/iterate/phases/argue.js +8 -2
  11. package/dist/arena/iterate/phases/revise.js +1 -1
  12. package/dist/arena/iterate/phases/tournament.js +3 -3
  13. package/dist/arena/ledger.js +9 -1
  14. package/dist/arena/phases/adjudication.js +1 -4
  15. package/dist/arena/phases/build-consensus.js +1 -4
  16. package/dist/arena/phases/cross-review.js +2 -8
  17. package/dist/arena/phases/debate-rounds.js +1 -4
  18. package/dist/arena/phases/participant-research.js +1 -4
  19. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  20. package/dist/arena/planner.js +0 -1
  21. package/dist/arena/providers/docs.d.ts +1 -0
  22. package/dist/arena/providers/docs.js +5 -2
  23. package/dist/arena/providers/repo.d.ts +1 -0
  24. package/dist/arena/providers/repo.js +5 -2
  25. package/dist/arena/strategies/utils.d.ts +1 -4
  26. package/dist/arena/strategies/utils.js +7 -51
  27. package/dist/arena/transitions.d.ts +7 -2
  28. package/dist/arena/transitions.js +9 -4
  29. package/dist/arena/types.d.ts +8 -2
  30. package/dist/automation/cron-expr.d.ts +41 -0
  31. package/dist/automation/cron-expr.js +169 -0
  32. package/dist/automation/index.d.ts +41 -0
  33. package/dist/automation/index.js +39 -0
  34. package/dist/automation/runner.d.ts +84 -0
  35. package/dist/automation/runner.js +92 -0
  36. package/dist/automation/scheduler.d.ts +194 -0
  37. package/dist/automation/scheduler.js +623 -0
  38. package/dist/automation/store.d.ts +39 -0
  39. package/dist/automation/store.js +122 -0
  40. package/dist/automation/write-policy.d.ts +34 -0
  41. package/dist/automation/write-policy.js +113 -0
  42. package/dist/automation/write-run.d.ts +51 -0
  43. package/dist/automation/write-run.js +38 -0
  44. package/dist/capability-control/disabled-lists.d.ts +25 -0
  45. package/dist/capability-control/disabled-lists.js +57 -0
  46. package/dist/capability-control/index.d.ts +5 -0
  47. package/dist/capability-control/index.js +3 -0
  48. package/dist/capability-control/overlay.d.ts +64 -0
  49. package/dist/capability-control/overlay.js +116 -0
  50. package/dist/capability-control/project.d.ts +65 -0
  51. package/dist/capability-control/project.js +147 -0
  52. package/dist/capability-control/service.d.ts +70 -0
  53. package/dist/capability-control/service.js +147 -0
  54. package/dist/capability-control/types.d.ts +62 -0
  55. package/dist/capability-control/types.js +17 -0
  56. package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
  57. package/dist/cc-orchestrator/agent-adapter.js +133 -0
  58. package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
  59. package/dist/cc-orchestrator/cc-capability.js +53 -0
  60. package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
  61. package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
  62. package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
  63. package/dist/cc-orchestrator/codex-session-history.js +187 -0
  64. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  65. package/dist/cc-orchestrator/external-agent-changes.js +214 -0
  66. package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
  67. package/dist/cc-orchestrator/external-agent-driver.js +69 -0
  68. package/dist/cc-orchestrator/index.d.ts +8 -0
  69. package/dist/cc-orchestrator/index.js +8 -0
  70. package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
  71. package/dist/cc-orchestrator/relevance-judge.js +29 -0
  72. package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
  73. package/dist/cc-orchestrator/session-discovery.js +125 -0
  74. package/dist/cc-orchestrator/session-history.d.ts +19 -0
  75. package/dist/cc-orchestrator/session-history.js +67 -0
  76. package/dist/cli/agent-server-stdio.d.ts +26 -1
  77. package/dist/cli/agent-server-stdio.js +221 -28
  78. package/dist/cli/agent-server-tcp.d.ts +20 -0
  79. package/dist/cli/agent-server-tcp.js +134 -0
  80. package/dist/cli/graceful-shutdown.d.ts +27 -0
  81. package/dist/cli/graceful-shutdown.js +30 -0
  82. package/dist/context/compaction.d.ts +56 -0
  83. package/dist/context/compaction.js +209 -15
  84. package/dist/context/manager.d.ts +0 -20
  85. package/dist/context/manager.js +25 -41
  86. package/dist/cost-tracker.js +5 -61
  87. package/dist/credentials/cipher.d.ts +49 -0
  88. package/dist/credentials/cipher.js +45 -0
  89. package/dist/credentials/cookie-jar.d.ts +24 -0
  90. package/dist/credentials/cookie-jar.js +40 -0
  91. package/dist/credentials/index.d.ts +7 -0
  92. package/dist/credentials/index.js +5 -0
  93. package/dist/credentials/inject-credential-tool.d.ts +20 -0
  94. package/dist/credentials/inject-credential-tool.js +130 -0
  95. package/dist/credentials/store.d.ts +72 -0
  96. package/dist/credentials/store.js +184 -0
  97. package/dist/credentials/types.d.ts +56 -0
  98. package/dist/credentials/use-credential-tool.d.ts +29 -0
  99. package/dist/credentials/use-credential-tool.js +205 -0
  100. package/dist/credentials/use-gate.d.ts +56 -0
  101. package/dist/credentials/use-gate.js +52 -0
  102. package/dist/cron/cron-runtime.d.ts +2 -0
  103. package/dist/cron/cron-runtime.js +2 -0
  104. package/dist/cron/cron-store.d.ts +2 -0
  105. package/dist/cron/cron-store.js +2 -0
  106. package/dist/cron/scheduler.d.ts +5 -32
  107. package/dist/cron/scheduler.js +5 -113
  108. package/dist/data/model-metadata.d.ts +77 -0
  109. package/dist/data/model-metadata.js +56 -0
  110. package/dist/data/model-metadata.json +216 -0
  111. package/dist/data/openrouter-models.d.ts +18 -7
  112. package/dist/data/openrouter-models.js +35 -8
  113. package/dist/data/openrouter-sync.js +1 -1
  114. package/dist/engine/aux-key.d.ts +10 -0
  115. package/dist/engine/aux-key.js +11 -0
  116. package/dist/engine/dynamic-tool-defs.d.ts +19 -0
  117. package/dist/engine/dynamic-tool-defs.js +36 -0
  118. package/dist/engine/engine.d.ts +431 -108
  119. package/dist/engine/engine.js +1630 -279
  120. package/dist/engine/friendly-error.d.ts +18 -0
  121. package/dist/engine/friendly-error.js +63 -0
  122. package/dist/engine/goal.d.ts +187 -0
  123. package/dist/engine/goal.js +196 -0
  124. package/dist/engine/image-compression.d.ts +81 -0
  125. package/dist/engine/image-compression.js +186 -0
  126. package/dist/engine/image-policy.d.ts +165 -0
  127. package/dist/engine/image-policy.js +189 -0
  128. package/dist/engine/model-connections-pool.d.ts +17 -0
  129. package/dist/engine/model-connections-pool.js +66 -0
  130. package/dist/engine/model-facade.d.ts +10 -0
  131. package/dist/engine/model-facade.js +25 -0
  132. package/dist/engine/patch-orphaned-tools.js +3 -0
  133. package/dist/engine/reactive-threshold.d.ts +13 -0
  134. package/dist/engine/reactive-threshold.js +17 -0
  135. package/dist/engine/resolve-llm-config.d.ts +16 -0
  136. package/dist/engine/resolve-llm-config.js +44 -0
  137. package/dist/engine/runtime.js +9 -1
  138. package/dist/engine/sandbox-cache-key.d.ts +10 -0
  139. package/dist/engine/sandbox-cache-key.js +9 -0
  140. package/dist/engine/sandbox-config.d.ts +31 -0
  141. package/dist/engine/sandbox-config.js +38 -0
  142. package/dist/engine/session-title.d.ts +13 -0
  143. package/dist/engine/session-title.js +34 -0
  144. package/dist/engine/session-usage.d.ts +19 -0
  145. package/dist/engine/session-usage.js +25 -0
  146. package/dist/engine/steer-queue.d.ts +32 -0
  147. package/dist/engine/steer-queue.js +26 -0
  148. package/dist/engine/streaming-tool-queue.d.ts +12 -0
  149. package/dist/engine/streaming-tool-queue.js +50 -10
  150. package/dist/engine/turn-loop.d.ts +94 -9
  151. package/dist/engine/turn-loop.js +775 -342
  152. package/dist/engine/types.d.ts +175 -0
  153. package/dist/engine/types.js +13 -0
  154. package/dist/external-agents/config.d.ts +2 -0
  155. package/dist/external-agents/config.js +15 -0
  156. package/dist/external-agents/types.d.ts +31 -0
  157. package/dist/external-agents/types.js +1 -0
  158. package/dist/git/parse-log.d.ts +13 -0
  159. package/dist/git/parse-log.js +21 -0
  160. package/dist/git/utils.d.ts +8 -6
  161. package/dist/git/utils.js +62 -26
  162. package/dist/git/worktree.d.ts +48 -0
  163. package/dist/git/worktree.js +111 -20
  164. package/dist/hooks/events.d.ts +23 -1
  165. package/dist/hooks/goal-stop-hook.d.ts +58 -0
  166. package/dist/hooks/goal-stop-hook.js +279 -0
  167. package/dist/hooks/hook-output.d.ts +33 -0
  168. package/dist/hooks/hook-output.js +79 -0
  169. package/dist/hooks/registry.d.ts +15 -0
  170. package/dist/hooks/registry.js +57 -1
  171. package/dist/hooks/shell-runner.d.ts +2 -0
  172. package/dist/hooks/shell-runner.js +48 -18
  173. package/dist/index.d.ts +79 -11
  174. package/dist/index.js +89 -6
  175. package/dist/llm/capabilities/param-specs.d.ts +14 -0
  176. package/dist/llm/capabilities/param-specs.js +62 -0
  177. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  178. package/dist/llm/capabilities/reasoning-control.js +38 -0
  179. package/dist/llm/capabilities/rules.js +37 -3
  180. package/dist/llm/capabilities/types.d.ts +32 -2
  181. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  182. package/dist/llm/clamp-max-tokens.js +17 -0
  183. package/dist/llm/client-base.d.ts +69 -5
  184. package/dist/llm/client-base.js +185 -25
  185. package/dist/llm/client-factory.d.ts +3 -3
  186. package/dist/llm/client-factory.js +2 -2
  187. package/dist/llm/model-cache.js +4 -2
  188. package/dist/llm/model-pool.d.ts +29 -11
  189. package/dist/llm/model-pool.js +36 -20
  190. package/dist/llm/provider-auth.d.ts +41 -0
  191. package/dist/llm/provider-auth.js +76 -0
  192. package/dist/llm/provider-catalog.d.ts +6 -2
  193. package/dist/llm/providers/anthropic.d.ts +32 -2
  194. package/dist/llm/providers/anthropic.js +207 -18
  195. package/dist/llm/providers/openai.d.ts +47 -2
  196. package/dist/llm/providers/openai.js +478 -71
  197. package/dist/llm/reasoning-setting.d.ts +51 -0
  198. package/dist/llm/reasoning-setting.js +40 -0
  199. package/dist/llm/stop-reason.d.ts +13 -0
  200. package/dist/llm/stop-reason.js +21 -0
  201. package/dist/llm/stream-watchdog.js +5 -1
  202. package/dist/llm/strip-vision.d.ts +32 -0
  203. package/dist/llm/strip-vision.js +94 -0
  204. package/dist/llm/token-counter.js +9 -2
  205. package/dist/llm/types.d.ts +9 -6
  206. package/dist/logging/logger.js +18 -4
  207. package/dist/logging/sanitize-messages.d.ts +10 -0
  208. package/dist/logging/sanitize-messages.js +112 -0
  209. package/dist/lsp/client.js +28 -8
  210. package/dist/lsp/manager.d.ts +1 -1
  211. package/dist/lsp/manager.js +42 -11
  212. package/dist/lsp/root-path.d.ts +9 -0
  213. package/dist/lsp/root-path.js +12 -0
  214. package/dist/model-catalog/builtin.d.ts +12 -0
  215. package/dist/model-catalog/builtin.js +407 -0
  216. package/dist/model-catalog/gen-connections.d.ts +20 -0
  217. package/dist/model-catalog/gen-connections.js +28 -0
  218. package/dist/model-catalog/index.d.ts +41 -0
  219. package/dist/model-catalog/index.js +90 -0
  220. package/dist/model-catalog/params.d.ts +20 -0
  221. package/dist/model-catalog/params.js +45 -0
  222. package/dist/model-catalog/resolve.d.ts +48 -0
  223. package/dist/model-catalog/resolve.js +33 -0
  224. package/dist/model-catalog/save-entry.d.ts +32 -0
  225. package/dist/model-catalog/save-entry.js +104 -0
  226. package/dist/model-catalog/types.d.ts +561 -0
  227. package/dist/model-catalog/types.js +93 -0
  228. package/dist/model-catalog/upsert.d.ts +9 -0
  229. package/dist/model-catalog/upsert.js +8 -0
  230. package/dist/onboarding.d.ts +12 -82
  231. package/dist/onboarding.js +75 -333
  232. package/dist/plugins/gitOps.d.ts +53 -0
  233. package/dist/plugins/gitOps.js +130 -5
  234. package/dist/plugins/installer/checkUpdate.d.ts +16 -0
  235. package/dist/plugins/installer/checkUpdate.js +32 -0
  236. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  237. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  238. package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
  239. package/dist/plugins/installer/codex/convertCommands.js +46 -0
  240. package/dist/plugins/installer/codex/convertMcp.d.ts +11 -0
  241. package/dist/plugins/installer/codex/convertMcp.js +85 -0
  242. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  243. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  244. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  245. package/dist/plugins/installer/detectFormat.js +6 -0
  246. package/dist/plugins/installer/install.d.ts +7 -0
  247. package/dist/plugins/installer/install.js +123 -0
  248. package/dist/plugins/installer/installFromArchive.d.ts +43 -0
  249. package/dist/plugins/installer/installFromArchive.js +134 -0
  250. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  251. package/dist/plugins/installer/installFromSource.js +51 -0
  252. package/dist/plugins/installer/list.d.ts +9 -0
  253. package/dist/plugins/installer/list.js +30 -0
  254. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  255. package/dist/plugins/installer/loadPluginAgents.js +27 -0
  256. package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
  257. package/dist/plugins/installer/loadPluginMcp.js +107 -0
  258. package/dist/plugins/installer/parseSource.d.ts +20 -0
  259. package/dist/plugins/installer/parseSource.js +74 -0
  260. package/dist/plugins/installer/paths.d.ts +5 -0
  261. package/dist/plugins/installer/paths.js +25 -0
  262. package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
  263. package/dist/plugins/installer/pruneDisabled.js +73 -0
  264. package/dist/plugins/installer/types.d.ts +52 -0
  265. package/dist/plugins/installer/types.js +28 -0
  266. package/dist/plugins/installer/uninstall.d.ts +2 -0
  267. package/dist/plugins/installer/uninstall.js +18 -0
  268. package/dist/plugins/installer/unzip.d.ts +14 -0
  269. package/dist/plugins/installer/unzip.js +82 -0
  270. package/dist/plugins/installer/update.d.ts +28 -0
  271. package/dist/plugins/installer/update.js +97 -0
  272. package/dist/plugins/loadPluginHooks.d.ts +54 -1
  273. package/dist/plugins/loadPluginHooks.js +83 -1
  274. package/dist/plugins/marketplaceManager.d.ts +15 -1
  275. package/dist/plugins/marketplaceManager.js +58 -4
  276. package/dist/plugins/pluginCommandHook.js +34 -14
  277. package/dist/plugins/pluginContent.d.ts +30 -0
  278. package/dist/plugins/pluginContent.js +83 -0
  279. package/dist/plugins/pluginInstaller.d.ts +34 -0
  280. package/dist/plugins/pluginInstaller.js +168 -35
  281. package/dist/plugins/schemas.js +41 -6
  282. package/dist/plugins/types.d.ts +10 -0
  283. package/dist/preset/index.d.ts +7 -1
  284. package/dist/preset/index.js +112 -6
  285. package/dist/product/types.d.ts +1 -1
  286. package/dist/prompt/composer.d.ts +31 -0
  287. package/dist/prompt/composer.js +95 -25
  288. package/dist/prompt/instruction-scanner.js +5 -3
  289. package/dist/prompt/section-loader.js +1 -0
  290. package/dist/prompt/sections/base.md +1 -0
  291. package/dist/prompt/sections/browser.md +10 -0
  292. package/dist/prompt/sections/coding.md +4 -0
  293. package/dist/prompt/sections/orchestration.md +8 -0
  294. package/dist/protocol/chat-session-manager.d.ts +15 -2
  295. package/dist/protocol/chat-session-manager.js +46 -0
  296. package/dist/protocol/chat-session.d.ts +82 -0
  297. package/dist/protocol/chat-session.js +121 -1
  298. package/dist/protocol/client.d.ts +27 -0
  299. package/dist/protocol/client.js +38 -0
  300. package/dist/protocol/redact.d.ts +50 -0
  301. package/dist/protocol/redact.js +71 -0
  302. package/dist/protocol/server.d.ts +153 -2
  303. package/dist/protocol/server.js +807 -39
  304. package/dist/protocol/tcp-transport.d.ts +45 -0
  305. package/dist/protocol/tcp-transport.js +74 -0
  306. package/dist/protocol/transport.js +3 -2
  307. package/dist/protocol/types.d.ts +69 -0
  308. package/dist/protocol/types.js +22 -0
  309. package/dist/quota/credentials.d.ts +3 -0
  310. package/dist/quota/credentials.js +80 -0
  311. package/dist/quota/index.d.ts +36 -0
  312. package/dist/quota/index.js +155 -0
  313. package/dist/quota/types.d.ts +48 -0
  314. package/dist/quota/types.js +13 -0
  315. package/dist/remote/bridge.d.ts +11 -2
  316. package/dist/remote/bridge.js +61 -33
  317. package/dist/review/review-prompt.d.ts +28 -0
  318. package/dist/review/review-prompt.js +81 -0
  319. package/dist/run/ArtifactTracker.js +5 -4
  320. package/dist/run/EngineRunner.d.ts +27 -1
  321. package/dist/run/EngineRunner.js +46 -8
  322. package/dist/run/FileRunStore.js +40 -11
  323. package/dist/run/RunApprovalBackend.js +25 -5
  324. package/dist/run/RunLock.d.ts +28 -2
  325. package/dist/run/RunLock.js +49 -7
  326. package/dist/run/RunManager.d.ts +12 -0
  327. package/dist/run/RunManager.js +86 -3
  328. package/dist/run/RunQueue.d.ts +1 -0
  329. package/dist/run/RunQueue.js +8 -2
  330. package/dist/run/factory.d.ts +8 -1
  331. package/dist/run/factory.js +1 -0
  332. package/dist/run/index.d.ts +2 -2
  333. package/dist/run/index.js +1 -1
  334. package/dist/run/redirect-target.d.ts +7 -0
  335. package/dist/run/redirect-target.js +13 -0
  336. package/dist/run/types.js +1 -1
  337. package/dist/runtime/background-shell.d.ts +139 -0
  338. package/dist/runtime/background-shell.js +495 -0
  339. package/dist/runtime/output-clean.d.ts +24 -0
  340. package/dist/runtime/output-clean.js +41 -0
  341. package/dist/runtime/ring-file.d.ts +64 -0
  342. package/dist/runtime/ring-file.js +174 -0
  343. package/dist/runtime/safe-spawn.js +15 -34
  344. package/dist/runtime/spawn-common.d.ts +148 -0
  345. package/dist/runtime/spawn-common.js +303 -0
  346. package/dist/runtime/truncate-output.d.ts +22 -0
  347. package/dist/runtime/truncate-output.js +49 -0
  348. package/dist/runtime/utf8-cut.d.ts +11 -0
  349. package/dist/runtime/utf8-cut.js +23 -0
  350. package/dist/services/auto-dream.d.ts +4 -0
  351. package/dist/services/auto-dream.js +26 -26
  352. package/dist/services/browser-open.d.ts +13 -0
  353. package/dist/services/browser-open.js +16 -0
  354. package/dist/services/diagnostics.d.ts +1 -2
  355. package/dist/services/diagnostics.js +12 -7
  356. package/dist/services/dream-consolidation.d.ts +57 -0
  357. package/dist/services/dream-consolidation.js +151 -0
  358. package/dist/services/extract-memories.d.ts +14 -1
  359. package/dist/services/extract-memories.js +45 -6
  360. package/dist/services/memory-orchestrator.d.ts +21 -0
  361. package/dist/services/memory-orchestrator.js +125 -32
  362. package/dist/services/notifier.d.ts +8 -0
  363. package/dist/services/notifier.js +39 -25
  364. package/dist/services/oauth.js +5 -7
  365. package/dist/services/session-memory-sort.d.ts +8 -0
  366. package/dist/services/session-memory-sort.js +9 -0
  367. package/dist/services/session-memory.js +30 -19
  368. package/dist/session/file-history.d.ts +124 -1
  369. package/dist/session/file-history.js +232 -9
  370. package/dist/session/memory.d.ts +116 -2
  371. package/dist/session/memory.js +250 -28
  372. package/dist/session/session-manager.d.ts +63 -1
  373. package/dist/session/session-manager.js +186 -5
  374. package/dist/session/simple-diff.d.ts +23 -0
  375. package/dist/session/simple-diff.js +84 -0
  376. package/dist/session/transcript.d.ts +25 -1
  377. package/dist/session/transcript.js +35 -2
  378. package/dist/session/undo-target.d.ts +67 -0
  379. package/dist/session/undo-target.js +144 -0
  380. package/dist/settings/disk-defaults.d.ts +42 -0
  381. package/dist/settings/disk-defaults.js +33 -0
  382. package/dist/settings/feature-flags.d.ts +64 -0
  383. package/dist/settings/feature-flags.js +61 -0
  384. package/dist/settings/manager.d.ts +101 -1
  385. package/dist/settings/manager.js +345 -15
  386. package/dist/settings/migrate-config.d.ts +45 -0
  387. package/dist/settings/migrate-config.js +125 -0
  388. package/dist/settings/personalization.d.ts +19 -0
  389. package/dist/settings/personalization.js +7 -0
  390. package/dist/settings/schema-export.d.ts +25 -0
  391. package/dist/settings/schema-export.js +38 -0
  392. package/dist/settings/schema.d.ts +1827 -396
  393. package/dist/settings/schema.js +361 -81
  394. package/dist/skills/scanner.d.ts +9 -0
  395. package/dist/skills/scanner.js +30 -2
  396. package/dist/stt/resolve-transcribe.d.ts +31 -0
  397. package/dist/stt/resolve-transcribe.js +108 -0
  398. package/dist/stt/transcribe.d.ts +51 -0
  399. package/dist/stt/transcribe.js +65 -0
  400. package/dist/tool-system/browser-bridge.d.ts +226 -0
  401. package/dist/tool-system/browser-bridge.js +163 -0
  402. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  403. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  404. package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
  405. package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
  406. package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
  407. package/dist/tool-system/builtin/agent-notifications.js +9 -3
  408. package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
  409. package/dist/tool-system/builtin/agent-output-file.js +72 -0
  410. package/dist/tool-system/builtin/agent-registry.d.ts +23 -0
  411. package/dist/tool-system/builtin/agent-registry.js +16 -4
  412. package/dist/tool-system/builtin/agent.d.ts +50 -11
  413. package/dist/tool-system/builtin/agent.js +576 -73
  414. package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
  415. package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
  416. package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
  417. package/dist/tool-system/builtin/apply-patch/index.js +7 -3
  418. package/dist/tool-system/builtin/arena.js +6 -7
  419. package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
  420. package/dist/tool-system/builtin/background-jobs.js +124 -0
  421. package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
  422. package/dist/tool-system/builtin/background-shell-tools.js +108 -0
  423. package/dist/tool-system/builtin/background-work.d.ts +67 -0
  424. package/dist/tool-system/builtin/background-work.js +86 -0
  425. package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
  426. package/dist/tool-system/builtin/bash-output-style.js +40 -0
  427. package/dist/tool-system/builtin/bash.d.ts +5 -2
  428. package/dist/tool-system/builtin/bash.js +98 -62
  429. package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
  430. package/dist/tool-system/builtin/browser-tools.js +312 -0
  431. package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
  432. package/dist/tool-system/builtin/cancel-goal.js +64 -0
  433. package/dist/tool-system/builtin/check-quota.d.ts +15 -0
  434. package/dist/tool-system/builtin/check-quota.js +34 -0
  435. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  436. package/dist/tool-system/builtin/complete-goal.js +45 -0
  437. package/dist/tool-system/builtin/config.js +7 -0
  438. package/dist/tool-system/builtin/cron.d.ts +7 -0
  439. package/dist/tool-system/builtin/cron.js +105 -9
  440. package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
  441. package/dist/tool-system/builtin/drive-claude-code.js +157 -0
  442. package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
  443. package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
  444. package/dist/tool-system/builtin/edit.d.ts +2 -1
  445. package/dist/tool-system/builtin/edit.js +21 -12
  446. package/dist/tool-system/builtin/eol.d.ts +29 -0
  447. package/dist/tool-system/builtin/eol.js +37 -0
  448. package/dist/tool-system/builtin/file-cache.d.ts +6 -0
  449. package/dist/tool-system/builtin/file-cache.js +8 -0
  450. package/dist/tool-system/builtin/generate-image.d.ts +60 -0
  451. package/dist/tool-system/builtin/generate-image.js +374 -0
  452. package/dist/tool-system/builtin/generate-video.d.ts +55 -0
  453. package/dist/tool-system/builtin/generate-video.js +361 -0
  454. package/dist/tool-system/builtin/grep.js +6 -2
  455. package/dist/tool-system/builtin/image-providers.d.ts +86 -0
  456. package/dist/tool-system/builtin/image-providers.js +190 -0
  457. package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
  458. package/dist/tool-system/builtin/image-uploader.js +74 -0
  459. package/dist/tool-system/builtin/index.d.ts +24 -1
  460. package/dist/tool-system/builtin/index.js +310 -25
  461. package/dist/tool-system/builtin/mcp-tools.js +23 -3
  462. package/dist/tool-system/builtin/memory.js +45 -7
  463. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  464. package/dist/tool-system/builtin/notebook-edit.js +1 -1
  465. package/dist/tool-system/builtin/powershell.js +4 -1
  466. package/dist/tool-system/builtin/read.d.ts +2 -1
  467. package/dist/tool-system/builtin/read.js +11 -3
  468. package/dist/tool-system/builtin/repl.js +4 -1
  469. package/dist/tool-system/builtin/skill.js +9 -0
  470. package/dist/tool-system/builtin/sleep.js +12 -5
  471. package/dist/tool-system/builtin/task.js +4 -1
  472. package/dist/tool-system/builtin/tool-search.js +25 -7
  473. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  474. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  475. package/dist/tool-system/builtin/video-providers.d.ts +154 -0
  476. package/dist/tool-system/builtin/video-providers.js +235 -0
  477. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  478. package/dist/tool-system/builtin/view-image.js +96 -0
  479. package/dist/tool-system/builtin/web-fetch.js +12 -2
  480. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  481. package/dist/tool-system/builtin/web-search.js +57 -10
  482. package/dist/tool-system/builtin/worktree.d.ts +2 -1
  483. package/dist/tool-system/builtin/worktree.js +31 -4
  484. package/dist/tool-system/builtin/write.d.ts +2 -1
  485. package/dist/tool-system/builtin/write.js +1 -1
  486. package/dist/tool-system/context.d.ts +166 -6
  487. package/dist/tool-system/executor.d.ts +3 -2
  488. package/dist/tool-system/executor.js +197 -119
  489. package/dist/tool-system/investigation-guard.d.ts +4 -1
  490. package/dist/tool-system/investigation-guard.js +20 -4
  491. package/dist/tool-system/mcp-manager.d.ts +78 -16
  492. package/dist/tool-system/mcp-manager.js +357 -28
  493. package/dist/tool-system/path-policy.d.ts +99 -0
  494. package/dist/tool-system/path-policy.js +578 -0
  495. package/dist/tool-system/permission.d.ts +36 -1
  496. package/dist/tool-system/permission.js +322 -117
  497. package/dist/tool-system/plan-mode-allowlist.d.ts +32 -0
  498. package/dist/tool-system/plan-mode-allowlist.js +65 -0
  499. package/dist/tool-system/registry.d.ts +1 -0
  500. package/dist/tool-system/registry.js +26 -1
  501. package/dist/tool-system/sandbox/index.d.ts +8 -0
  502. package/dist/tool-system/sandbox/index.js +7 -2
  503. package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
  504. package/dist/tool-system/validate-tool-metadata.js +63 -0
  505. package/dist/tool-system/validation.d.ts +10 -0
  506. package/dist/tool-system/validation.js +16 -3
  507. package/dist/types.d.ts +301 -15
  508. package/dist/updater.js +20 -9
  509. package/dist/utils/envUtils.d.ts +0 -9
  510. package/dist/utils/envUtils.js +3 -28
  511. package/dist/utils/exec.d.ts +40 -0
  512. package/dist/utils/exec.js +144 -0
  513. package/dist/utils/format.js +4 -2
  514. package/dist/utils/json.d.ts +12 -0
  515. package/dist/utils/json.js +92 -0
  516. package/dist/utils/lockfile.js +9 -2
  517. package/dist/utils/theme.d.ts +7 -1
  518. package/dist/utils/theme.js +5 -3
  519. package/dist/utils/toolDisplay.js +0 -1
  520. package/package.json +14 -7
  521. package/dist/agent/coordinator.d.ts +0 -49
  522. package/dist/agent/coordinator.js +0 -77
  523. package/dist/settings/manager.test.js +0 -73
  524. package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
  525. package/dist/tool-system/builtin/remote-trigger.js +0 -54
  526. package/dist/tool-system/builtin/send-message.d.ts +0 -6
  527. package/dist/tool-system/builtin/send-message.js +0 -47
  528. /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
@@ -13,7 +13,34 @@ import { ContextLimitError, LLMError, LLMRateLimitError } from "../../exceptions
13
13
  import { logger } from "../../logging/logger.js";
14
14
  import { countTokens } from "../token-counter.js";
15
15
  import { capabilitiesFor } from "../capabilities/index.js";
16
+ import { clampMaxTokens } from "../clamp-max-tokens.js";
17
+ import { resolveApiKey, resolveHeaders } from "../provider-auth.js";
18
+ import { stripVisionFromHistory } from "../strip-vision.js";
16
19
  import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError, } from "../stream-watchdog.js";
20
+ /**
21
+ * Extract prompt-cache counts from an OpenAI-compatible usage object.
22
+ *
23
+ * - Cache HITS live under `usage.prompt_tokens_details.cached_tokens` (NOT a
24
+ * top-level field). Both OpenAI and OpenRouter report this.
25
+ * - Cache WRITES (first-time prefix ingestion) are reported by OpenRouter as
26
+ * `prompt_tokens_details.cache_write_tokens` (verified live 2026-07-02).
27
+ * OpenAI's automatic caching has no separate write charge and omits it. We
28
+ * map it to `cacheCreationTokens` so the UI can show "writing cache" on the
29
+ * first turn, not just hits on later turns.
30
+ *
31
+ * Returns a spreadable partial so callers omit each key entirely when the API
32
+ * reports no value — keeping the field `undefined` rather than a misleading 0.
33
+ * See docs/todo/prompt-cache-optimization.md.
34
+ */
35
+ function cachedTokensOf(usage) {
36
+ const details = usage?.prompt_tokens_details;
37
+ const out = {};
38
+ if (typeof details?.cached_tokens === "number")
39
+ out.cacheReadTokens = details.cached_tokens;
40
+ if (typeof details?.cache_write_tokens === "number")
41
+ out.cacheCreationTokens = details.cache_write_tokens;
42
+ return out;
43
+ }
17
44
  /**
18
45
  * Consume an async iterable of stream chunks with an idle watchdog.
19
46
  * Returns the accumulated text — either from onChunk return values, or
@@ -26,12 +53,24 @@ import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError, } from "../stream-watch
26
53
  * the function rejects with StreamIdleTimeoutError.
27
54
  */
28
55
  export async function runStreamWithWatchdog(stream, opts = {}) {
29
- const watchdogActive = STREAM_WATCHDOG_CONFIG.enabled || opts.idleTimeoutMs !== undefined;
56
+ // An explicit idleTimeoutMs always activates the watchdog. Otherwise follow
57
+ // disableWatchdog (per-call override) if set, else the env default.
58
+ const watchdogActive = opts.idleTimeoutMs !== undefined ||
59
+ (opts.disableWatchdog === undefined
60
+ ? STREAM_WATCHDOG_CONFIG.enabled
61
+ : !opts.disableWatchdog);
30
62
  const idleTimeoutMs = opts.idleTimeoutMs ?? STREAM_WATCHDOG_CONFIG.idleTimeoutMs;
31
63
  let text = "";
32
64
  // Fast path: watchdog disabled AND caller did not override → no overhead.
33
65
  if (!watchdogActive) {
34
66
  for await (const chunk of stream) {
67
+ // Stop consuming the moment the turn is aborted — do NOT forward more
68
+ // chunks to onChunk (which emits text_delta to the UI). The SDK may still
69
+ // be draining buffered chunks after abort(); this prevents them leaking
70
+ // post-Stop. `break` from a for-await calls the iterator's return() for us,
71
+ // letting the SDK tear the stream down.
72
+ if (opts.signal?.aborted)
73
+ break;
35
74
  if (opts.onChunk) {
36
75
  text += opts.onChunk(chunk) ?? "";
37
76
  }
@@ -46,6 +85,11 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
46
85
  const iterator = stream[Symbol.asyncIterator]();
47
86
  try {
48
87
  while (true) {
88
+ // Abort short-circuit: stop before awaiting/forwarding the next chunk so
89
+ // buffered post-abort deltas never reach onChunk. The finally below calls
90
+ // iterator.return() to tear the SDK stream down.
91
+ if (opts.signal?.aborted)
92
+ break;
49
93
  const nextPromise = iterator.next();
50
94
  // Build a timeout promise that rejects if no chunk arrives in time.
51
95
  let timeoutHandle = null;
@@ -54,14 +98,34 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
54
98
  reject(new StreamIdleTimeoutError(idleTimeoutMs, opts.requestId));
55
99
  }, idleTimeoutMs);
56
100
  });
101
+ // Abort promise: resolve as soon as the signal fires so a cancel mid-chunk
102
+ // (while awaiting the next delta) breaks out immediately instead of
103
+ // waiting for the next chunk or the idle deadline.
104
+ const abortCleanups = [];
105
+ const abortPromise = new Promise((resolve) => {
106
+ const sig = opts.signal;
107
+ if (!sig)
108
+ return; // never resolves → no effect on the race
109
+ if (sig.aborted) {
110
+ resolve({ aborted: true });
111
+ return;
112
+ }
113
+ const onAbort = () => resolve({ aborted: true });
114
+ sig.addEventListener("abort", onAbort, { once: true });
115
+ abortCleanups.push(() => sig.removeEventListener("abort", onAbort));
116
+ });
57
117
  let result;
58
118
  try {
59
- result = await Promise.race([nextPromise, timeoutPromise]);
119
+ result = await Promise.race([nextPromise, timeoutPromise, abortPromise]);
60
120
  }
61
121
  finally {
62
122
  if (timeoutHandle !== null)
63
123
  clearTimeout(timeoutHandle);
124
+ for (const c of abortCleanups)
125
+ c();
64
126
  }
127
+ if ("aborted" in result)
128
+ break;
65
129
  if (result.done)
66
130
  break;
67
131
  const chunk = result.value;
@@ -88,17 +152,30 @@ export class OpenAIClient extends LLMClientBase {
88
152
  // client. Cheaper and more reliable than re-deriving from the model id when
89
153
  // a new variant ships before our regex knows about it.
90
154
  _forceMaxCompletionTokens = false;
91
- constructor(config) {
92
- super(config);
155
+ // Sticky override: some gpt-5.x variants reject `reasoning_effort` when it's
156
+ // combined with `tools` on /v1/chat/completions ("Please use /v1/responses
157
+ // instead"). Once we see that 400, drop `reasoning_effort` for the lifetime
158
+ // of the client so tool-calling turns (e.g. the dream consolidation loop)
159
+ // succeed. Omitting the field just means "model default reasoning", which is
160
+ // fine for our background/aux calls.
161
+ _dropReasoningEffort = false;
162
+ constructor(config, defaults) {
163
+ super(config, defaults);
93
164
  }
94
165
  initClient() {
95
166
  // Lazy init — client created on first use
96
167
  }
97
168
  get client() {
98
169
  if (!this._client) {
170
+ const headers = resolveHeaders(this.config.httpHeaders);
99
171
  this._client = new OpenAI({
100
- apiKey: this.config.apiKey ?? process.env.OPENAI_API_KEY,
172
+ // apiKey: explicit > authCommand stdout > OPENAI_API_KEY (TODO 7.2).
173
+ // OpenAI's SDK requires a non-empty string; fall back to a placeholder
174
+ // when a custom provider authenticates purely via httpHeaders.
175
+ apiKey: resolveApiKey(this.config, process.env.OPENAI_API_KEY) ??
176
+ (Object.keys(headers).length > 0 ? "x-headers-auth" : undefined),
101
177
  ...(this.config.baseUrl ? { baseURL: this.config.baseUrl } : {}),
178
+ ...(Object.keys(headers).length > 0 ? { defaultHeaders: headers } : {}),
102
179
  timeout: this.timeout,
103
180
  });
104
181
  }
@@ -119,12 +196,31 @@ export class OpenAIClient extends LLMClientBase {
119
196
  }
120
197
  return this._capability;
121
198
  }
199
+ /**
200
+ * True when this client routes an Anthropic-family model through OpenRouter's
201
+ * OpenAI-compatible endpoint. Anthropic caching is EXPLICIT — nothing is
202
+ * cached unless the request carries `cache_control` breakpoints (verified live
203
+ * 2026-07-02: plain requests to anthropic/claude-opus-4.7-fast via OpenRouter
204
+ * report cached_tokens 0 on every repeat; a single system-block breakpoint
205
+ * turns the whole stable prefix — tools + system — into a cache hit, ~89%
206
+ * cheaper on the follow-up). OpenAI and other OpenRouter models cache
207
+ * automatically, so they must NOT get breakpoints. The slug arrives resolved
208
+ * (e.g. "anthropic/claude-opus-4.7-fast") or as the router alias
209
+ * ("~anthropic/claude-opus-latest") — both start with an optional "~" then
210
+ * "anthropic/".
211
+ */
212
+ get isOpenRouterAnthropic() {
213
+ return this.config.providerKind === "openrouter" && /^~?anthropic\//.test(this.model);
214
+ }
122
215
  async createMessage(options) {
123
- return this.withRetry(async () => {
124
- // Per-call thinking wins; otherwise fall back to provider default
125
- // (settings.providers[].thinking, threaded through LLMConfig).
126
- const thinking = options.thinking ?? this.config.thinking;
127
- const messages = this.buildMessages(options.systemPrompt, options.messages, thinking);
216
+ return this.withRetry(async (requestSignal) => {
217
+ // requestSignal = caller's cancel signal composed with a per-request
218
+ // hard deadline (withRetry). Hand it to the SDK so a wedged socket is
219
+ // torn down instead of hanging for tens of minutes.
220
+ // Per-call reasoning wins; otherwise fall back to provider default
221
+ // (settings.providers[].reasoning, threaded through LLMConfig).
222
+ const reasoning = options.reasoning ?? this.config.reasoning;
223
+ const messages = this.buildMessages(options.systemPrompt, options.messages, reasoning);
128
224
  const tools = options.tools?.length ? this.convertTools(options.tools) : undefined;
129
225
  const span = logger.span("llm.request", {
130
226
  cat: "llm",
@@ -136,8 +232,8 @@ export class OpenAIClient extends LLMClientBase {
136
232
  });
137
233
  try {
138
234
  const response = options.stream && options.onChunk
139
- ? await this.streamMessage(options, messages, tools, thinking)
140
- : await this.nonStreamMessage(options, messages, tools, thinking);
235
+ ? await this.streamMessage(options, messages, tools, reasoning, requestSignal)
236
+ : await this.nonStreamMessage(options, messages, tools, reasoning, requestSignal);
141
237
  span.end({
142
238
  stopReason: response.stopReason,
143
239
  promptTokens: response.usage?.promptTokens,
@@ -151,56 +247,88 @@ export class OpenAIClient extends LLMClientBase {
151
247
  span.fail(err);
152
248
  throw err;
153
249
  }
154
- });
250
+ }, { signal: options.signal });
155
251
  }
156
252
  /**
157
253
  * Build the request body honoring the model's capability descriptor.
158
254
  * Centralized so both streaming and non-streaming paths agree on the
159
255
  * exact shape.
160
256
  */
161
- buildRequestBody(options, messages, tools, thinking, stream) {
257
+ buildRequestBody(options, messages, tools, reasoning, stream) {
162
258
  const cap = this.capability;
163
- const maxTokens = options.maxTokens ?? this.maxTokens;
259
+ // Clamp to the model's known output ceiling so a stale catalog value
260
+ // (e.g. 384000 inherited after a hot model switch) can't 400 a
261
+ // smaller-cap model. No known cap → send the value as-is.
262
+ const maxTokens = clampMaxTokens(options.maxTokens ?? this.maxTokens, cap.maxOutputTokens);
164
263
  // Token-limit field — capability picks `max_tokens` vs `max_completion_tokens`.
165
264
  // Sticky fallback (set by handleApiError on a 400) overrides the rule for
166
- // ids the regex hasn't learned about yet.
265
+ // ids the regex hasn't learned about yet. When neither a requested value nor
266
+ // a known cap exists, omit the field entirely and let the endpoint apply its
267
+ // own ceiling (rather than inventing 8192 and truncating long outputs).
167
268
  const useCompletion = this._forceMaxCompletionTokens || cap.tokenLimitField === "max_completion_tokens";
168
- const tokenLimit = useCompletion
169
- ? { max_completion_tokens: maxTokens }
170
- : { max_tokens: maxTokens };
171
- // Sampling params only include if the model accepts them.
172
- const sampling = {};
269
+ const tokenLimit = maxTokens === undefined
270
+ ? {}
271
+ : useCompletion
272
+ ? { max_completion_tokens: maxTokens }
273
+ : { max_tokens: maxTokens };
274
+ // Sampling params — only include if the model accepts them. Split by
275
+ // precedence so the request body composes correctly against catalog
276
+ // extraBody (see the merge below):
277
+ // - samplingDefault: the connection's DEFAULT temperature (this.temperature).
278
+ // It's the BASE — a user's catalog `temperature` in extraBody should
279
+ // override the bare default.
280
+ // - samplingOverride: a PER-REQUEST temperature (options.temperature). It
281
+ // wins over both the default and the catalog value.
282
+ const samplingDefault = {};
283
+ const samplingOverride = {};
173
284
  if (!cap.rejectedParams.has("temperature")) {
174
- sampling.temperature =
175
- options.temperature !== undefined ? options.temperature : this.temperature;
285
+ if (options.temperature !== undefined) {
286
+ samplingOverride.temperature = options.temperature;
287
+ }
288
+ else {
289
+ samplingDefault.temperature = this.temperature;
290
+ }
176
291
  }
177
- // Reasoning shape — different vendors, different fields, never combine.
178
- // We treat `options.thinking` as the user's intent ("enabled"/"disabled")
179
- // and translate to whichever wire shape the model expects.
180
- const reasoning = {};
181
- if (thinking) {
292
+ // Reasoning shape — translate the user's ReasoningSetting to the wire
293
+ // shape. Different vendors, different fields, never combine. We read the
294
+ // real picked level (no "medium" hardcode) only fall back to "medium"
295
+ // when the setting says "thinking on" but carries no explicit effort
296
+ // ({mode:"on"}).
297
+ const reasoningBody = {};
298
+ // gpt-5.5+ 400s when `reasoning_effort` rides alongside `tools` on
299
+ // /v1/chat/completions. If the capability flags that combo AND this request
300
+ // carries tools, suppress the field up-front (same effect as the reactive
301
+ // _dropReasoningEffort sticky, but BEFORE the first failed request — so we
302
+ // never burn a 400 + 1s-backoff retry on every tool-using turn).
303
+ const suppressEffort = this._dropReasoningEffort ||
304
+ (cap.reasoning.kind === "openai-effort" &&
305
+ cap.reasoning.noEffortWithTools === true &&
306
+ !!tools?.length);
307
+ if (reasoning && reasoning.mode !== "off") {
182
308
  switch (cap.reasoning.kind) {
183
309
  case "deepseek-thinking":
184
310
  // DeepSeek V4, Z.AI GLM-4.5+ — top-level {thinking: {type}}.
185
- reasoning.thinking = { type: thinking };
311
+ // Binary: any non-off means thinking on (effort irrelevant).
312
+ reasoningBody.thinking = { type: "enabled" };
186
313
  break;
187
314
  case "openai-effort":
188
315
  // OpenAI o-series, gpt-5+, Gemini OpenAI-compat, xAI grok-4.3,
189
316
  // Mistral magistral, Groq reasoning models — `reasoning_effort`.
190
- // "enabled" "medium" (safe middle). "disabled" → the capability's
191
- // `disabledEffort` value; defaults to "minimal" (OpenAI), but xAI
192
- // uses "low" (no minimal) and Mistral uses "none" (only high|none).
193
- reasoning.reasoning_effort =
194
- thinking === "disabled"
195
- ? (cap.reasoning.disabledEffort ?? "minimal")
196
- : "medium";
317
+ // Send the user's real level; {mode:"on"} (no level) "medium".
318
+ //
319
+ // Skip when the endpoint has told us (reactive _dropReasoningEffort)
320
+ // OR the capability tells us up-front (noEffortWithTools) that
321
+ // `reasoning_effort` is incompatible with `tools` here — sending it
322
+ // would just trigger the "use /v1/responses" 400.
323
+ if (!suppressEffort) {
324
+ reasoningBody.reasoning_effort =
325
+ reasoning.mode === "effort" ? reasoning.effort : "medium";
326
+ }
197
327
  break;
198
328
  case "openrouter-reasoning":
199
- // OpenRouter normalized shape — {reasoning: {effort, exclude}}.
200
- reasoning.reasoning =
201
- thinking === "disabled"
202
- ? { effort: "minimal", exclude: true }
203
- : { effort: "medium" };
329
+ // OpenRouter normalized shape — {reasoning: {effort}}.
330
+ reasoningBody.reasoning =
331
+ reasoning.mode === "effort" ? { effort: reasoning.effort } : { effort: "medium" };
204
332
  break;
205
333
  case "anthropic-budget":
206
334
  case "anthropic-adaptive":
@@ -210,19 +338,79 @@ export class OpenAIClient extends LLMClientBase {
210
338
  break;
211
339
  }
212
340
  }
341
+ else if (reasoning && reasoning.mode === "off") {
342
+ // Explicit OFF — each shape's "don't think" wire form.
343
+ switch (cap.reasoning.kind) {
344
+ case "deepseek-thinking":
345
+ reasoningBody.thinking = { type: "disabled" };
346
+ break;
347
+ case "openai-effort":
348
+ // The capability's `disabledEffort` (defaults "minimal"; xAI "low",
349
+ // Mistral "none"). Skip if the endpoint already rejected the field
350
+ // OR the capability says it's incompatible with tools here.
351
+ if (!suppressEffort) {
352
+ reasoningBody.reasoning_effort = cap.reasoning.disabledEffort ?? "minimal";
353
+ }
354
+ break;
355
+ case "openrouter-reasoning":
356
+ reasoningBody.reasoning = { effort: "minimal", exclude: true };
357
+ break;
358
+ default:
359
+ break;
360
+ }
361
+ }
362
+ // reasoning_summary (TODO 7.2): when the model uses the object-form
363
+ // reasoning shape (OpenRouter normalized / Responses-style), attach the
364
+ // requested summary level to that object. For the bare `reasoning_effort`
365
+ // shape there's no summary field on chat-completions, so we skip it rather
366
+ // than send an unknown top-level param.
367
+ if (this.config.reasoningSummary && reasoningBody.reasoning &&
368
+ typeof reasoningBody.reasoning === "object") {
369
+ reasoningBody.reasoning.summary =
370
+ this.config.reasoningSummary;
371
+ }
372
+ // Catalog-driven passthrough params (temperature/top_p/thinking etc, already
373
+ // wire-mapped from the connection's paramValues by applyParams). Filter each
374
+ // key by rejectedParams so we never send a field the model rejects (same
375
+ // contract as `sampling` above — e.g. temperature to gpt-5).
376
+ const extra = {};
377
+ const rejected = cap.rejectedParams;
378
+ for (const [k, v] of Object.entries(this.config.extraBody ?? {})) {
379
+ if (rejected.has(k))
380
+ continue;
381
+ extra[k] = v;
382
+ }
383
+ // Compose the param fragments with explicit precedence + deep merge:
384
+ // connection default sampling (base)
385
+ // ← static catalog extraBody
386
+ // ← per-request sampling override
387
+ // ← reasoning translation (wins)
388
+ // - A catalog `temperature` in extraBody overrides the bare connection
389
+ // default, but a PER-REQUEST temperature (options.temperature) overrides
390
+ // both — a plain `...sampling, ...extra` got this backwards. (review #4)
391
+ // - extra and reasoningBody can both carry nested objects under the SAME
392
+ // top-level key (a catalog param wired to `thinking.type`/`reasoning.effort`
393
+ // vs the reasoning translation's `thinking`/`reasoning`). A shallow spread
394
+ // would wholesale-replace one nested object; deepMergeInto merges them. (review #5)
395
+ const paramBody = {};
396
+ deepMergeInto(paramBody, samplingDefault);
397
+ deepMergeInto(paramBody, extra);
398
+ deepMergeInto(paramBody, samplingOverride);
399
+ deepMergeInto(paramBody, reasoningBody);
213
400
  return {
214
401
  model: this.model,
215
402
  messages,
216
403
  ...tokenLimit,
217
- ...sampling,
218
- ...reasoning,
404
+ ...paramBody,
405
+ // service_tier (TODO 7.2): passed through verbatim when configured.
406
+ ...(this.config.serviceTier ? { service_tier: this.config.serviceTier } : {}),
219
407
  ...(tools ? { tools } : {}),
220
408
  ...(stream ? { stream: true, stream_options: { include_usage: true } } : {}),
221
409
  };
222
410
  }
223
- async nonStreamMessage(options, messages, tools, thinking) {
411
+ async nonStreamMessage(options, messages, tools, reasoning, requestSignal) {
224
412
  try {
225
- const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, thinking, false), { signal: options.signal });
413
+ const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, false), { signal: requestSignal ?? options.signal });
226
414
  const choice = response.choices[0];
227
415
  if (!choice)
228
416
  throw new LLMError("No response from OpenAI", "openai");
@@ -230,6 +418,9 @@ export class OpenAIClient extends LLMClientBase {
230
418
  promptTokens: response.usage?.prompt_tokens ?? 0,
231
419
  completionTokens: response.usage?.completion_tokens ?? 0,
232
420
  totalTokens: response.usage?.total_tokens ?? 0,
421
+ // OpenAI caching is automatic; hits land here, NOT in a top-level
422
+ // cacheReadTokens field. Reading the wrong field made hit-rate invisible.
423
+ ...cachedTokensOf(response.usage),
233
424
  };
234
425
  this.recordUsage(usage, options);
235
426
  return this.processChoice(choice, usage);
@@ -239,13 +430,19 @@ export class OpenAIClient extends LLMClientBase {
239
430
  throw err;
240
431
  }
241
432
  }
242
- async streamMessage(options, messages, tools, thinking) {
433
+ async streamMessage(options, messages, tools, reasoning, requestSignal) {
434
+ const sdkSignal = requestSignal ?? options.signal;
243
435
  try {
244
- const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, thinking, true), { signal: options.signal });
436
+ const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, true), { signal: sdkSignal });
245
437
  let text = "";
246
438
  let reasoningContent = "";
247
439
  const toolCallsMap = new Map();
248
440
  let streamUsage;
441
+ // Last finish_reason seen on the stream. Without this we returned a
442
+ // hardcoded "stop", so an output-cap cutoff (finish_reason "length")
443
+ // was indistinguishable from a clean finish and the turn loop never
444
+ // ran its max-output continuation. Capture it and return it verbatim.
445
+ let finishReason;
249
446
  // TTFT — first chunk that actually carried text. Tool-call-only chunks
250
447
  // earlier in the stream don't count: the user-visible "text starts now"
251
448
  // moment is what we want to compare across providers.
@@ -257,6 +454,11 @@ export class OpenAIClient extends LLMClientBase {
257
454
  if (chunk.usage) {
258
455
  streamUsage = chunk.usage;
259
456
  }
457
+ // Capture finish_reason BEFORE the no-delta early return below: the
458
+ // final chunk frequently carries finish_reason with an empty delta.
459
+ const chunkFinish = chunk.choices?.[0]?.finish_reason;
460
+ if (chunkFinish)
461
+ finishReason = chunkFinish;
260
462
  const delta = chunk.choices[0]?.delta;
261
463
  if (!delta)
262
464
  return "";
@@ -287,11 +489,18 @@ export class OpenAIClient extends LLMClientBase {
287
489
  for (const tc of delta.tool_calls) {
288
490
  const idx = tc.index;
289
491
  if (!toolCallsMap.has(idx)) {
290
- toolCallsMap.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", args: "" });
291
- options.onChunk?.({
292
- type: "tool_use_start",
293
- toolCall: { id: tc.id, toolName: tc.function?.name },
294
- });
492
+ const id = tc.id ?? "";
493
+ const name = tc.function?.name ?? "";
494
+ toolCallsMap.set(idx, { id, name, args: "" });
495
+ // Only announce once we actually have an id+name; some providers
496
+ // stream them across deltas. Emitting undefined here violates the
497
+ // ToolCall contract and breaks downstream consumers.
498
+ if (id && name) {
499
+ options.onChunk?.({
500
+ type: "tool_use_start",
501
+ toolCall: { id, toolName: name },
502
+ });
503
+ }
295
504
  }
296
505
  const existing = toolCallsMap.get(idx);
297
506
  if (tc.id)
@@ -324,32 +533,49 @@ export class OpenAIClient extends LLMClientBase {
324
533
  return delta.content ?? "";
325
534
  };
326
535
  await runStreamWithWatchdog(stream, {
327
- idleTimeoutMs: STREAM_WATCHDOG_CONFIG.enabled
328
- ? STREAM_WATCHDOG_CONFIG.idleTimeoutMs
329
- : undefined,
536
+ // Idle watchdog is now ON by default (see STREAM_WATCHDOG_CONFIG) — it
537
+ // catches a stream that connects then stalls mid-generation; the
538
+ // per-request deadline (sdkSignal) catches connect/first-byte hangs.
539
+ idleTimeoutMs: STREAM_WATCHDOG_CONFIG.idleTimeoutMs,
330
540
  requestId,
331
541
  onChunk: handleChunk,
542
+ signal: sdkSignal,
332
543
  });
333
544
  const toolCalls = [];
334
545
  for (const [, tc] of toolCallsMap) {
546
+ // Drop incomplete tool calls: an empty id or name (the fallbacks set
547
+ // when a delta never delivered them) would become a malformed call
548
+ // that breaks transcript keying and tool dispatch downstream.
549
+ if (!tc.id || !tc.name) {
550
+ logger.warn("openai.incomplete_tool_call_dropped", {
551
+ id: tc.id || "(empty)",
552
+ name: tc.name || "(empty)",
553
+ });
554
+ continue;
555
+ }
335
556
  let args = {};
336
557
  try {
337
558
  args = JSON.parse(tc.args || "{}");
338
559
  }
339
- catch { }
560
+ catch {
561
+ /* intentional: model emitted malformed tool-call JSON — fall back to
562
+ empty args rather than crashing the stream; the tool layer reports
563
+ the resulting validation error back to the model. */
564
+ }
340
565
  toolCalls.push({ id: tc.id, toolName: tc.name, args });
341
566
  }
342
567
  const usage = {
343
568
  promptTokens: streamUsage?.prompt_tokens ?? 0,
344
569
  completionTokens: streamUsage?.completion_tokens ?? 0,
345
570
  totalTokens: streamUsage?.total_tokens ?? 0,
571
+ ...cachedTokensOf(streamUsage),
346
572
  };
347
573
  this.recordUsage(usage, options);
348
574
  return {
349
575
  text,
350
576
  toolCalls,
351
577
  usage,
352
- stopReason: "stop",
578
+ stopReason: finishReason ?? "stop",
353
579
  ...(reasoningContent ? { reasoningContent } : {}),
354
580
  };
355
581
  }
@@ -367,7 +593,11 @@ export class OpenAIClient extends LLMClientBase {
367
593
  try {
368
594
  args = JSON.parse(tc.function.arguments || "{}");
369
595
  }
370
- catch { }
596
+ catch {
597
+ /* intentional: model emitted malformed tool-call JSON — fall back to
598
+ empty args rather than crashing; the tool layer reports the
599
+ resulting validation error back to the model. */
600
+ }
371
601
  toolCalls.push({
372
602
  id: tc.id,
373
603
  toolName: tc.function.name,
@@ -384,8 +614,14 @@ export class OpenAIClient extends LLMClientBase {
384
614
  ...(reasoningContent ? { reasoningContent } : {}),
385
615
  };
386
616
  }
387
- buildMessages(systemPrompt, messages, thinking) {
617
+ buildMessages(systemPrompt, messages, reasoning) {
388
618
  const result = [{ role: "system", content: systemPrompt }];
619
+ // Drop historical image blocks when the active model can't accept vision.
620
+ // Engine.run only gates *new* attachments; an image left in history from
621
+ // when a vision model was active otherwise re-serializes into `image_url`
622
+ // below and 400s ("unknown variant `image_url`") after a model switch.
623
+ // Identity-preserving on the common path (vision models / no images).
624
+ messages = stripVisionFromHistory(messages, this.capability.supportsVision);
389
625
  // Reasoning-content echo-back contract — driven by capability:
390
626
  // "when-tools" : backfill an empty placeholder if the prior assistant
391
627
  // turn doesn't carry one (DeepSeek V4 + tools 400s
@@ -398,7 +634,7 @@ export class OpenAIClient extends LLMClientBase {
398
634
  const cap = this.capability;
399
635
  const hasTools = messages.some((m) => Array.isArray(m.content) &&
400
636
  m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"));
401
- const needsReasoningBackfill = thinking !== "disabled" &&
637
+ const needsReasoningBackfill = reasoning?.mode !== "off" &&
402
638
  cap.echoReasoning === "when-tools" &&
403
639
  hasTools;
404
640
  const stripReasoning = cap.echoReasoning === "never";
@@ -492,10 +728,44 @@ export class OpenAIClient extends LLMClientBase {
492
728
  const toolResults = [];
493
729
  for (const block of msg.content) {
494
730
  if (block.type === "tool_result" && block.tool_use_id) {
495
- toolResults.push({
496
- tool_use_id: block.tool_use_id,
497
- content: typeof block.content === "string" ? block.content : "",
498
- });
731
+ if (typeof block.content === "string") {
732
+ toolResults.push({
733
+ tool_use_id: block.tool_use_id,
734
+ content: block.content,
735
+ });
736
+ }
737
+ else if (Array.isArray(block.content)) {
738
+ // view_image returns an image inside tool_result.content. OpenAI's
739
+ // role:"tool" message can't carry an image, so split it: text stays
740
+ // in the tool message, image blocks are hoisted into imageParts and
741
+ // get emitted as their own user image_url message below.
742
+ const texts = [];
743
+ for (const inner of block.content) {
744
+ if (inner.type === "text" && inner.text) {
745
+ texts.push(inner.text);
746
+ }
747
+ else if (inner.type === "image" && inner.source) {
748
+ const wireDetail = mapImageDetailToOpenAI(this.imageDetail);
749
+ imageParts.push({
750
+ type: "image_url",
751
+ image_url: {
752
+ url: `data:${inner.source.media_type};base64,${inner.source.data}`,
753
+ ...(wireDetail ? { detail: wireDetail } : {}),
754
+ },
755
+ });
756
+ }
757
+ }
758
+ toolResults.push({
759
+ tool_use_id: block.tool_use_id,
760
+ content: texts.length > 0 ? texts.join("\n") : "[image returned to user message]",
761
+ });
762
+ }
763
+ else {
764
+ toolResults.push({
765
+ tool_use_id: block.tool_use_id,
766
+ content: "",
767
+ });
768
+ }
499
769
  }
500
770
  else if (block.type === "text" && block.text) {
501
771
  textParts.push(block.text);
@@ -503,13 +773,25 @@ export class OpenAIClient extends LLMClientBase {
503
773
  else if (block.type === "image" && block.source) {
504
774
  // OpenAI-compat image_url: every supported provider (OpenAI,
505
775
  // OpenRouter, OpenAI-compatible proxies for Gemini/xAI/etc)
506
- // accepts a base64 data URL as the URL. Per-(provider, model)
507
- // vision-capability gating happens earlier in Engine.run
508
- // by the time we reach here, the model supports vision.
776
+ // accepts a base64 data URL as the URL. Non-vision models never
777
+ // reach here stripVisionFromHistory() (top of buildMessages)
778
+ // has already swapped their image blocks for text placeholders,
779
+ // and Engine.run rejects *new* attachments to non-vision models.
780
+ //
781
+ // The `detail` hint is honored by OpenAI; OpenAI-compat
782
+ // proxies (OpenRouter for non-OpenAI models, etc.) tolerate
783
+ // the field even when their backend ignores it, so it's
784
+ // safe to always set when settings.images.detail is on.
785
+ // OpenAI's wire only accepts "low" / "high" / "auto"; map
786
+ // our internal "original" (a Codex-style high-fidelity
787
+ // marker) to "high" since OpenAI server-side scales 2048+
788
+ // images anyway.
789
+ const wireDetail = mapImageDetailToOpenAI(this.imageDetail);
509
790
  imageParts.push({
510
791
  type: "image_url",
511
792
  image_url: {
512
793
  url: `data:${block.source.media_type};base64,${block.source.data}`,
794
+ ...(wireDetail ? { detail: wireDetail } : {}),
513
795
  },
514
796
  });
515
797
  }
@@ -540,8 +822,55 @@ export class OpenAIClient extends LLMClientBase {
540
822
  }
541
823
  }
542
824
  }
825
+ if (this.isOpenRouterAnthropic) {
826
+ this.applyAnthropicCacheBreakpoints(result);
827
+ }
543
828
  return result;
544
829
  }
830
+ /**
831
+ * In-place: add prompt-cache breakpoints for Anthropic-over-OpenRouter.
832
+ * Mirrors the native anthropic provider (≤4 breakpoints):
833
+ * 1. System block — the stable prefix. Anthropic sees tools BEFORE the
834
+ * system prompt, so one marker on the system block caches tools too
835
+ * (verified live: system-only marker cached 3511/3952 prompt tokens
836
+ * including tool defs).
837
+ * 2. Last message — one rolling breakpoint so the growing conversation
838
+ * history becomes a cached prefix. Not scrolled: as history grows the
839
+ * "last message" naturally advances and its tail is the next write.
840
+ * A string `content` is lifted to a single-element `[{type:"text",...}]`
841
+ * array so it can carry `cache_control`; OpenRouter accepts this OpenAI
842
+ * multimodal wire form for text.
843
+ */
844
+ applyAnthropicCacheBreakpoints(messages) {
845
+ const mark = (m) => {
846
+ if (!m)
847
+ return;
848
+ // Lift a plain-string content to a text-block array so it can carry the
849
+ // marker. Non-text content (tool messages, image arrays) already uses an
850
+ // array of parts — mark the last part instead.
851
+ if (typeof m.content === "string") {
852
+ m.content = [
853
+ { type: "text", text: m.content, cache_control: { type: "ephemeral" } },
854
+ ];
855
+ return;
856
+ }
857
+ if (Array.isArray(m.content) && m.content.length > 0) {
858
+ // cache_control is an Anthropic-via-OpenRouter extension field, not in
859
+ // the OpenAI content-part union — attach through `unknown`.
860
+ const last = m.content[m.content.length - 1];
861
+ last.cache_control = { type: "ephemeral" };
862
+ }
863
+ };
864
+ // 1. System block (always index 0 — buildMessages seeds it first).
865
+ const sys = messages[0];
866
+ if (sys && sys.role === "system")
867
+ mark(sys);
868
+ // 2. Rolling history breakpoint on the very last message. Skip if it IS the
869
+ // system message (no conversation yet) — one breakpoint already covers it.
870
+ const last = messages[messages.length - 1];
871
+ if (last && last !== sys)
872
+ mark(last);
873
+ }
545
874
  convertTools(tools) {
546
875
  return tools.map((t) => ({
547
876
  type: "function",
@@ -553,22 +882,57 @@ export class OpenAIClient extends LLMClientBase {
553
882
  }));
554
883
  }
555
884
  handleApiError(err) {
885
+ // User pressed ESC / Stop — the SDK throws APIUserAbortError when
886
+ // the request's AbortSignal fires mid-flight. Rethrow it unchanged
887
+ // so callers up the chain (turn-loop → server.ts) can recognise it
888
+ // as a cancellation rather than a real API failure. Wrapping it
889
+ // into "OpenAI API error: Request was aborted" was surfacing a
890
+ // scary toast for what is, from the user's perspective, "I clicked
891
+ // Stop and it worked."
892
+ if (err instanceof OpenAI.APIUserAbortError) {
893
+ throw err;
894
+ }
556
895
  if (err instanceof OpenAI.APIError) {
557
896
  if (err.status === 429) {
558
897
  throw new LLMRateLimitError("openai");
559
898
  }
560
899
  const msg = (err.message ?? "").toLowerCase();
900
+ // Some 400s are deterministically self-correctable: we flip a sticky
901
+ // flag that changes the NEXT request body. For those we rethrow a
902
+ // STATUS-LESS LLMError so withRetry's isClientError() check doesn't bail
903
+ // (4xx is normally non-retryable) and the immediate retry goes out with
904
+ // the corrected body — fixing the call that triggered it, not just the
905
+ // next one.
906
+ let selfCorrected = false;
561
907
  // o-series / gpt-5+ reject `max_tokens` and demand
562
908
  // `max_completion_tokens`. The id-based regex catches the common
563
909
  // cases; this is the belt-and-suspenders path for ids that ship
564
910
  // before the regex knows about them (e.g. new `gpt-5.x` variants
565
- // routed via OpenAI-compatible proxies). Flip a sticky flag so the
566
- // next request — including `withRetry`'s next attempt — sends the
567
- // right field, instead of looping on the same 400.
911
+ // routed via OpenAI-compatible proxies).
568
912
  if (err.status === 400 &&
569
913
  msg.includes("max_tokens") &&
570
- msg.includes("max_completion_tokens")) {
914
+ msg.includes("max_completion_tokens") &&
915
+ !this._forceMaxCompletionTokens) {
571
916
  this._forceMaxCompletionTokens = true;
917
+ selfCorrected = true;
918
+ }
919
+ // gpt-5.x: "Function tools with reasoning_effort are not supported for
920
+ // <model> in /v1/chat/completions. Please use /v1/responses instead."
921
+ // Drop reasoning_effort for the lifetime of the client so the retry —
922
+ // and every later tool-calling turn — goes through. We can't switch to
923
+ // /v1/responses here, but tool calls work on /v1/chat/completions as long
924
+ // as reasoning_effort is absent.
925
+ if (err.status === 400 &&
926
+ msg.includes("reasoning_effort") &&
927
+ (msg.includes("tools") || msg.includes("/v1/responses")) &&
928
+ !this._dropReasoningEffort) {
929
+ this._dropReasoningEffort = true;
930
+ selfCorrected = true;
931
+ }
932
+ if (selfCorrected) {
933
+ // No status in details → withRetry treats it as retryable and reissues
934
+ // with the now-corrected request body.
935
+ throw new LLMError(`OpenAI API error (auto-correcting): ${err.message}`, "openai");
572
936
  }
573
937
  if (msg.includes("context_length_exceeded") ||
574
938
  msg.includes("maximum context length") ||
@@ -593,6 +957,27 @@ export class OpenAIClient extends LLMClientBase {
593
957
  throw err;
594
958
  }
595
959
  }
960
+ /**
961
+ * Merge `src` into `dst` in place, recursing into plain-object values so a
962
+ * nested field (e.g. `thinking.type`) from one source doesn't wholesale-replace
963
+ * the nested object from another. Arrays and non-objects overwrite. `src` wins
964
+ * on leaf collisions. Used to compose the request body's param fragments
965
+ * (static extraBody base ← per-request sampling ← reasoning translation) so
966
+ * shared nested keys like `thinking`/`reasoning` merge instead of clobber.
967
+ */
968
+ function deepMergeInto(dst, src) {
969
+ for (const [k, v] of Object.entries(src)) {
970
+ const cur = dst[k];
971
+ if (v && typeof v === "object" && !Array.isArray(v) &&
972
+ cur && typeof cur === "object" && !Array.isArray(cur)) {
973
+ deepMergeInto(cur, v);
974
+ }
975
+ else {
976
+ dst[k] = v;
977
+ }
978
+ }
979
+ return dst;
980
+ }
596
981
  /**
597
982
  * Pull a non-empty reasoning payload out of a provider message.
598
983
  *
@@ -604,3 +989,25 @@ function extractReasoningContent(msg) {
604
989
  const candidate = msg.reasoning_content ?? msg.reasoning;
605
990
  return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
606
991
  }
992
+ /**
993
+ * Map our internal image-clarity enum to the OpenAI wire enum.
994
+ *
995
+ * OpenAI accepts only `low` / `high` / `auto`. Our provider-agnostic
996
+ * levels are low / standard / high — the real token saving happens in
997
+ * the renderer downscale (long-edge cap) before send, so on the wire
998
+ * we only need the coarse low/high distinction: low→low, while both
999
+ * standard and high collapse to high (OpenAI's full-tile rendering).
1000
+ *
1001
+ * Legacy "original" (from older settings) is treated as high.
1002
+ *
1003
+ * Returns undefined when the caller didn't set a detail at all, so
1004
+ * the OpenAI client uses its own default ("auto", equivalent to
1005
+ * "high" today).
1006
+ */
1007
+ function mapImageDetailToOpenAI(detail) {
1008
+ if (!detail)
1009
+ return undefined;
1010
+ if (detail === "low")
1011
+ return "low";
1012
+ return "high";
1013
+ }