@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.12

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 (415) hide show
  1. package/CHANGELOG.md +142 -7
  2. package/dist/cli.js +23108 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +2 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/cli-commands.d.ts +12 -0
  11. package/dist/types/commands/launch.d.ts +5 -1
  12. package/dist/types/commands/read.d.ts +1 -1
  13. package/dist/types/commands/usage.d.ts +25 -0
  14. package/dist/types/config/api-key-resolver.d.ts +3 -0
  15. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  16. package/dist/types/config/model-discovery.d.ts +55 -0
  17. package/dist/types/config/model-registry.d.ts +8 -219
  18. package/dist/types/config/model-resolver.d.ts +34 -10
  19. package/dist/types/config/model-roles.d.ts +28 -0
  20. package/dist/types/config/models-config-schema.d.ts +523 -42
  21. package/dist/types/config/models-config.d.ts +385 -0
  22. package/dist/types/config/settings-schema.d.ts +41 -8
  23. package/dist/types/config/settings.d.ts +8 -1
  24. package/dist/types/debug/log-viewer.d.ts +1 -1
  25. package/dist/types/debug/raw-sse.d.ts +1 -1
  26. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  27. package/dist/types/eval/backend.d.ts +0 -2
  28. package/dist/types/eval/idle-timeout.d.ts +0 -4
  29. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  30. package/dist/types/eval/py/executor.d.ts +5 -0
  31. package/dist/types/eval/py/kernel.d.ts +6 -1
  32. package/dist/types/eval/py/runtime.d.ts +9 -0
  33. package/dist/types/exec/bash-executor.d.ts +2 -0
  34. package/dist/types/export/html/template.generated.d.ts +1 -1
  35. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  36. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  37. package/dist/types/hindsight/mental-models.d.ts +17 -8
  38. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  39. package/dist/types/internal-urls/types.d.ts +1 -1
  40. package/dist/types/lsp/edits.d.ts +9 -0
  41. package/dist/types/lsp/index.d.ts +2 -2
  42. package/dist/types/lsp/types.d.ts +2 -0
  43. package/dist/types/lsp/utils.d.ts +3 -0
  44. package/dist/types/mcp/json-rpc.d.ts +5 -0
  45. package/dist/types/memory-backend/index.d.ts +1 -0
  46. package/dist/types/memory-backend/runtime.d.ts +4 -0
  47. package/dist/types/memory-backend/types.d.ts +66 -1
  48. package/dist/types/mnemopi/state.d.ts +11 -1
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  51. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  52. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  53. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  54. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  55. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  56. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  57. package/dist/types/modes/components/footer.d.ts +1 -1
  58. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  59. package/dist/types/modes/components/hook-input.d.ts +4 -0
  60. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  61. package/dist/types/modes/components/model-selector.d.ts +1 -1
  62. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  63. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  64. package/dist/types/modes/components/session-selector.d.ts +1 -1
  65. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  66. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  67. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  68. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  69. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  70. package/dist/types/modes/components/user-message.d.ts +2 -1
  71. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  72. package/dist/types/modes/components/welcome.d.ts +19 -3
  73. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  74. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  75. package/dist/types/modes/index.d.ts +3 -3
  76. package/dist/types/modes/interactive-mode.d.ts +8 -3
  77. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  78. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  79. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  80. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  81. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  82. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  83. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  84. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  85. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  86. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  87. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  88. package/dist/types/modes/types.d.ts +4 -1
  89. package/dist/types/secrets/index.d.ts +1 -1
  90. package/dist/types/secrets/obfuscator.d.ts +8 -2
  91. package/dist/types/session/agent-session.d.ts +15 -3
  92. package/dist/types/session/auth-broker-config.d.ts +4 -0
  93. package/dist/types/session/session-manager.d.ts +1 -1
  94. package/dist/types/session/streaming-output.d.ts +23 -0
  95. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  96. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  97. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  98. package/dist/types/slash-commands/types.d.ts +1 -1
  99. package/dist/types/ssh/connection-manager.d.ts +8 -0
  100. package/dist/types/system-prompt.d.ts +2 -0
  101. package/dist/types/task/executor.d.ts +1 -0
  102. package/dist/types/task/index.d.ts +2 -2
  103. package/dist/types/task/parallel.d.ts +2 -2
  104. package/dist/types/task/types.d.ts +8 -0
  105. package/dist/types/task/worktree.d.ts +2 -0
  106. package/dist/types/thinking.d.ts +4 -0
  107. package/dist/types/tiny/title-client.d.ts +11 -0
  108. package/dist/types/tiny/title-protocol.d.ts +1 -0
  109. package/dist/types/tools/ask.d.ts +4 -0
  110. package/dist/types/tools/conflict-detect.d.ts +16 -0
  111. package/dist/types/tools/github-cache.d.ts +7 -0
  112. package/dist/types/tools/index.d.ts +6 -0
  113. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  114. package/dist/types/tui/output-block.d.ts +3 -3
  115. package/dist/types/utils/changelog.d.ts +8 -0
  116. package/dist/types/utils/git.d.ts +15 -2
  117. package/dist/types/utils/title-generator.d.ts +3 -2
  118. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  119. package/dist/types/web/scrapers/types.d.ts +12 -0
  120. package/dist/types/web/search/providers/codex.d.ts +1 -1
  121. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  122. package/examples/extensions/tools.ts +5 -4
  123. package/package.json +14 -11
  124. package/scripts/build-binary.ts +18 -23
  125. package/scripts/bundle-dist.ts +81 -0
  126. package/scripts/{dev-launch → omp} +1 -1
  127. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  128. package/src/async/job-manager.ts +57 -3
  129. package/src/auto-thinking/classifier.ts +1 -0
  130. package/src/autoresearch/dashboard.ts +1 -1
  131. package/src/autoresearch/prompt-setup.md +6 -6
  132. package/src/autoresearch/prompt.md +6 -6
  133. package/src/capability/fs.ts +10 -0
  134. package/src/cli/args.ts +4 -1
  135. package/src/cli/auth-gateway-cli.ts +1 -3
  136. package/src/cli/dry-balance-cli.ts +1 -1
  137. package/src/cli/gallery-cli.ts +1 -1
  138. package/src/cli/gallery-fixtures/fs.ts +1 -1
  139. package/src/cli/gallery-fixtures/types.ts +5 -1
  140. package/src/cli/list-models.ts +2 -1
  141. package/src/cli/usage-cli.ts +603 -0
  142. package/src/cli-commands.ts +30 -0
  143. package/src/cli.ts +76 -13
  144. package/src/commands/complete.ts +1 -1
  145. package/src/commands/launch.ts +5 -1
  146. package/src/commands/read.ts +6 -3
  147. package/src/commands/usage.ts +35 -0
  148. package/src/commit/agentic/agent.ts +1 -1
  149. package/src/commit/model-selection.ts +4 -3
  150. package/src/config/api-key-resolver.ts +8 -6
  151. package/src/config/append-only-context-mode.ts +6 -12
  152. package/src/config/model-discovery.ts +554 -0
  153. package/src/config/model-registry.ts +320 -1041
  154. package/src/config/model-resolver.ts +173 -156
  155. package/src/config/model-roles.ts +74 -0
  156. package/src/config/models-config-schema.ts +57 -8
  157. package/src/config/models-config.ts +129 -0
  158. package/src/config/settings-schema.ts +61 -19
  159. package/src/config/settings.ts +98 -4
  160. package/src/dap/client.ts +124 -37
  161. package/src/dap/session.ts +259 -158
  162. package/src/debug/log-viewer.ts +1 -1
  163. package/src/debug/raw-sse.ts +1 -1
  164. package/src/edit/diff.ts +47 -3
  165. package/src/edit/hashline/block-resolver.ts +20 -1
  166. package/src/edit/hashline/diff.ts +36 -1
  167. package/src/edit/hashline/execute.ts +47 -4
  168. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  169. package/src/edit/index.ts +16 -1
  170. package/src/edit/modes/patch.ts +52 -0
  171. package/src/edit/modes/replace.ts +56 -22
  172. package/src/edit/notebook.ts +22 -2
  173. package/src/edit/renderer.ts +36 -10
  174. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  175. package/src/eval/backend.ts +0 -2
  176. package/src/eval/completion-bridge.ts +3 -1
  177. package/src/eval/idle-timeout.ts +2 -9
  178. package/src/eval/js/context-manager.ts +6 -8
  179. package/src/eval/js/executor.ts +6 -2
  180. package/src/eval/js/index.ts +0 -2
  181. package/src/eval/js/shared/helpers.ts +5 -6
  182. package/src/eval/js/shared/local-module-loader.ts +1 -1
  183. package/src/eval/js/shared/prelude.txt +62 -1
  184. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  185. package/src/eval/js/shared/runtime.ts +1 -1
  186. package/src/eval/py/executor.ts +29 -7
  187. package/src/eval/py/index.ts +6 -3
  188. package/src/eval/py/kernel.ts +43 -4
  189. package/src/eval/py/runner.py +107 -3
  190. package/src/eval/py/runtime.ts +37 -0
  191. package/src/exec/bash-executor.ts +85 -4
  192. package/src/export/html/template.generated.ts +1 -1
  193. package/src/export/html/template.js +3 -1
  194. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  195. package/src/extensibility/extensions/runner.ts +6 -1
  196. package/src/extensibility/extensions/types.ts +6 -2
  197. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  198. package/src/hindsight/bank.ts +17 -2
  199. package/src/hindsight/mental-models.ts +59 -12
  200. package/src/hindsight/state.ts +6 -1
  201. package/src/internal-urls/artifact-protocol.ts +11 -2
  202. package/src/internal-urls/docs-index.generated.ts +11 -11
  203. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  204. package/src/internal-urls/router.ts +1 -1
  205. package/src/internal-urls/types.ts +1 -1
  206. package/src/lib/xai-http.ts +1 -1
  207. package/src/lsp/client.ts +118 -38
  208. package/src/lsp/clients/biome-client.ts +101 -39
  209. package/src/lsp/edits.ts +143 -95
  210. package/src/lsp/index.ts +31 -22
  211. package/src/lsp/render.ts +1 -1
  212. package/src/lsp/types.ts +2 -0
  213. package/src/lsp/utils.ts +28 -10
  214. package/src/main.ts +183 -23
  215. package/src/mcp/json-rpc.ts +35 -5
  216. package/src/mcp/transports/stdio.ts +7 -1
  217. package/src/memories/index.ts +4 -1
  218. package/src/memory-backend/index.ts +1 -0
  219. package/src/memory-backend/local-backend.ts +9 -0
  220. package/src/memory-backend/off-backend.ts +9 -0
  221. package/src/memory-backend/runtime.ts +66 -0
  222. package/src/memory-backend/types.ts +81 -1
  223. package/src/mnemopi/backend.ts +176 -7
  224. package/src/mnemopi/state.ts +38 -2
  225. package/src/modes/acp/acp-agent.ts +119 -11
  226. package/src/modes/components/agent-dashboard.ts +10 -7
  227. package/src/modes/components/assistant-message.ts +32 -28
  228. package/src/modes/components/bash-execution.ts +1 -1
  229. package/src/modes/components/copy-selector.ts +1 -1
  230. package/src/modes/components/diff.ts +13 -2
  231. package/src/modes/components/dynamic-border.ts +12 -3
  232. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  233. package/src/modes/components/extensions/extension-list.ts +1 -1
  234. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  235. package/src/modes/components/footer.ts +4 -2
  236. package/src/modes/components/history-search.ts +1 -1
  237. package/src/modes/components/hook-editor.ts +8 -0
  238. package/src/modes/components/hook-input.ts +8 -0
  239. package/src/modes/components/hook-selector.ts +2 -2
  240. package/src/modes/components/model-selector.ts +4 -2
  241. package/src/modes/components/plan-review-overlay.ts +1 -1
  242. package/src/modes/components/session-observer-overlay.ts +2 -2
  243. package/src/modes/components/session-selector.ts +1 -1
  244. package/src/modes/components/settings-selector.ts +5 -1
  245. package/src/modes/components/status-line/component.ts +119 -35
  246. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  247. package/src/modes/components/transcript-container.ts +258 -53
  248. package/src/modes/components/tree-selector.ts +3 -3
  249. package/src/modes/components/user-message-selector.ts +1 -1
  250. package/src/modes/components/user-message.ts +17 -5
  251. package/src/modes/components/visual-truncate.ts +1 -1
  252. package/src/modes/components/welcome.ts +108 -26
  253. package/src/modes/controllers/command-controller.ts +11 -4
  254. package/src/modes/controllers/event-controller.ts +73 -4
  255. package/src/modes/controllers/input-controller.ts +2 -1
  256. package/src/modes/controllers/mcp-command-controller.ts +39 -4
  257. package/src/modes/controllers/selector-controller.ts +1 -1
  258. package/src/modes/controllers/streaming-reveal.ts +85 -18
  259. package/src/modes/index.ts +3 -21
  260. package/src/modes/interactive-mode.ts +42 -18
  261. package/src/modes/oauth-manual-input.ts +30 -3
  262. package/src/modes/rpc/rpc-client.ts +154 -3
  263. package/src/modes/rpc/rpc-mode.ts +97 -12
  264. package/src/modes/rpc/rpc-subagents.ts +265 -0
  265. package/src/modes/rpc/rpc-types.ts +81 -1
  266. package/src/modes/setup-wizard/index.ts +12 -2
  267. package/src/modes/setup-wizard/lazy.ts +16 -0
  268. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  269. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  270. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  271. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  272. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  273. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  274. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  275. package/src/modes/types.ts +4 -1
  276. package/src/prompts/agents/explore.md +2 -2
  277. package/src/prompts/agents/librarian.md +1 -2
  278. package/src/prompts/agents/oracle.md +1 -1
  279. package/src/prompts/agents/plan.md +5 -5
  280. package/src/prompts/agents/task.md +5 -5
  281. package/src/prompts/ci-green-request.md +5 -7
  282. package/src/prompts/goals/goal-budget-limit.md +2 -2
  283. package/src/prompts/goals/goal-continuation.md +4 -4
  284. package/src/prompts/goals/goal-mode-active.md +1 -1
  285. package/src/prompts/memories/read-path.md +1 -1
  286. package/src/prompts/memories/stage_one_system.md +2 -2
  287. package/src/prompts/review-custom-request.md +1 -1
  288. package/src/prompts/system/agent-creation-architect.md +2 -2
  289. package/src/prompts/system/auto-continue.md +1 -1
  290. package/src/prompts/system/background-tan-dispatch.md +1 -1
  291. package/src/prompts/system/btw-user.md +2 -2
  292. package/src/prompts/system/commit-message-system.md +13 -1
  293. package/src/prompts/system/custom-system-prompt.md +1 -1
  294. package/src/prompts/system/eager-todo.md +2 -2
  295. package/src/prompts/system/irc-incoming.md +1 -1
  296. package/src/prompts/system/manual-continue.md +1 -1
  297. package/src/prompts/system/omfg-user.md +3 -4
  298. package/src/prompts/system/orchestrate-notice.md +9 -9
  299. package/src/prompts/system/plan-mode-active.md +4 -4
  300. package/src/prompts/system/plan-mode-subagent.md +4 -5
  301. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  302. package/src/prompts/system/project-prompt.md +2 -2
  303. package/src/prompts/system/subagent-system-prompt.md +4 -4
  304. package/src/prompts/system/system-prompt.md +13 -24
  305. package/src/prompts/system/title-system.md +2 -2
  306. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  307. package/src/prompts/system/workflow-notice.md +1 -1
  308. package/src/prompts/tools/ast-edit.md +1 -1
  309. package/src/prompts/tools/ast-grep.md +2 -2
  310. package/src/prompts/tools/bash.md +5 -7
  311. package/src/prompts/tools/browser.md +7 -7
  312. package/src/prompts/tools/debug.md +1 -1
  313. package/src/prompts/tools/eval.md +3 -3
  314. package/src/prompts/tools/find.md +0 -1
  315. package/src/prompts/tools/github.md +8 -7
  316. package/src/prompts/tools/goal.md +1 -1
  317. package/src/prompts/tools/image-gen.md +1 -1
  318. package/src/prompts/tools/inspect-image-system.md +1 -1
  319. package/src/prompts/tools/irc.md +15 -15
  320. package/src/prompts/tools/lsp.md +2 -2
  321. package/src/prompts/tools/patch.md +2 -2
  322. package/src/prompts/tools/read.md +3 -4
  323. package/src/prompts/tools/recall.md +1 -1
  324. package/src/prompts/tools/reflect.md +1 -1
  325. package/src/prompts/tools/render-mermaid.md +2 -2
  326. package/src/prompts/tools/replace.md +4 -10
  327. package/src/prompts/tools/rewind.md +2 -2
  328. package/src/prompts/tools/search-tool-bm25.md +1 -9
  329. package/src/prompts/tools/search.md +0 -1
  330. package/src/prompts/tools/ssh.md +0 -4
  331. package/src/prompts/tools/task.md +2 -3
  332. package/src/prompts/tools/todo.md +1 -1
  333. package/src/sdk.ts +31 -11
  334. package/src/secrets/index.ts +8 -1
  335. package/src/secrets/obfuscator.ts +39 -18
  336. package/src/session/agent-session.ts +223 -64
  337. package/src/session/auth-broker-config.ts +30 -1
  338. package/src/session/session-manager.ts +2 -2
  339. package/src/session/streaming-output.ts +188 -11
  340. package/src/slash-commands/acp-builtins.ts +24 -0
  341. package/src/slash-commands/builtin-registry.ts +40 -0
  342. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  343. package/src/slash-commands/types.ts +1 -1
  344. package/src/ssh/connection-manager.ts +27 -0
  345. package/src/system-prompt.ts +14 -0
  346. package/src/task/commands.ts +2 -1
  347. package/src/task/executor.ts +74 -65
  348. package/src/task/index.ts +146 -68
  349. package/src/task/parallel.ts +3 -3
  350. package/src/task/render.ts +20 -5
  351. package/src/task/types.ts +9 -0
  352. package/src/task/worktree.ts +64 -56
  353. package/src/thinking.ts +9 -1
  354. package/src/tiny/title-client.ts +60 -16
  355. package/src/tiny/title-protocol.ts +1 -1
  356. package/src/tiny/worker.ts +6 -4
  357. package/src/tools/archive-reader.ts +30 -2
  358. package/src/tools/ask.ts +104 -21
  359. package/src/tools/ast-edit.ts +25 -5
  360. package/src/tools/auto-generated-guard.ts +20 -3
  361. package/src/tools/bash-interactive.ts +27 -7
  362. package/src/tools/bash.ts +100 -18
  363. package/src/tools/browser/launch.ts +11 -2
  364. package/src/tools/browser/readable.ts +19 -2
  365. package/src/tools/browser/registry.ts +4 -1
  366. package/src/tools/browser/render.ts +2 -2
  367. package/src/tools/browser/tab-supervisor.ts +55 -16
  368. package/src/tools/conflict-detect.ts +50 -4
  369. package/src/tools/debug.ts +1 -1
  370. package/src/tools/eval-render.ts +5 -5
  371. package/src/tools/eval.ts +0 -2
  372. package/src/tools/fetch.ts +33 -10
  373. package/src/tools/gh-cache-invalidation.ts +63 -8
  374. package/src/tools/gh-renderer.ts +1 -1
  375. package/src/tools/gh.ts +172 -29
  376. package/src/tools/github-cache.ts +70 -6
  377. package/src/tools/image-gen.ts +14 -13
  378. package/src/tools/index.ts +13 -1
  379. package/src/tools/inspect-image.ts +1 -0
  380. package/src/tools/irc.ts +5 -1
  381. package/src/tools/job.ts +1 -1
  382. package/src/tools/read.ts +202 -61
  383. package/src/tools/render-utils.ts +3 -3
  384. package/src/tools/resolve.ts +1 -1
  385. package/src/tools/search.ts +92 -29
  386. package/src/tools/sqlite-reader.ts +17 -5
  387. package/src/tools/ssh.ts +8 -8
  388. package/src/tools/todo.ts +38 -8
  389. package/src/tools/write.ts +118 -18
  390. package/src/tui/output-block.ts +4 -4
  391. package/src/utils/changelog.ts +27 -1
  392. package/src/utils/commit-message-generator.ts +1 -0
  393. package/src/utils/file-mentions.ts +2 -1
  394. package/src/utils/git.ts +267 -13
  395. package/src/utils/title-generator.ts +24 -5
  396. package/src/web/scrapers/arxiv.ts +1 -1
  397. package/src/web/scrapers/go-pkg.ts +1 -1
  398. package/src/web/scrapers/iacr.ts +1 -1
  399. package/src/web/scrapers/readthedocs.ts +1 -1
  400. package/src/web/scrapers/twitter.ts +2 -1
  401. package/src/web/scrapers/types.ts +87 -8
  402. package/src/web/scrapers/wikipedia.ts +1 -1
  403. package/src/web/scrapers/youtube.ts +6 -1
  404. package/src/web/search/index.ts +1 -1
  405. package/src/web/search/providers/codex.ts +2 -1
  406. package/src/web/search/providers/gemini.ts +2 -3
  407. package/src/web/search/render.ts +8 -6
  408. package/dist/types/config/model-equivalence.d.ts +0 -24
  409. package/dist/types/config/model-id-affixes.d.ts +0 -12
  410. package/dist/types/config/model-provider-priority.d.ts +0 -1
  411. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  412. package/src/config/model-equivalence.ts +0 -875
  413. package/src/config/model-id-affixes.ts +0 -81
  414. package/src/config/model-provider-priority.ts +0 -56
  415. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -1,28 +1,48 @@
