@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11

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 (352) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/dist/cli.js +23087 -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 +1 -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/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +20 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -16
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/debug/terminal-info.d.ts +0 -1
  25. package/dist/types/eval/backend.d.ts +0 -2
  26. package/dist/types/eval/idle-timeout.d.ts +0 -4
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  28. package/dist/types/export/html/template.generated.d.ts +1 -1
  29. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  30. package/dist/types/hindsight/mental-models.d.ts +17 -8
  31. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  32. package/dist/types/internal-urls/types.d.ts +1 -1
  33. package/dist/types/lsp/edits.d.ts +9 -0
  34. package/dist/types/lsp/index.d.ts +2 -2
  35. package/dist/types/lsp/types.d.ts +2 -0
  36. package/dist/types/lsp/utils.d.ts +3 -0
  37. package/dist/types/mcp/json-rpc.d.ts +5 -0
  38. package/dist/types/mnemopi/state.d.ts +11 -1
  39. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  40. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  41. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  42. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  43. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  46. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  47. package/dist/types/modes/components/footer.d.ts +1 -1
  48. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  49. package/dist/types/modes/components/hook-input.d.ts +4 -0
  50. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  51. package/dist/types/modes/components/model-selector.d.ts +1 -1
  52. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  54. package/dist/types/modes/components/session-selector.d.ts +1 -1
  55. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  56. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  57. package/dist/types/modes/components/transcript-container.d.ts +31 -26
  58. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  60. package/dist/types/modes/components/user-message.d.ts +2 -1
  61. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  62. package/dist/types/modes/components/welcome.d.ts +19 -3
  63. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  64. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  65. package/dist/types/modes/interactive-mode.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  70. package/dist/types/modes/types.d.ts +2 -1
  71. package/dist/types/session/agent-session.d.ts +1 -1
  72. package/dist/types/session/auth-broker-config.d.ts +4 -0
  73. package/dist/types/session/session-manager.d.ts +1 -1
  74. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  75. package/dist/types/ssh/connection-manager.d.ts +8 -0
  76. package/dist/types/task/discovery.d.ts +1 -2
  77. package/dist/types/task/parallel.d.ts +2 -2
  78. package/dist/types/task/worktree.d.ts +2 -0
  79. package/dist/types/tiny/title-client.d.ts +1 -1
  80. package/dist/types/tools/ask.d.ts +4 -0
  81. package/dist/types/tools/conflict-detect.d.ts +16 -0
  82. package/dist/types/tools/github-cache.d.ts +7 -0
  83. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  84. package/dist/types/tools/todo.d.ts +2 -0
  85. package/dist/types/tui/output-block.d.ts +3 -3
  86. package/dist/types/utils/changelog.d.ts +8 -0
  87. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  88. package/dist/types/web/scrapers/types.d.ts +12 -0
  89. package/dist/types/web/search/providers/codex.d.ts +1 -1
  90. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  91. package/examples/extensions/tools.ts +5 -4
  92. package/package.json +14 -11
  93. package/scripts/build-binary.ts +18 -23
  94. package/scripts/bundle-dist.ts +81 -0
  95. package/scripts/{dev-launch → omp} +1 -1
  96. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  97. package/src/async/job-manager.ts +57 -3
  98. package/src/autoresearch/dashboard.ts +1 -1
  99. package/src/autoresearch/prompt-setup.md +6 -6
  100. package/src/autoresearch/prompt.md +6 -6
  101. package/src/capability/fs.ts +10 -0
  102. package/src/cli/args.ts +1 -1
  103. package/src/cli/auth-gateway-cli.ts +1 -3
  104. package/src/cli/dry-balance-cli.ts +1 -1
  105. package/src/cli/gallery-cli.ts +1 -1
  106. package/src/cli/gallery-fixtures/fs.ts +1 -1
  107. package/src/cli/gallery-fixtures/types.ts +5 -1
  108. package/src/cli/list-models.ts +7 -12
  109. package/src/cli/usage-cli.ts +603 -0
  110. package/src/cli-commands.ts +1 -0
  111. package/src/cli.ts +69 -5
  112. package/src/commands/complete.ts +1 -1
  113. package/src/commands/launch.ts +1 -1
  114. package/src/commands/read.ts +6 -3
  115. package/src/commands/usage.ts +35 -0
  116. package/src/commit/agentic/agent.ts +1 -1
  117. package/src/commit/model-selection.ts +1 -1
  118. package/src/config/append-only-context-mode.ts +6 -12
  119. package/src/config/model-discovery.ts +554 -0
  120. package/src/config/model-registry.ts +308 -1025
  121. package/src/config/model-resolver.ts +113 -156
  122. package/src/config/model-roles.ts +74 -0
  123. package/src/config/models-config-schema.ts +57 -8
  124. package/src/config/models-config.ts +129 -0
  125. package/src/config/settings-schema.ts +18 -14
  126. package/src/config/settings.ts +37 -1
  127. package/src/dap/client.ts +124 -37
  128. package/src/dap/session.ts +259 -158
  129. package/src/debug/log-viewer.ts +1 -1
  130. package/src/debug/raw-sse.ts +1 -1
  131. package/src/debug/terminal-info.ts +0 -3
  132. package/src/edit/diff.ts +95 -18
  133. package/src/edit/hashline/block-resolver.ts +20 -1
  134. package/src/edit/hashline/diff.ts +36 -1
  135. package/src/edit/hashline/execute.ts +8 -2
  136. package/src/edit/index.ts +16 -1
  137. package/src/edit/modes/patch.ts +52 -0
  138. package/src/edit/modes/replace.ts +56 -22
  139. package/src/edit/notebook.ts +22 -2
  140. package/src/edit/renderer.ts +36 -10
  141. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  142. package/src/eval/backend.ts +0 -2
  143. package/src/eval/completion-bridge.ts +2 -1
  144. package/src/eval/idle-timeout.ts +2 -9
  145. package/src/eval/js/context-manager.ts +6 -8
  146. package/src/eval/js/executor.ts +6 -2
  147. package/src/eval/js/index.ts +0 -2
  148. package/src/eval/js/shared/helpers.ts +5 -6
  149. package/src/eval/js/shared/local-module-loader.ts +1 -1
  150. package/src/eval/js/shared/prelude.txt +62 -1
  151. package/src/eval/js/shared/rewrite-imports.ts +49 -23
  152. package/src/eval/js/shared/runtime.ts +1 -1
  153. package/src/eval/py/index.ts +0 -2
  154. package/src/eval/py/kernel.ts +19 -0
  155. package/src/eval/py/runner.py +107 -3
  156. package/src/exec/bash-executor.ts +3 -1
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/export/html/template.js +3 -1
  159. package/src/extensibility/extensions/types.ts +3 -2
  160. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  161. package/src/hindsight/mental-models.ts +59 -12
  162. package/src/hindsight/state.ts +6 -1
  163. package/src/internal-urls/artifact-protocol.ts +11 -2
  164. package/src/internal-urls/docs-index.generated.ts +10 -10
  165. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  166. package/src/internal-urls/router.ts +1 -1
  167. package/src/internal-urls/types.ts +1 -1
  168. package/src/lib/xai-http.ts +1 -1
  169. package/src/lsp/client.ts +118 -38
  170. package/src/lsp/clients/biome-client.ts +101 -39
  171. package/src/lsp/edits.ts +143 -95
  172. package/src/lsp/index.ts +31 -22
  173. package/src/lsp/render.ts +1 -1
  174. package/src/lsp/types.ts +2 -0
  175. package/src/lsp/utils.ts +28 -10
  176. package/src/main.ts +165 -17
  177. package/src/mcp/json-rpc.ts +35 -5
  178. package/src/mcp/transports/stdio.ts +7 -1
  179. package/src/memories/index.ts +2 -1
  180. package/src/mnemopi/backend.ts +25 -3
  181. package/src/mnemopi/state.ts +38 -2
  182. package/src/modes/components/agent-dashboard.ts +10 -7
  183. package/src/modes/components/assistant-message.ts +19 -13
  184. package/src/modes/components/bash-execution.ts +1 -1
  185. package/src/modes/components/copy-selector.ts +1 -1
  186. package/src/modes/components/diff.ts +13 -2
  187. package/src/modes/components/dynamic-border.ts +12 -3
  188. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  189. package/src/modes/components/extensions/extension-list.ts +1 -1
  190. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  191. package/src/modes/components/footer.ts +1 -1
  192. package/src/modes/components/history-search.ts +1 -1
  193. package/src/modes/components/hook-editor.ts +8 -0
  194. package/src/modes/components/hook-input.ts +8 -0
  195. package/src/modes/components/hook-selector.ts +2 -2
  196. package/src/modes/components/model-selector.ts +66 -54
  197. package/src/modes/components/plan-review-overlay.ts +1 -1
  198. package/src/modes/components/session-observer-overlay.ts +2 -2
  199. package/src/modes/components/session-selector.ts +1 -1
  200. package/src/modes/components/settings-selector.ts +5 -1
  201. package/src/modes/components/status-line/component.ts +1 -1
  202. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  203. package/src/modes/components/transcript-container.ts +373 -141
  204. package/src/modes/components/tree-selector.ts +3 -3
  205. package/src/modes/components/user-message-selector.ts +1 -1
  206. package/src/modes/components/user-message.ts +17 -5
  207. package/src/modes/components/visual-truncate.ts +1 -1
  208. package/src/modes/components/welcome.ts +108 -26
  209. package/src/modes/controllers/command-controller.ts +10 -3
  210. package/src/modes/controllers/event-controller.ts +73 -49
  211. package/src/modes/controllers/input-controller.ts +5 -5
  212. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  213. package/src/modes/controllers/selector-controller.ts +1 -5
  214. package/src/modes/controllers/streaming-reveal.ts +85 -18
  215. package/src/modes/interactive-mode.ts +5 -19
  216. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  217. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  218. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  219. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  220. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  221. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  222. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  223. package/src/modes/types.ts +2 -1
  224. package/src/prompts/agents/explore.md +2 -2
  225. package/src/prompts/agents/librarian.md +1 -2
  226. package/src/prompts/agents/oracle.md +1 -1
  227. package/src/prompts/agents/plan.md +5 -5
  228. package/src/prompts/agents/task.md +5 -5
  229. package/src/prompts/ci-green-request.md +5 -7
  230. package/src/prompts/goals/goal-budget-limit.md +2 -2
  231. package/src/prompts/goals/goal-continuation.md +4 -4
  232. package/src/prompts/goals/goal-mode-active.md +1 -1
  233. package/src/prompts/memories/read-path.md +1 -1
  234. package/src/prompts/memories/stage_one_system.md +2 -2
  235. package/src/prompts/review-custom-request.md +1 -1
  236. package/src/prompts/system/agent-creation-architect.md +2 -2
  237. package/src/prompts/system/auto-continue.md +1 -1
  238. package/src/prompts/system/background-tan-dispatch.md +1 -1
  239. package/src/prompts/system/btw-user.md +2 -2
  240. package/src/prompts/system/commit-message-system.md +13 -1
  241. package/src/prompts/system/custom-system-prompt.md +1 -1
  242. package/src/prompts/system/eager-todo.md +2 -2
  243. package/src/prompts/system/irc-incoming.md +1 -1
  244. package/src/prompts/system/manual-continue.md +1 -1
  245. package/src/prompts/system/omfg-user.md +3 -4
  246. package/src/prompts/system/orchestrate-notice.md +9 -9
  247. package/src/prompts/system/plan-mode-active.md +4 -4
  248. package/src/prompts/system/plan-mode-subagent.md +4 -5
  249. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  250. package/src/prompts/system/project-prompt.md +2 -2
  251. package/src/prompts/system/subagent-system-prompt.md +4 -4
  252. package/src/prompts/system/system-prompt.md +15 -26
  253. package/src/prompts/system/title-system.md +2 -2
  254. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  255. package/src/prompts/system/workflow-notice.md +1 -1
  256. package/src/prompts/tools/ast-edit.md +1 -1
  257. package/src/prompts/tools/ast-grep.md +2 -2
  258. package/src/prompts/tools/bash.md +8 -10
  259. package/src/prompts/tools/browser.md +7 -7
  260. package/src/prompts/tools/debug.md +1 -1
  261. package/src/prompts/tools/eval.md +3 -3
  262. package/src/prompts/tools/find.md +0 -1
  263. package/src/prompts/tools/github.md +8 -7
  264. package/src/prompts/tools/goal.md +1 -1
  265. package/src/prompts/tools/image-gen.md +1 -1
  266. package/src/prompts/tools/inspect-image-system.md +1 -1
  267. package/src/prompts/tools/irc.md +15 -15
  268. package/src/prompts/tools/lsp.md +2 -2
  269. package/src/prompts/tools/patch.md +2 -2
  270. package/src/prompts/tools/read.md +3 -4
  271. package/src/prompts/tools/recall.md +1 -1
  272. package/src/prompts/tools/reflect.md +1 -1
  273. package/src/prompts/tools/render-mermaid.md +2 -2
  274. package/src/prompts/tools/replace.md +4 -10
  275. package/src/prompts/tools/rewind.md +2 -2
  276. package/src/prompts/tools/search-tool-bm25.md +1 -9
  277. package/src/prompts/tools/search.md +0 -1
  278. package/src/prompts/tools/ssh.md +0 -4
  279. package/src/prompts/tools/task.md +2 -3
  280. package/src/prompts/tools/todo.md +6 -2
  281. package/src/sdk.ts +23 -10
  282. package/src/session/agent-session.ts +44 -10
  283. package/src/session/auth-broker-config.ts +30 -1
  284. package/src/session/session-manager.ts +2 -2
  285. package/src/session/streaming-output.ts +23 -2
  286. package/src/slash-commands/builtin-registry.ts +20 -0
  287. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  288. package/src/ssh/connection-manager.ts +27 -0
  289. package/src/task/commands.ts +2 -1
  290. package/src/task/discovery.ts +17 -24
  291. package/src/task/executor.ts +61 -53
  292. package/src/task/index.ts +137 -60
  293. package/src/task/parallel.ts +3 -3
  294. package/src/task/render.ts +2 -2
  295. package/src/task/worktree.ts +64 -56
  296. package/src/thinking.ts +2 -1
  297. package/src/tiny/title-client.ts +32 -14
  298. package/src/tools/archive-reader.ts +30 -2
  299. package/src/tools/ask.ts +104 -21
  300. package/src/tools/ast-edit.ts +25 -5
  301. package/src/tools/auto-generated-guard.ts +20 -3
  302. package/src/tools/bash-interactive.ts +27 -7
  303. package/src/tools/bash.ts +54 -13
  304. package/src/tools/browser/launch.ts +11 -2
  305. package/src/tools/browser/readable.ts +19 -2
  306. package/src/tools/browser/registry.ts +4 -1
  307. package/src/tools/browser/render.ts +2 -2
  308. package/src/tools/browser/tab-supervisor.ts +55 -16
  309. package/src/tools/conflict-detect.ts +50 -4
  310. package/src/tools/debug.ts +1 -1
  311. package/src/tools/eval-render.ts +5 -5
  312. package/src/tools/eval.ts +0 -2
  313. package/src/tools/fetch.ts +33 -10
  314. package/src/tools/gh-cache-invalidation.ts +63 -8
  315. package/src/tools/gh-renderer.ts +1 -1
  316. package/src/tools/gh.ts +172 -29
  317. package/src/tools/github-cache.ts +70 -6
  318. package/src/tools/image-gen.ts +3 -9
  319. package/src/tools/irc.ts +5 -1
  320. package/src/tools/job.ts +1 -1
  321. package/src/tools/read.ts +202 -61
  322. package/src/tools/render-utils.ts +3 -3
  323. package/src/tools/resolve.ts +1 -1
  324. package/src/tools/search.ts +92 -29
  325. package/src/tools/sqlite-reader.ts +17 -5
  326. package/src/tools/ssh.ts +8 -8
  327. package/src/tools/todo.ts +51 -12
  328. package/src/tools/write.ts +118 -18
  329. package/src/tui/output-block.ts +4 -4
  330. package/src/utils/changelog.ts +27 -1
  331. package/src/utils/file-mentions.ts +2 -1
  332. package/src/web/scrapers/arxiv.ts +1 -1
  333. package/src/web/scrapers/go-pkg.ts +1 -1
  334. package/src/web/scrapers/iacr.ts +1 -1
  335. package/src/web/scrapers/readthedocs.ts +1 -1
  336. package/src/web/scrapers/twitter.ts +2 -1
  337. package/src/web/scrapers/types.ts +87 -8
  338. package/src/web/scrapers/wikipedia.ts +1 -1
  339. package/src/web/scrapers/youtube.ts +6 -1
  340. package/src/web/search/index.ts +1 -1
  341. package/src/web/search/providers/anthropic.ts +8 -2
  342. package/src/web/search/providers/codex.ts +2 -1
  343. package/src/web/search/providers/gemini.ts +2 -3
  344. package/src/web/search/render.ts +8 -6
  345. package/dist/types/config/model-equivalence.d.ts +0 -24
  346. package/dist/types/config/model-id-affixes.d.ts +0 -12
  347. package/dist/types/config/model-provider-priority.d.ts +0 -1
  348. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  349. package/src/config/model-equivalence.ts +0 -875
  350. package/src/config/model-id-affixes.ts +0 -81
  351. package/src/config/model-provider-priority.ts +0 -56
  352. 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;