1
1
  /**
2
- * Model resolution, scoping, and initial selection
2
+ * Model resolution, scoping, and initial selection.
3
+ *
4
+ * Layering:
5
+ * - `matchModel` is the single matching engine. Order: exact `provider/id`
6
+ * reference (with OpenRouter routed/date fallbacks) → exact canonical id →
7
+ * exact bare id → provider-scoped fuzzy → substring with alias-vs-dated pick.
8
+ * - `parseModelPatternWithContext`/`parseModelPattern` layer the selector
9
+ * grammar on top: trailing `:level` thinking suffixes (`splitThinkingSuffix`)
10
+ * and `@upstream` provider routing (`splitUpstreamRouting`).
11
+ * - Everything else (`resolveModelFromString`, `resolveModelOverride*`,
12
+ * `resolveRoleSelection`, `resolveModelScope`, `resolveCliModel`,
13
+ * `findSmolModel`/`findSlowModel`) adapts inputs — roles, settings patterns,
14
+ * CLI flags, scope globs — onto that pipeline.
3
15
  */
4
16
 
5
17
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
- import {
7
- type Api,
8
- clampThinkingLevelForModel,
9
- DEFAULT_MODEL_PER_PROVIDER,
10
- type Effort,
11
- type KnownProvider,
12
- type Model,
13
- modelsAreEqual,
14
- } from "@oh-my-pi/pi-ai";
18
+ import type { Api, Effort, KnownProvider, Model, ModelSpec } from "@oh-my-pi/pi-ai";
19
+ import { buildModel } from "@oh-my-pi/pi-catalog/build";
20
+ import { modelMatchesHost } from "@oh-my-pi/pi-catalog/hosts";
21
+ import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
22
+ import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
23
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
24
+ import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models";
15
25
  import { fuzzyMatch } from "@oh-my-pi/pi-tui";