@@ -1127,14 +1113,11 @@ export function resolveCliModel(options: {
1127
1113
  // provider+id match over flat id match. Without this, a model with id
1128
1114
  // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
1129
1115
  // 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
- }
1116
+ let exact = findExactModelReferenceMatch(trimmedModel, availableModels);
1137
1117
  if (!exact && !trimmedModel.includes(":")) {
1118
+ // CLI flags address the full catalog, so unlike the engine's canonical
1119
+ // step this lookup is unrestricted; the `:`-guard defers suffixed
1120
+ // selectors (thinking levels, ollama-style ids) to the grammar below.
1138
1121
  const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
1139
1122
  if (canonicalMatch) {
1140
1123
  return {
@@ -1147,6 +1130,8 @@ export function resolveCliModel(options: {
1147
1130
  }
1148
1131
  }
1149
1132
  if (!exact) {
1133
+ // Flat exact id (or full selector) by catalog order: CLI resolution
1134
+ // stays deterministic across runs regardless of usage-based ranking.
1150
1135
  exact = availableModels.find(
1151
1136
  model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
1152
1137
  );
@@ -1213,11 +1198,7 @@ export function resolveCliModel(options: {
1213
1198
 
1214
1199
  let selector = provider ? formatModelString(model) : undefined;
1215
1200
  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;
1201
+ const canonicalCandidate = splitThinkingSuffix(pattern).base;
1221
1202
  if (!canonicalCandidate.includes("/")) {
1222
1203
  const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
1223
1204
  if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
@@ -1316,18 +1297,9 @@ export async function findInitialModel(options: {
1316
1297
  // 4. Try first available model with valid API key
1317
1298
  const availableModels = modelRegistry.getAvailable();
1318
1299
 
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 };
1300
+ const fallback = pickDefaultAvailableModel(availableModels);
1301
+ if (fallback) {
1302
+ return { model: fallback, thinkingLevel: undefined, fallbackMessage: undefined };
1331
1303
  }
1332
1304
 
1333
1305
  // 5. No model found
@@ -1377,23 +1349,8 @@ export async function restoreModelFromSession(
1377
1349
  // Try to find any available model
1378
1350
  const availableModels = modelRegistry.getAvailable();
1379
1351
 
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
-
1352
+ const fallbackModel = pickDefaultAvailableModel(availableModels);
1353
+ if (fallbackModel) {
1397
1354
  if (shouldPrintMessages) {
1398
1355
  console.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));
1399
1356
  }
@@ -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),