16
26
  import { logger } from "@oh-my-pi/pi-utils";
17
27
  import chalk from "chalk";
18
28
  import MODEL_PRIO from "../priority.json" with { type: "json" };
19
29
  import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
20
- import { buildModelProviderPriorityRank } from "./model-provider-priority";
21
- import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
30
+ import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry";
31
+ import { MODEL_ROLE_IDS, type ModelRole } from "./model-roles";
22
32
  import type { Settings } from "./settings";
23
33
 
24
- /** Default model IDs for each known provider */
25
- export const defaultModelPerProvider: Record<KnownProvider, string> = DEFAULT_MODEL_PER_PROVIDER;
34
+ /**
35
+ * Pick the first available model matching a known provider's default id
36
+ * (catalog table order), falling back to the first available model.
37
+ */
38
+ function pickDefaultAvailableModel(availableModels: Model<Api>[]): Model<Api> | undefined {
39
+ for (const provider of Object.keys(DEFAULT_MODEL_PER_PROVIDER) as KnownProvider[]) {
40
+ const defaultId = DEFAULT_MODEL_PER_PROVIDER[provider];
41
+ const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
42
+ if (match) return match;
43
+ }
44
+ return availableModels[0];
45
+ }
26
46
 
27
47
  export interface ScopedModel {
28
48
  model: Model<Api>;
@@ -30,6 +50,22 @@ export interface ScopedModel {
30
50
  explicitThinkingLevel: boolean;
31
51
  }
32
52
 
53
+ /**
54
+ * Split a trailing `:<level>` thinking selector off a model pattern.
55
+ *
56
+ * `level` is set only when the suffix parses as a valid thinking level, in
57
+ * which case `base` has the suffix stripped; otherwise `base` is the input.
58
+ * `minColonIndex` requires the colon to appear strictly after that index —
59
+ * role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
60
+ * as long as the `pi/` prefix.
61
+ */
62
+ function splitThinkingSuffix(pattern: string, minColonIndex = -1): { base: string; level?: ThinkingLevel } {
63
+ const colonIdx = pattern.lastIndexOf(":");
64
+ if (colonIdx <= minColonIndex) return { base: pattern };
65
+ const level = parseThinkingLevel(pattern.slice(colonIdx + 1));
66
+ return level ? { base: pattern.slice(0, colonIdx), level } : { base: pattern };
67
+ }
68
+
33
69
  /**
34
70
  * Parse a model string in "provider/modelId" format.
35
71
  * Returns undefined if the format is invalid.
@@ -42,15 +78,8 @@ export function parseModelString(
42
78
  const id = modelStr.slice(slashIdx + 1);
43
79
  const provider = modelStr.slice(0, slashIdx);
44
80
  // Strip valid thinking level suffix (e.g., "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high")
45
- const colonIdx = id.lastIndexOf(":");
46
- if (colonIdx !== -1) {
47
- const suffix = id.slice(colonIdx + 1);
48
- const thinkingLevel = parseThinkingLevel(suffix);
49
- if (thinkingLevel) {
50
- return { provider, id: id.slice(0, colonIdx), thinkingLevel };
51
- }
52
- }
53
- return { provider, id };
81
+ const { base, level } = splitThinkingSuffix(id);
82
+ return level ? { provider, id: base, thinkingLevel: level } : { provider, id };
54
83
  }
55
84
 
56
85
  /**
@@ -142,17 +171,19 @@ function splitUpstreamRouting(pattern: string): { base: string; upstream: string
142
171
 
143
172
  /** OpenRouter and Vercel AI Gateway are the aggregators that honor per-request upstream routing. */
144
173
  function supportsUpstreamRouting(model: Model<Api>): boolean {
145
- return model.baseUrl.includes("openrouter.ai") || model.baseUrl.includes("ai-gateway.vercel.sh");
174
+ return modelMatchesHost(model, "openrouter") || modelMatchesHost(model, "vercelAIGateway");
146
175
  }
147
176
 
148
177
  /** Pin a resolved aggregator model to a single upstream provider via its compat routing block. */
149
178
  function applyUpstreamRouting(model: Model<Api>, upstream: string): Model<Api> {
150
179
  const aggregatorModel = model as Model<"openai-completions">;
151
180
  const routing = { only: [upstream] };
152
- const compat = model.baseUrl.includes("ai-gateway.vercel.sh")
153
- ? { ...aggregatorModel.compat, vercelGatewayRouting: routing }
154
- : { ...aggregatorModel.compat, openRouterRouting: routing };
155
- return { ...model, compat } as Model<Api>;
181
+ return buildModel({
182
+ ...model,
183
+ compat: modelMatchesHost(model, "vercelAIGateway")
184
+ ? { ...aggregatorModel.compatConfig, vercelGatewayRouting: routing }
185
+ : { ...aggregatorModel.compatConfig, openRouterRouting: routing },
186
+ } as ModelSpec<Api>);
156
187
  }
157
188
 
158
189
  const kProviderModelIndex = Symbol("model-resolver.providerIndex");
@@ -339,10 +370,7 @@ function isAlias(id: string): boolean {
339
370
  * Find an exact explicit provider/model match.
340
371
  * Bare model ids are handled separately so canonical ids can coalesce variants.
341
372
  */
342
- export function findExactModelReferenceMatch(
343
- modelReference: string,
344
- availableModels: Model<Api>[],
345
- ): Model<Api> | undefined {
373
+ function findExactModelReferenceMatch(modelReference: string, availableModels: Model<Api>[]): Model<Api> | undefined {
346
374
  const trimmedReference = modelReference.trim();
347
375
  if (!trimmedReference) {
348
376
  return undefined;
@@ -378,10 +406,15 @@ function findExactCanonicalModelMatch(
378
406
  }
379
407
 
380
408
  /**
381
- * Try to match a pattern to a model from the available models list.
409
+ * The single model-matching engine. Tries, in order:
410
+ * 1. exact `provider/id` reference (OpenRouter routed/date fallbacks included),
411
+ * 2. exact canonical id (coalesces provider variants),
412
+ * 3. exact bare id (preference-ranked),
413
+ * 4. provider-scoped fuzzy match,
414
+ * 5. substring match with the alias-vs-dated pick.
382
415
  * Returns the matched model or undefined if no match found.
383
416
  */
384
- function tryMatchModel(
417
+ function matchModel(
385
418
  modelPattern: string,
386
419
  availableModels: Model<Api>[],
387
420
  context: ModelPreferenceContext,
@@ -505,31 +538,21 @@ function parseModelPatternWithContext(
505
538
  options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
506
539
  ): ParsedModelResult {
507
540
  // Try exact match first
508
- const exactMatch = tryMatchModel(pattern, availableModels, context, options);
541
+ const exactMatch = matchModel(pattern, availableModels, context, options);
509
542
  if (exactMatch) {
510
543
  return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
511
544
  }
512
545
 
513
- // No match - try splitting on last colon if present
514
- const lastColonIndex = pattern.lastIndexOf(":");
515
- if (lastColonIndex === -1) {
516
- // No colons, pattern simply doesn't match any model
517
- return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
518
- }
519
-
520
- const prefix = pattern.substring(0, lastColonIndex);
521
- const suffix = pattern.substring(lastColonIndex + 1);
522
-
523
- const parsedThinkingLevel = parseThinkingLevel(suffix);
524
- if (parsedThinkingLevel) {
525
- // Valid thinking level - recurse on prefix and use this level
526
- const result = parseModelPatternWithContext(prefix, availableModels, context, options);
546
+ // No match - try stripping a valid thinking suffix and recursing
547
+ const { base, level } = splitThinkingSuffix(pattern);
548
+ if (level) {
549
+ const result = parseModelPatternWithContext(base, availableModels, context, options);
527
550
  if (result.model) {
528
551
  // Only use this thinking level if no warning from inner recursion
529
552
  const explicitThinkingLevel = !result.warning;
530
553
  return {
531
554
  model: result.model,
532
- thinkingLevel: explicitThinkingLevel ? parsedThinkingLevel : undefined,
555
+ thinkingLevel: explicitThinkingLevel ? level : undefined,
533
556
  warning: result.warning,
534
557
  explicitThinkingLevel,
535
558
  };
@@ -537,6 +560,14 @@ function parseModelPatternWithContext(
537
560
  return result;
538
561
  }
539
562
 
563
+ const lastColonIndex = pattern.lastIndexOf(":");
564
+ if (lastColonIndex === -1) {
565
+ // No colons, pattern simply doesn't match any model
566
+ return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
567
+ }
568
+ const prefix = pattern.substring(0, lastColonIndex);
569
+ const suffix = pattern.substring(lastColonIndex + 1);
570
+
540
571
  const allowFallback = options?.allowInvalidThinkingSelectorFallback ?? true;
541
572
  if (!allowFallback) {
542
573
  return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
@@ -606,10 +637,7 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
606
637
  const normalized = value.trim();
607
638
  if (!normalized) return undefined;
608
639
 
609
- const lastColonIndex = normalized.lastIndexOf(":");
610
- const thinkingLevel =
611
- lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
612
- const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
640
+ const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(normalized, PREFIX_MODEL_ROLE.length);
613
641
  const role = getModelRoleAlias(aliasCandidate);
614
642
  if (!role) return [normalized];
615
643
 
@@ -695,7 +723,7 @@ export function resolveModelRoleValue(
695
723
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
696
724
  }
697
725
 
698
- const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
726
+ const effectivePatterns = resolveConfiguredModelPatterns(normalized, options?.settings);
699
727
  if (!effectivePatterns || effectivePatterns.length === 0) {
700
728
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
701
729
  }
@@ -736,9 +764,7 @@ export function extractExplicitThinkingSelector(
736
764
  let current = normalized;
737
765
  while (!visited.has(current)) {
738
766
  visited.add(current);
739
- const lastColonIndex = current.lastIndexOf(":");
740
- const thinkingSelector =
741
- lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(current.slice(lastColonIndex + 1)) : undefined;
767
+ const thinkingSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length).level;
742
768
  if (thinkingSelector) {
743
769
  return thinkingSelector;
744
770
  }
@@ -903,20 +929,8 @@ function resolveExactCanonicalScopePattern(
903
929
  modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
904
930
  availableModels: Model<Api>[],
905
931
  ): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
906
- const lastColonIndex = pattern.lastIndexOf(":");
907
- let canonicalId = pattern;
908
- let thinkingLevel: ThinkingLevel | undefined;
909
- let explicitThinkingLevel = false;
910
-
911
- if (lastColonIndex !== -1) {
912
- const suffix = pattern.substring(lastColonIndex + 1);
913
- const parsedThinkingLevel = parseThinkingLevel(suffix);
914
- if (parsedThinkingLevel) {
915
- canonicalId = pattern.substring(0, lastColonIndex);
916
- thinkingLevel = parsedThinkingLevel;
917
- explicitThinkingLevel = true;
918
- }
919
- }
932
+ const { base: canonicalId, level: thinkingLevel } = splitThinkingSuffix(pattern);
933
+ const explicitThinkingLevel = thinkingLevel !== undefined;
920
934
 
921
935
  const variants = modelRegistry
922
936
  .getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
@@ -947,25 +961,23 @@ export async function resolveModelScope(
947
961
  const availableModels = modelRegistry.getAvailable();
948
962
  const context = buildPreferenceContext(availableModels, preferences);
949
963
  const scopedModels: ScopedModel[] = [];
964
+ const addScopedModel = (model: Model<Api>, thinkingLevel: ThinkingLevel | undefined, explicit: boolean) => {
965
+ if (scopedModels.some(sm => modelsAreEqual(sm.model, model))) return;
966
+ scopedModels.push({
967
+ model,
968
+ thinkingLevel: explicit
969
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
970
+ : thinkingLevel,
971
+ explicitThinkingLevel: explicit,
972
+ });
973
+ };
950
974
 
951
975
  for (const pattern of patterns) {
952
976
  // Check if pattern contains glob characters
953
977
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
954
978
  // Extract optional thinking level suffix (e.g., "provider/*:high")
955
- const colonIdx = pattern.lastIndexOf(":");
956
- let globPattern = pattern;
957
- let thinkingLevel: ThinkingLevel | undefined;
958
- let explicitThinkingLevel = false;
959
-
960
- if (colonIdx !== -1) {
961
- const suffix = pattern.substring(colonIdx + 1);
962
- const parsedThinkingLevel = parseThinkingLevel(suffix);
963
- if (parsedThinkingLevel) {
964
- thinkingLevel = parsedThinkingLevel;
965
- explicitThinkingLevel = true;
966
- globPattern = pattern.substring(0, colonIdx);
967
- }
968
- }
979
+ const { base: globPattern, level: thinkingLevel } = splitThinkingSuffix(pattern);
980
+ const explicitThinkingLevel = thinkingLevel !== undefined;
969
981
 
970
982
  // Match against "provider/modelId" format OR just model ID
971
983
  // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*"
@@ -981,15 +993,7 @@ export async function resolveModelScope(
981
993
  }
982
994
 
983
995
  for (const model of matchingModels) {
984
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
985
- scopedModels.push({
986
- model,
987
- thinkingLevel: explicitThinkingLevel
988
- ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
989
- : thinkingLevel,
990
- explicitThinkingLevel,
991
- });
992
- }
996
+ addScopedModel(model, thinkingLevel, explicitThinkingLevel);
993
997
  }
994
998
  continue;
995
999
  }
@@ -997,16 +1001,7 @@ export async function resolveModelScope(
997
1001
  const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
998
1002
  if (exactCanonical) {
999
1003
  for (const model of exactCanonical.models) {
1000
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
1001
- scopedModels.push({
1002
- model,
1003
- thinkingLevel: exactCanonical.explicitThinkingLevel
1004
- ? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ??
1005
- exactCanonical.thinkingLevel)
1006
- : exactCanonical.thinkingLevel,
1007
- explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
1008
- });
1009
- }
1004
+ addScopedModel(model, exactCanonical.thinkingLevel, exactCanonical.explicitThinkingLevel);
1010
1005
  }
1011
1006
  continue;
1012
1007
  }
@@ -1027,16 +1022,7 @@ export async function resolveModelScope(
1027
1022
  continue;
1028
1023
  }
1029
1024
 
1030
- // Avoid duplicates
1031
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
1032
- scopedModels.push({
1033
- model,
1034
- thinkingLevel: explicitThinkingLevel
1035
- ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
1036
- : thinkingLevel,
1037
- explicitThinkingLevel,
1038
- });
1039
- }
1025
+ addScopedModel(model, thinkingLevel, explicitThinkingLevel);
1040
1026
  }
1041
1027
 
1042
1028
  return scopedModels;
@@ -1072,6 +1058,66 @@ export async function resolveAllowedModels(
1072
1058
  return available.filter(model => allowed.has(`${model.provider}/${model.id}`));
1073
1059
  }
1074
1060
 
1061
+ /**
1062
+ * Synchronous subset of {@link resolveAllowedModels} for contexts where async is unavailable
1063
+ * (e.g. `getAvailableModels()` which is called from the ACP model-list advertisement, RPC
1064
+ * `get_available_models`, and the `/model` slash command). Uses the same effective
1065
+ * `enabledModels` scope semantics as startup resolution:
1066
+ *
1067
+ * - Glob selectors match `provider/modelId` and bare model id
1068
+ * - Exact canonical ids expand to all available concrete variants
1069
+ * - Exact `provider/modelId`, bare ids, provider-scoped fuzzy, and substring selectors
1070
+ * resolve through the shared model-pattern matcher
1071
+ * - Optional `:thinkingLevel` suffixes are stripped only when valid
1072
+ *
1073
+ * When no pattern resolves to any model (misconfiguration / typo) an empty list is returned,
1074
+ * consistent with the empty-list contract of {@link resolveAllowedModels}. Callers that render
1075
+ * a UI picker should treat an empty list as "hide the picker entry", matching how the SDK
1076
+ * surfaces the same misconfiguration during session initialization.
1077
+ */
1078
+ export function filterAvailableModelsByEnabledPatterns(
1079
+ available: Model<Api>[],
1080
+ patterns: readonly string[],
1081
+ registry: Pick<ModelRegistry, "getCanonicalVariants">,
1082
+ ): Model<Api>[] {
1083
+ if (patterns.length === 0) return available;
1084
+
1085
+ const context = buildPreferenceContext(available, undefined);
1086
+ const allowed = new Set<string>();
1087
+ const addAllowed = (model: Model<Api>) => {
1088
+ allowed.add(`${model.provider}/${model.id}`);
1089
+ };
1090
+
1091
+ for (const pattern of patterns) {
1092
+ if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
1093
+ const { base: globPattern } = splitThinkingSuffix(pattern);
1094
+ const glob = new Bun.Glob(globPattern.toLowerCase());
1095
+ for (const model of available) {
1096
+ const fullId = `${model.provider}/${model.id}`.toLowerCase();
1097
+ if (glob.match(fullId) || glob.match(model.id.toLowerCase())) {
1098
+ addAllowed(model);
1099
+ }
1100
+ }
1101
+ continue;
1102
+ }
1103
+
1104
+ const exactCanonical = resolveExactCanonicalScopePattern(pattern, registry, available);
1105
+ if (exactCanonical) {
1106
+ for (const model of exactCanonical.models) {
1107
+ addAllowed(model);
1108
+ }
1109
+ continue;
1110
+ }
1111
+
1112
+ const { model } = parseModelPatternWithContext(pattern, available, context, { modelRegistry: registry });
1113
+ if (model) {
1114
+ addAllowed(model);
1115
+ }
1116
+ }
1117
+
1118
+ return allowed.size === 0 ? [] : available.filter(model => allowed.has(`${model.provider}/${model.id}`));
1119
+ }
1120
+
1075
1121
  export interface ResolveCliModelResult {
1076
1122
  model: Model<Api> | undefined;
1077
1123
  selector?: string;
@@ -1127,14 +1173,11 @@ export function resolveCliModel(options: {
1127
1173
  // provider+id match over flat id match. Without this, a model with id
1128
1174
  // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
1129
1175
  // with id "glm-5", because Array.find returns the first catalog hit.
1130
- const slashIdx = lower.indexOf("/");
1131
- let exact: (typeof availableModels)[number] | undefined;
1132
- if (slashIdx !== -1) {
1133
- const prefix = lower.substring(0, slashIdx);
1134
- const suffix = trimmedModel.substring(slashIdx + 1);
1135
- exact = resolveProviderModelReference(prefix, suffix, availableModels);
1136
- }
1176
+ let exact = findExactModelReferenceMatch(trimmedModel, availableModels);
1137
1177
  if (!exact && !trimmedModel.includes(":")) {
1178
+ // CLI flags address the full catalog, so unlike the engine's canonical
1179
+ // step this lookup is unrestricted; the `:`-guard defers suffixed
1180
+ // selectors (thinking levels, ollama-style ids) to the grammar below.
1138
1181
  const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
1139
1182
  if (canonicalMatch) {
1140
1183
  return {
@@ -1147,6 +1190,8 @@ export function resolveCliModel(options: {
1147
1190
  }
1148
1191
  }
1149
1192
  if (!exact) {
1193
+ // Flat exact id (or full selector) by catalog order: CLI resolution
1194
+ // stays deterministic across runs regardless of usage-based ranking.
1150
1195
  exact = availableModels.find(
1151
1196
  model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
1152
1197
  );
@@ -1213,11 +1258,7 @@ export function resolveCliModel(options: {
1213
1258
 
1214
1259
  let selector = provider ? formatModelString(model) : undefined;
1215
1260
  if (!provider) {
1216
- const lastColonIndex = pattern.lastIndexOf(":");
1217
- const canonicalCandidate =
1218
- lastColonIndex !== -1 && parseThinkingLevel(pattern.substring(lastColonIndex + 1))
1219
- ? pattern.substring(0, lastColonIndex)
1220
- : pattern;
1261
+ const canonicalCandidate = splitThinkingSuffix(pattern).base;
1221
1262
  if (!canonicalCandidate.includes("/")) {
1222
1263
  const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
1223
1264
  if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
@@ -1316,18 +1357,9 @@ export async function findInitialModel(options: {
1316
1357
  // 4. Try first available model with valid API key
1317
1358
  const availableModels = modelRegistry.getAvailable();
1318
1359
 
1319
- if (availableModels.length > 0) {
1320
- // Try to find a default model from known providers
1321
- for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
1322
- const defaultId = defaultModelPerProvider[provider];
1323
- const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
1324
- if (match) {
1325
- return { model: match, thinkingLevel: undefined, fallbackMessage: undefined };
1326
- }
1327
- }
1328
-
1329
- // If no default found, use first available
1330
- return { model: availableModels[0], thinkingLevel: undefined, fallbackMessage: undefined };
1360
+ const fallback = pickDefaultAvailableModel(availableModels);
1361
+ if (fallback) {
1362
+ return { model: fallback, thinkingLevel: undefined, fallbackMessage: undefined };
1331
1363
  }
1332
1364
 
1333
1365
  // 5. No model found
@@ -1377,23 +1409,8 @@ export async function restoreModelFromSession(
1377
1409
  // Try to find any available model
1378
1410
  const availableModels = modelRegistry.getAvailable();
1379
1411
 
1380
- if (availableModels.length > 0) {
1381
- // Try to find a default model from known providers
1382
- let fallbackModel: Model<Api> | undefined;
1383
- for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
1384
- const defaultId = defaultModelPerProvider[provider];
1385
- const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
1386
- if (match) {
1387
- fallbackModel = match;
1388
- break;
1389
- }
1390
- }
1391
-
1392
- // If no default found, use first available
1393
- if (!fallbackModel) {
1394
- fallbackModel = availableModels[0];
1395
- }
1396
-
1412
+ const fallbackModel = pickDefaultAvailableModel(availableModels);
1413
+ if (fallbackModel) {
1397
1414
  if (shouldPrintMessages) {
1398
1415
  console.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));
1399
1416
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Built-in model roles and role metadata helpers.
3
+ */
4
+
5
+ import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
6
+ import type { Settings } from "./settings";
7
+
8
+ export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
9
+
10
+ export interface ModelRoleInfo {
11
+ tag?: string;
12
+ name: string;
13
+ color?: ThemeColor;
14
+ }
15
+
16
+ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
17
+ default: { tag: "DEFAULT", name: "Default", color: "success" },
18
+ smol: { tag: "SMOL", name: "Fast", color: "warning" },
19
+ slow: { tag: "SLOW", name: "Thinking", color: "accent" },
20
+ vision: { tag: "VISION", name: "Vision", color: "error" },
21
+ plan: { tag: "PLAN", name: "Architect", color: "muted" },
22
+ designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
23
+ commit: { tag: "COMMIT", name: "Commit", color: "dim" },
24
+ task: { tag: "TASK", name: "Subtask", color: "muted" },
25
+ };
26
+
27
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
28
+
29
+ /** Alias for ModelRoleInfo - used for both built-in and custom roles */
30
+ export type RoleInfo = ModelRoleInfo;
31
+
32
+ /**
33
+ * Return the canonical set of known roles for selector/carousel UI.
34
+ *
35
+ * Built-ins always come first. Configured cycle order, model assignments, and
36
+ * tag metadata can introduce additional custom roles without requiring duplicate
37
+ * entries across settings.
38
+ */
39
+ export function getKnownRoleIds(settings: Settings): string[] {
40
+ const roles = [...MODEL_ROLE_IDS] as string[];
41
+ const seen = new Set<string>(roles);
42
+ const addRole = (role: string) => {
43
+ if (seen.has(role)) return;
44
+ seen.add(role);
45
+ roles.push(role);
46
+ };
47
+
48
+ for (const role of settings.get("cycleOrder")) addRole(role);
49
+ for (const role in settings.getModelRoles()) addRole(role);
50
+ for (const role in settings.get("modelTags")) addRole(role);
51
+
52
+ return roles;
53
+ }
54
+
55
+ /**
56
+ * Get role info for a role name (built-in or custom).
57
+ * Configured metadata overrides built-in defaults when present.
58
+ */
59
+ export function getRoleInfo(role: string, settings: Settings): RoleInfo {
60
+ const builtIn = role in MODEL_ROLES ? MODEL_ROLES[role as ModelRole] : undefined;
61
+ const configured = settings.get("modelTags")[role];
62
+
63
+ if (configured) {
64
+ return {
65
+ tag: builtIn?.tag,
66
+ name: configured.name || builtIn?.name || role,
67
+ color: configured.color && isValidThemeColor(configured.color) ? configured.color : builtIn?.color,
68
+ };
69
+ }
70
+
71
+ if (builtIn) return builtIn;
72
+
73
+ return { name: role, color: "muted" };
74
+ }
@@ -18,7 +18,7 @@ const ReasoningEffortMapSchema = z.object({
18
18
  xhigh: z.string().optional(),
19
19
  });
20
20
 
21
- export const OpenAICompatSchema = z.object({
21
+ const OpenAICompatFieldsSchema = z.object({
22
22
  supportsStore: z.boolean().optional(),
23
23
  supportsDeveloperRole: z.boolean().optional(),
24
24
  supportsMultipleSystemMessages: z.boolean().optional(),
@@ -44,6 +44,18 @@ export const OpenAICompatSchema = z.object({
44
44
  cacheControlFormat: z.enum(["anthropic"]).optional(),
45
45
  supportsStrictMode: z.boolean().optional(),
46
46
  toolStrictMode: z.enum(["all_strict", "none"]).optional(),
47
+ streamIdleTimeoutMs: z.number().positive().optional(),
48
+ supportsLongPromptCacheRetention: z.boolean().optional(),
49
+ supportsReasoningParams: z.boolean().optional(),
50
+ alwaysSendMaxTokens: z.boolean().optional(),
51
+ strictResponsesPairing: z.boolean().optional(),
52
+ // anthropic-messages compat flags (same `compat` slot, per-api interpretation)
53
+ requiresToolResultId: z.boolean().optional(),
54
+ replayUnsignedThinking: z.boolean().optional(),
55
+ });
56
+
57
+ export const OpenAICompatSchema = OpenAICompatFieldsSchema.extend({
58
+ whenThinking: OpenAICompatFieldsSchema.optional(),
47
59
  });
48
60
 
49
61
  const EffortSchema = z.enum(["minimal", "low", "medium", "high", "xhigh"]);
@@ -56,13 +68,50 @@ const ThinkingControlModeSchema = z.enum([
56
68
  "anthropic-budget-effort",
57
69
  ]);
58
70
 
59
- const ModelThinkingSchema = z.object({
60
- minLevel: EffortSchema,
61
- maxLevel: EffortSchema,
62
- mode: ThinkingControlModeSchema,
63
- defaultLevel: EffortSchema.optional(),
64
- levels: z.array(EffortSchema).optional(),
65
- });
71
+ const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh"] as const;
72
+
73
+ /**
74
+ * Accepts the canonical `efforts` vocabulary plus the legacy
75
+ * `minLevel`/`maxLevel`/`levels` range shape, normalizing both to
76
+ * `ThinkingConfig` (ordered `efforts`, never empty). Precedence mirrors the
77
+ * old runtime: explicit `levels` beat the min..max range; `efforts` beats both.
78
+ */
79
+ const ModelThinkingSchema = z
80
+ .object({
81
+ mode: ThinkingControlModeSchema,
82
+ efforts: z.array(EffortSchema).min(1).optional(),
83
+ defaultLevel: EffortSchema.optional(),
84
+ effortMap: ReasoningEffortMapSchema.optional(),
85
+ supportsDisplay: z.boolean().optional(),
86
+ // Legacy range vocabulary (pre-efforts configs).
87
+ minLevel: EffortSchema.optional(),
88
+ maxLevel: EffortSchema.optional(),
89
+ levels: z.array(EffortSchema).min(1).optional(),
90
+ })
91
+ .refine(
92
+ value =>
93
+ value.efforts !== undefined ||
94
+ value.levels !== undefined ||
95
+ (value.minLevel !== undefined && value.maxLevel !== undefined),
96
+ {
97
+ message: "thinking requires `efforts` (or legacy `levels`/`minLevel`+`maxLevel`)",
98
+ },
99
+ )
100
+ .transform(({ efforts, levels, minLevel, maxLevel, mode, defaultLevel, effortMap, supportsDisplay }) => {
101
+ let resolved = efforts ?? levels;
102
+ if (!resolved) {
103
+ const minIndex = EFFORT_ORDER.indexOf(minLevel!);
104
+ const maxIndex = EFFORT_ORDER.indexOf(maxLevel!);
105
+ resolved = EFFORT_ORDER.slice(minIndex, Math.max(minIndex, maxIndex) + 1);
106
+ }
107
+ return {
108
+ mode,
109
+ efforts: resolved,
110
+ ...(defaultLevel !== undefined && { defaultLevel }),
111
+ ...(effortMap !== undefined && { effortMap }),
112
+ ...(supportsDisplay !== undefined && { supportsDisplay }),
113
+ };
114
+ });
66
115
 
67
116
  const ModelDefinitionSchema = z.object({
68
117
  id: z.string().min(1),