@wahack/pi-coding-agent 15.11.0

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 (1152) hide show
  1. package/CHANGELOG.md +10031 -0
  2. package/README.md +36 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +104 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/extensions/README.md +142 -0
  7. package/examples/extensions/api-demo.ts +79 -0
  8. package/examples/extensions/chalk-logger.ts +25 -0
  9. package/examples/extensions/hello.ts +31 -0
  10. package/examples/extensions/pirate.ts +43 -0
  11. package/examples/extensions/plan-mode.ts +549 -0
  12. package/examples/extensions/reload-runtime.ts +38 -0
  13. package/examples/extensions/thinking-note.ts +13 -0
  14. package/examples/extensions/tools.ts +145 -0
  15. package/examples/extensions/with-deps/index.ts +36 -0
  16. package/examples/extensions/with-deps/package-lock.json +31 -0
  17. package/examples/extensions/with-deps/package.json +17 -0
  18. package/examples/hooks/README.md +56 -0
  19. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  20. package/examples/hooks/confirm-destructive.ts +58 -0
  21. package/examples/hooks/custom-compaction.ts +115 -0
  22. package/examples/hooks/dirty-repo-guard.ts +51 -0
  23. package/examples/hooks/file-trigger.ts +40 -0
  24. package/examples/hooks/git-checkpoint.ts +52 -0
  25. package/examples/hooks/handoff.ts +149 -0
  26. package/examples/hooks/permission-gate.ts +33 -0
  27. package/examples/hooks/protected-paths.ts +29 -0
  28. package/examples/hooks/qna.ts +118 -0
  29. package/examples/hooks/status-line.ts +39 -0
  30. package/examples/sdk/01-minimal.ts +21 -0
  31. package/examples/sdk/02-custom-model.ts +49 -0
  32. package/examples/sdk/03-custom-prompt.ts +46 -0
  33. package/examples/sdk/04-skills.ts +43 -0
  34. package/examples/sdk/06-extensions.ts +82 -0
  35. package/examples/sdk/06-hooks.ts +61 -0
  36. package/examples/sdk/07-context-files.ts +35 -0
  37. package/examples/sdk/08-prompt-templates.ts +41 -0
  38. package/examples/sdk/08-slash-commands.ts +46 -0
  39. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  40. package/examples/sdk/11-sessions.ts +47 -0
  41. package/examples/sdk/12-redis-sessions.ts +54 -0
  42. package/examples/sdk/13-sql-sessions.ts +61 -0
  43. package/examples/sdk/README.md +172 -0
  44. package/package.json +554 -0
  45. package/scripts/build-binary.ts +100 -0
  46. package/scripts/bundle-dist.ts +90 -0
  47. package/scripts/format-prompts.ts +68 -0
  48. package/scripts/generate-docs-index.ts +40 -0
  49. package/scripts/generate-template.ts +33 -0
  50. package/scripts/omp +42 -0
  51. package/scripts/omp.ts +19 -0
  52. package/src/async/index.ts +1 -0
  53. package/src/async/job-manager.ts +625 -0
  54. package/src/auto-thinking/classifier.ts +185 -0
  55. package/src/autoresearch/command-resume.md +14 -0
  56. package/src/autoresearch/dashboard.ts +436 -0
  57. package/src/autoresearch/git.ts +319 -0
  58. package/src/autoresearch/helpers.ts +218 -0
  59. package/src/autoresearch/index.ts +536 -0
  60. package/src/autoresearch/prompt-setup.md +43 -0
  61. package/src/autoresearch/prompt.md +103 -0
  62. package/src/autoresearch/resume-message.md +10 -0
  63. package/src/autoresearch/state.ts +273 -0
  64. package/src/autoresearch/storage.ts +699 -0
  65. package/src/autoresearch/tools/init-experiment.ts +272 -0
  66. package/src/autoresearch/tools/log-experiment.ts +524 -0
  67. package/src/autoresearch/tools/run-experiment.ts +407 -0
  68. package/src/autoresearch/tools/update-notes.ts +109 -0
  69. package/src/autoresearch/types.ts +168 -0
  70. package/src/bun-imports.d.ts +28 -0
  71. package/src/capability/context-file.ts +44 -0
  72. package/src/capability/extension-module.ts +34 -0
  73. package/src/capability/extension.ts +47 -0
  74. package/src/capability/fs.ts +117 -0
  75. package/src/capability/hook.ts +40 -0
  76. package/src/capability/index.ts +436 -0
  77. package/src/capability/instruction.ts +37 -0
  78. package/src/capability/mcp.ts +74 -0
  79. package/src/capability/prompt.ts +35 -0
  80. package/src/capability/rule-buckets.ts +66 -0
  81. package/src/capability/rule.ts +261 -0
  82. package/src/capability/settings.ts +34 -0
  83. package/src/capability/skill.ts +63 -0
  84. package/src/capability/slash-command.ts +40 -0
  85. package/src/capability/ssh.ts +41 -0
  86. package/src/capability/system-prompt.ts +34 -0
  87. package/src/capability/tool.ts +38 -0
  88. package/src/capability/types.ts +168 -0
  89. package/src/cli/agents-cli.ts +138 -0
  90. package/src/cli/args.ts +340 -0
  91. package/src/cli/auth-broker-cli.ts +895 -0
  92. package/src/cli/auth-gateway-cli.ts +611 -0
  93. package/src/cli/classify-install-target.ts +76 -0
  94. package/src/cli/claude-trace-cli.ts +795 -0
  95. package/src/cli/commands/init-xdg.ts +27 -0
  96. package/src/cli/completion-gen.ts +550 -0
  97. package/src/cli/config-cli.ts +418 -0
  98. package/src/cli/dry-balance-cli.ts +856 -0
  99. package/src/cli/extension-flags.ts +48 -0
  100. package/src/cli/file-processor.ts +133 -0
  101. package/src/cli/gallery-cli.ts +230 -0
  102. package/src/cli/gallery-fixtures/agentic.ts +407 -0
  103. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  104. package/src/cli/gallery-fixtures/edit.ts +194 -0
  105. package/src/cli/gallery-fixtures/fs.ts +220 -0
  106. package/src/cli/gallery-fixtures/index.ts +40 -0
  107. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  108. package/src/cli/gallery-fixtures/memory.ts +81 -0
  109. package/src/cli/gallery-fixtures/misc.ts +250 -0
  110. package/src/cli/gallery-fixtures/search.ts +213 -0
  111. package/src/cli/gallery-fixtures/shell.ts +167 -0
  112. package/src/cli/gallery-fixtures/types.ts +57 -0
  113. package/src/cli/gallery-fixtures/web.ts +158 -0
  114. package/src/cli/gallery-screenshot.ts +279 -0
  115. package/src/cli/grep-cli.ts +160 -0
  116. package/src/cli/grievances-cli.ts +256 -0
  117. package/src/cli/initial-message.ts +58 -0
  118. package/src/cli/list-models.ts +194 -0
  119. package/src/cli/plugin-cli.ts +996 -0
  120. package/src/cli/read-cli.ts +57 -0
  121. package/src/cli/session-picker.ts +79 -0
  122. package/src/cli/setup-cli.ts +231 -0
  123. package/src/cli/shell-cli.ts +176 -0
  124. package/src/cli/ssh-cli.ts +179 -0
  125. package/src/cli/startup-cwd.ts +68 -0
  126. package/src/cli/stats-cli.ts +238 -0
  127. package/src/cli/tiny-models-cli.ts +127 -0
  128. package/src/cli/update-cli.ts +611 -0
  129. package/src/cli/usage-cli.ts +603 -0
  130. package/src/cli/web-search-cli.ts +132 -0
  131. package/src/cli/worktree-cli.ts +291 -0
  132. package/src/cli-commands.ts +79 -0
  133. package/src/cli.ts +200 -0
  134. package/src/commands/acp.ts +24 -0
  135. package/src/commands/agents.ts +57 -0
  136. package/src/commands/auth-broker.ts +99 -0
  137. package/src/commands/auth-gateway.ts +69 -0
  138. package/src/commands/commit.ts +46 -0
  139. package/src/commands/complete.ts +66 -0
  140. package/src/commands/completions.ts +60 -0
  141. package/src/commands/config.ts +51 -0
  142. package/src/commands/dry-balance.ts +43 -0
  143. package/src/commands/gallery.ts +52 -0
  144. package/src/commands/grep.ts +48 -0
  145. package/src/commands/grievances.ts +51 -0
  146. package/src/commands/install.ts +107 -0
  147. package/src/commands/launch.ts +169 -0
  148. package/src/commands/plugin.ts +78 -0
  149. package/src/commands/read.ts +38 -0
  150. package/src/commands/setup.ts +67 -0
  151. package/src/commands/shell.ts +29 -0
  152. package/src/commands/ssh.ts +60 -0
  153. package/src/commands/stats.ts +29 -0
  154. package/src/commands/tiny-models.ts +36 -0
  155. package/src/commands/update.ts +21 -0
  156. package/src/commands/usage.ts +35 -0
  157. package/src/commands/web-search.ts +42 -0
  158. package/src/commands/worktree.ts +56 -0
  159. package/src/commit/agentic/agent.ts +317 -0
  160. package/src/commit/agentic/fallback.ts +96 -0
  161. package/src/commit/agentic/index.ts +355 -0
  162. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  163. package/src/commit/agentic/prompts/session-user.md +25 -0
  164. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  165. package/src/commit/agentic/prompts/system.md +38 -0
  166. package/src/commit/agentic/state.ts +60 -0
  167. package/src/commit/agentic/tools/analyze-file.ts +146 -0
  168. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  169. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  170. package/src/commit/agentic/tools/git-overview.ts +81 -0
  171. package/src/commit/agentic/tools/index.ts +54 -0
  172. package/src/commit/agentic/tools/propose-changelog.ts +144 -0
  173. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  174. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  175. package/src/commit/agentic/tools/schemas.ts +23 -0
  176. package/src/commit/agentic/tools/split-commit.ts +245 -0
  177. package/src/commit/agentic/topo-sort.ts +44 -0
  178. package/src/commit/agentic/trivial.ts +51 -0
  179. package/src/commit/agentic/validation.ts +183 -0
  180. package/src/commit/analysis/conventional.ts +64 -0
  181. package/src/commit/analysis/index.ts +4 -0
  182. package/src/commit/analysis/scope.ts +242 -0
  183. package/src/commit/analysis/summary.ts +105 -0
  184. package/src/commit/analysis/validation.ts +66 -0
  185. package/src/commit/changelog/detect.ts +40 -0
  186. package/src/commit/changelog/generate.ts +97 -0
  187. package/src/commit/changelog/index.ts +234 -0
  188. package/src/commit/changelog/parse.ts +44 -0
  189. package/src/commit/cli.ts +85 -0
  190. package/src/commit/git/diff.ts +148 -0
  191. package/src/commit/index.ts +5 -0
  192. package/src/commit/map-reduce/index.ts +69 -0
  193. package/src/commit/map-reduce/map-phase.ts +193 -0
  194. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  195. package/src/commit/map-reduce/utils.ts +9 -0
  196. package/src/commit/message.ts +11 -0
  197. package/src/commit/model-selection.ts +92 -0
  198. package/src/commit/pipeline.ts +243 -0
  199. package/src/commit/prompts/analysis-system.md +148 -0
  200. package/src/commit/prompts/analysis-user.md +38 -0
  201. package/src/commit/prompts/changelog-system.md +50 -0
  202. package/src/commit/prompts/changelog-user.md +18 -0
  203. package/src/commit/prompts/file-observer-system.md +24 -0
  204. package/src/commit/prompts/file-observer-user.md +8 -0
  205. package/src/commit/prompts/reduce-system.md +50 -0
  206. package/src/commit/prompts/reduce-user.md +17 -0
  207. package/src/commit/prompts/summary-retry.md +3 -0
  208. package/src/commit/prompts/summary-system.md +38 -0
  209. package/src/commit/prompts/summary-user.md +13 -0
  210. package/src/commit/prompts/types-description.md +2 -0
  211. package/src/commit/shared-llm.ts +77 -0
  212. package/src/commit/types.ts +118 -0
  213. package/src/commit/utils/exclusions.ts +42 -0
  214. package/src/commit/utils.ts +58 -0
  215. package/src/config/api-key-resolver.ts +60 -0
  216. package/src/config/append-only-context-mode.ts +31 -0
  217. package/src/config/config-file.ts +317 -0
  218. package/src/config/file-lock.ts +164 -0
  219. package/src/config/keybindings.ts +628 -0
  220. package/src/config/mcp-schema.json +230 -0
  221. package/src/config/model-discovery.ts +554 -0
  222. package/src/config/model-registry.ts +2090 -0
  223. package/src/config/model-resolver.ts +1502 -0
  224. package/src/config/model-roles.ts +74 -0
  225. package/src/config/models-config-schema.ts +226 -0
  226. package/src/config/models-config.ts +129 -0
  227. package/src/config/prompt-templates.ts +185 -0
  228. package/src/config/resolve-config-value.ts +94 -0
  229. package/src/config/settings-schema.ts +3530 -0
  230. package/src/config/settings.ts +1178 -0
  231. package/src/config.ts +242 -0
  232. package/src/cursor.ts +340 -0
  233. package/src/dap/client.ts +760 -0
  234. package/src/dap/config.ts +189 -0
  235. package/src/dap/defaults.json +212 -0
  236. package/src/dap/index.ts +4 -0
  237. package/src/dap/session.ts +1441 -0
  238. package/src/dap/types.ts +610 -0
  239. package/src/debug/index.ts +515 -0
  240. package/src/debug/log-formatting.ts +58 -0
  241. package/src/debug/log-viewer.ts +908 -0
  242. package/src/debug/profiler.ts +162 -0
  243. package/src/debug/protocol-probe.ts +267 -0
  244. package/src/debug/raw-sse-buffer.ts +273 -0
  245. package/src/debug/raw-sse.ts +292 -0
  246. package/src/debug/report-bundle.ts +374 -0
  247. package/src/debug/system-info.ts +111 -0
  248. package/src/debug/terminal-info.ts +124 -0
  249. package/src/discovery/agents-md.ts +67 -0
  250. package/src/discovery/agents.ts +230 -0
  251. package/src/discovery/at-imports.ts +273 -0
  252. package/src/discovery/builtin-defaults.ts +39 -0
  253. package/src/discovery/builtin-rules/index.ts +54 -0
  254. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  255. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  256. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  257. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  258. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  259. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  260. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  261. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  262. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  263. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
  264. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  265. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  266. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  267. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  268. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  269. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  270. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  271. package/src/discovery/builtin.ts +906 -0
  272. package/src/discovery/claude-plugins.ts +386 -0
  273. package/src/discovery/claude.ts +584 -0
  274. package/src/discovery/cline.ts +83 -0
  275. package/src/discovery/codex.ts +522 -0
  276. package/src/discovery/cursor.ts +220 -0
  277. package/src/discovery/gemini.ts +383 -0
  278. package/src/discovery/github.ts +154 -0
  279. package/src/discovery/helpers.ts +1016 -0
  280. package/src/discovery/index.ts +81 -0
  281. package/src/discovery/mcp-json.ts +171 -0
  282. package/src/discovery/omp-extension-roots.ts +190 -0
  283. package/src/discovery/omp-plugins.ts +383 -0
  284. package/src/discovery/opencode.ts +398 -0
  285. package/src/discovery/plugin-dir-roots.ts +28 -0
  286. package/src/discovery/ssh.ts +153 -0
  287. package/src/discovery/substitute-plugin-root.ts +29 -0
  288. package/src/discovery/vscode.ts +105 -0
  289. package/src/discovery/windsurf.ts +147 -0
  290. package/src/edit/apply-patch/index.ts +87 -0
  291. package/src/edit/apply-patch/parser.ts +174 -0
  292. package/src/edit/diff.ts +999 -0
  293. package/src/edit/file-snapshot-store.ts +91 -0
  294. package/src/edit/hashline/block-resolver.ts +33 -0
  295. package/src/edit/hashline/diff.ts +290 -0
  296. package/src/edit/hashline/execute.ts +242 -0
  297. package/src/edit/hashline/filesystem.ts +130 -0
  298. package/src/edit/hashline/index.ts +5 -0
  299. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  300. package/src/edit/hashline/params.ts +18 -0
  301. package/src/edit/index.ts +571 -0
  302. package/src/edit/modes/apply-patch.lark +19 -0
  303. package/src/edit/modes/apply-patch.ts +53 -0
  304. package/src/edit/modes/patch.ts +1891 -0
  305. package/src/edit/modes/replace.ts +1137 -0
  306. package/src/edit/normalize.ts +345 -0
  307. package/src/edit/notebook.ts +242 -0
  308. package/src/edit/read-file.ts +25 -0
  309. package/src/edit/renderer.ts +769 -0
  310. package/src/edit/streaming.ts +517 -0
  311. package/src/eval/__tests__/agent-bridge.test.ts +708 -0
  312. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  313. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  314. package/src/eval/__tests__/completion-bridge.test.ts +412 -0
  315. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  316. package/src/eval/__tests__/idle-timeout.test.ts +80 -0
  317. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  318. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  319. package/src/eval/agent-bridge.ts +319 -0
  320. package/src/eval/backend.ts +71 -0
  321. package/src/eval/bridge-timeout.ts +44 -0
  322. package/src/eval/budget-bridge.ts +48 -0
  323. package/src/eval/completion-bridge.ts +207 -0
  324. package/src/eval/concurrency-bridge.ts +34 -0
  325. package/src/eval/idle-timeout.ts +91 -0
  326. package/src/eval/index.ts +4 -0
  327. package/src/eval/js/context-manager.ts +502 -0
  328. package/src/eval/js/executor.ts +173 -0
  329. package/src/eval/js/index.ts +51 -0
  330. package/src/eval/js/shared/helpers.ts +283 -0
  331. package/src/eval/js/shared/indirect-eval.ts +30 -0
  332. package/src/eval/js/shared/local-module-loader.ts +342 -0
  333. package/src/eval/js/shared/prelude.ts +2 -0
  334. package/src/eval/js/shared/prelude.txt +246 -0
  335. package/src/eval/js/shared/rewrite-imports.ts +532 -0
  336. package/src/eval/js/shared/runtime.ts +352 -0
  337. package/src/eval/js/shared/types.ts +18 -0
  338. package/src/eval/js/tool-bridge.ts +162 -0
  339. package/src/eval/js/worker-core.ts +132 -0
  340. package/src/eval/js/worker-entry.ts +30 -0
  341. package/src/eval/js/worker-protocol.ts +47 -0
  342. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  343. package/src/eval/py/display.ts +71 -0
  344. package/src/eval/py/executor.ts +742 -0
  345. package/src/eval/py/index.ts +68 -0
  346. package/src/eval/py/kernel.ts +748 -0
  347. package/src/eval/py/prelude.py +658 -0
  348. package/src/eval/py/prelude.ts +3 -0
  349. package/src/eval/py/runner.py +1133 -0
  350. package/src/eval/py/runtime.ts +276 -0
  351. package/src/eval/py/spawn-options.ts +126 -0
  352. package/src/eval/py/tool-bridge.ts +182 -0
  353. package/src/eval/session-id.ts +8 -0
  354. package/src/eval/types.ts +48 -0
  355. package/src/exa/index.ts +2 -0
  356. package/src/exa/mcp-client.ts +370 -0
  357. package/src/exa/types.ts +69 -0
  358. package/src/exec/bash-executor.ts +419 -0
  359. package/src/exec/exec.ts +53 -0
  360. package/src/exec/non-interactive-env.ts +48 -0
  361. package/src/export/custom-share.ts +65 -0
  362. package/src/export/html/index.ts +164 -0
  363. package/src/export/html/template.css +1051 -0
  364. package/src/export/html/template.generated.ts +2 -0
  365. package/src/export/html/template.html +46 -0
  366. package/src/export/html/template.js +2271 -0
  367. package/src/export/html/template.macro.ts +25 -0
  368. package/src/export/html/vendor/highlight.min.js +1213 -0
  369. package/src/export/html/vendor/marked.min.js +6 -0
  370. package/src/export/ttsr.ts +583 -0
  371. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  372. package/src/extensibility/custom-commands/bundled/review/index.ts +489 -0
  373. package/src/extensibility/custom-commands/index.ts +2 -0
  374. package/src/extensibility/custom-commands/loader.ts +238 -0
  375. package/src/extensibility/custom-commands/types.ts +113 -0
  376. package/src/extensibility/custom-tools/index.ts +7 -0
  377. package/src/extensibility/custom-tools/loader.ts +269 -0
  378. package/src/extensibility/custom-tools/types.ts +270 -0
  379. package/src/extensibility/custom-tools/wrapper.ts +47 -0
  380. package/src/extensibility/extensions/compact-handler.ts +40 -0
  381. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  382. package/src/extensibility/extensions/index.ts +16 -0
  383. package/src/extensibility/extensions/loader.ts +572 -0
  384. package/src/extensibility/extensions/runner.ts +922 -0
  385. package/src/extensibility/extensions/types.ts +1322 -0
  386. package/src/extensibility/extensions/wrapper.ts +223 -0
  387. package/src/extensibility/hooks/index.ts +5 -0
  388. package/src/extensibility/hooks/loader.ts +257 -0
  389. package/src/extensibility/hooks/runner.ts +425 -0
  390. package/src/extensibility/hooks/tool-wrapper.ts +107 -0
  391. package/src/extensibility/hooks/types.ts +606 -0
  392. package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
  393. package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
  394. package/src/extensibility/plugins/doctor.ts +65 -0
  395. package/src/extensibility/plugins/git-url.ts +367 -0
  396. package/src/extensibility/plugins/index.ts +9 -0
  397. package/src/extensibility/plugins/installer.ts +192 -0
  398. package/src/extensibility/plugins/legacy-pi-compat.ts +682 -0
  399. package/src/extensibility/plugins/loader.ts +313 -0
  400. package/src/extensibility/plugins/manager.ts +827 -0
  401. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  402. package/src/extensibility/plugins/marketplace/fetcher.ts +317 -0
  403. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  404. package/src/extensibility/plugins/marketplace/manager.ts +770 -0
  405. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  406. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  407. package/src/extensibility/plugins/marketplace/types.ts +191 -0
  408. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  409. package/src/extensibility/plugins/parser.ts +105 -0
  410. package/src/extensibility/plugins/types.ts +194 -0
  411. package/src/extensibility/shared-events.ts +343 -0
  412. package/src/extensibility/skills.ts +312 -0
  413. package/src/extensibility/slash-commands.ts +227 -0
  414. package/src/extensibility/tool-proxy.ts +25 -0
  415. package/src/extensibility/typebox.ts +418 -0
  416. package/src/extensibility/utils.ts +44 -0
  417. package/src/goals/index.ts +3 -0
  418. package/src/goals/runtime.ts +528 -0
  419. package/src/goals/state.ts +37 -0
  420. package/src/goals/tools/goal-tool.ts +251 -0
  421. package/src/hindsight/backend.ts +354 -0
  422. package/src/hindsight/bank.ts +156 -0
  423. package/src/hindsight/client.ts +598 -0
  424. package/src/hindsight/config.ts +175 -0
  425. package/src/hindsight/content.ts +210 -0
  426. package/src/hindsight/index.ts +8 -0
  427. package/src/hindsight/mental-models.ts +429 -0
  428. package/src/hindsight/seeds.json +32 -0
  429. package/src/hindsight/state.ts +488 -0
  430. package/src/hindsight/transcript.ts +71 -0
  431. package/src/index.ts +59 -0
  432. package/src/internal-urls/agent-protocol.ts +146 -0
  433. package/src/internal-urls/artifact-protocol.ts +107 -0
  434. package/src/internal-urls/docs-index.generated.ts +106 -0
  435. package/src/internal-urls/history-protocol.ts +113 -0
  436. package/src/internal-urls/index.ts +25 -0
  437. package/src/internal-urls/issue-pr-protocol.ts +584 -0
  438. package/src/internal-urls/json-query.ts +126 -0
  439. package/src/internal-urls/local-protocol.ts +287 -0
  440. package/src/internal-urls/mcp-protocol.ts +151 -0
  441. package/src/internal-urls/memory-protocol.ts +169 -0
  442. package/src/internal-urls/omp-protocol.ts +93 -0
  443. package/src/internal-urls/parse.ts +72 -0
  444. package/src/internal-urls/registry-helpers.ts +25 -0
  445. package/src/internal-urls/router.ts +105 -0
  446. package/src/internal-urls/rule-protocol.ts +45 -0
  447. package/src/internal-urls/skill-protocol.ts +96 -0
  448. package/src/internal-urls/types.ts +152 -0
  449. package/src/internal-urls/vault-protocol.ts +936 -0
  450. package/src/irc/bus.ts +292 -0
  451. package/src/lib/xai-http.ts +124 -0
  452. package/src/lsp/client.ts +1193 -0
  453. package/src/lsp/clients/biome-client.ts +264 -0
  454. package/src/lsp/clients/index.ts +50 -0
  455. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  456. package/src/lsp/clients/swiftlint-client.ts +120 -0
  457. package/src/lsp/config.ts +502 -0
  458. package/src/lsp/defaults.json +493 -0
  459. package/src/lsp/diagnostics-ledger.ts +51 -0
  460. package/src/lsp/edits.ts +267 -0
  461. package/src/lsp/index.ts +2477 -0
  462. package/src/lsp/lspmux.ts +233 -0
  463. package/src/lsp/render.ts +694 -0
  464. package/src/lsp/startup-events.ts +13 -0
  465. package/src/lsp/types.ts +455 -0
  466. package/src/lsp/utils.ts +718 -0
  467. package/src/main.ts +1325 -0
  468. package/src/mcp/client.ts +484 -0
  469. package/src/mcp/config-writer.ts +225 -0
  470. package/src/mcp/config.ts +365 -0
  471. package/src/mcp/index.ts +29 -0
  472. package/src/mcp/json-rpc.ts +122 -0
  473. package/src/mcp/loader.ts +124 -0
  474. package/src/mcp/manager.ts +1275 -0
  475. package/src/mcp/oauth-discovery.ts +442 -0
  476. package/src/mcp/oauth-flow.ts +442 -0
  477. package/src/mcp/render.ts +132 -0
  478. package/src/mcp/smithery-auth.ts +104 -0
  479. package/src/mcp/smithery-connect.ts +145 -0
  480. package/src/mcp/smithery-registry.ts +477 -0
  481. package/src/mcp/timeout.ts +59 -0
  482. package/src/mcp/tool-bridge.ts +426 -0
  483. package/src/mcp/tool-cache.ts +117 -0
  484. package/src/mcp/transports/http.ts +519 -0
  485. package/src/mcp/transports/index.ts +6 -0
  486. package/src/mcp/transports/stdio.ts +528 -0
  487. package/src/mcp/types.ts +423 -0
  488. package/src/memories/index.ts +1150 -0
  489. package/src/memories/storage.ts +577 -0
  490. package/src/memory-backend/index.ts +18 -0
  491. package/src/memory-backend/local-backend.ts +39 -0
  492. package/src/memory-backend/off-backend.ts +25 -0
  493. package/src/memory-backend/resolve.ts +25 -0
  494. package/src/memory-backend/runtime.ts +66 -0
  495. package/src/memory-backend/types.ts +166 -0
  496. package/src/mnemopi/backend.ts +547 -0
  497. package/src/mnemopi/config.ts +160 -0
  498. package/src/mnemopi/index.ts +3 -0
  499. package/src/mnemopi/state.ts +584 -0
  500. package/src/modes/acp/acp-agent.ts +2407 -0
  501. package/src/modes/acp/acp-client-bridge.ts +154 -0
  502. package/src/modes/acp/acp-event-mapper.ts +929 -0
  503. package/src/modes/acp/acp-mode.ts +23 -0
  504. package/src/modes/acp/index.ts +2 -0
  505. package/src/modes/acp/terminal-auth.ts +37 -0
  506. package/src/modes/components/agent-dashboard.ts +1206 -0
  507. package/src/modes/components/agent-hub.ts +1071 -0
  508. package/src/modes/components/assistant-message.ts +307 -0
  509. package/src/modes/components/bash-execution.ts +220 -0
  510. package/src/modes/components/bordered-loader.ts +41 -0
  511. package/src/modes/components/branch-summary-message.ts +45 -0
  512. package/src/modes/components/btw-panel.ts +104 -0
  513. package/src/modes/components/chat-block.ts +111 -0
  514. package/src/modes/components/compaction-summary-message.ts +87 -0
  515. package/src/modes/components/copy-selector.ts +206 -0
  516. package/src/modes/components/countdown-timer.ts +75 -0
  517. package/src/modes/components/custom-editor.ts +398 -0
  518. package/src/modes/components/custom-message.ts +63 -0
  519. package/src/modes/components/diff.ts +277 -0
  520. package/src/modes/components/dynamic-border.ts +34 -0
  521. package/src/modes/components/error-banner.ts +33 -0
  522. package/src/modes/components/eval-execution.ts +158 -0
  523. package/src/modes/components/execution-shared.ts +101 -0
  524. package/src/modes/components/extensions/extension-dashboard.ts +399 -0
  525. package/src/modes/components/extensions/extension-list.ts +502 -0
  526. package/src/modes/components/extensions/index.ts +9 -0
  527. package/src/modes/components/extensions/inspector-panel.ts +317 -0
  528. package/src/modes/components/extensions/state-manager.ts +627 -0
  529. package/src/modes/components/extensions/types.ts +186 -0
  530. package/src/modes/components/footer.ts +274 -0
  531. package/src/modes/components/history-search.ts +280 -0
  532. package/src/modes/components/hook-editor.ts +167 -0
  533. package/src/modes/components/hook-input.ts +87 -0
  534. package/src/modes/components/hook-message.ts +66 -0
  535. package/src/modes/components/hook-selector.ts +660 -0
  536. package/src/modes/components/index.ts +38 -0
  537. package/src/modes/components/keybinding-hints.ts +65 -0
  538. package/src/modes/components/late-diagnostics-message.ts +60 -0
  539. package/src/modes/components/login-dialog.ts +164 -0
  540. package/src/modes/components/mcp-add-wizard.ts +1340 -0
  541. package/src/modes/components/message-frame.ts +88 -0
  542. package/src/modes/components/model-selector.ts +1271 -0
  543. package/src/modes/components/oauth-selector.ts +368 -0
  544. package/src/modes/components/omfg-panel.ts +141 -0
  545. package/src/modes/components/overlay-box.ts +108 -0
  546. package/src/modes/components/plan-review-overlay.ts +820 -0
  547. package/src/modes/components/plan-toc.ts +138 -0
  548. package/src/modes/components/plugin-selector.ts +95 -0
  549. package/src/modes/components/plugin-settings.ts +722 -0
  550. package/src/modes/components/queue-mode-selector.ts +56 -0
  551. package/src/modes/components/read-tool-group.ts +670 -0
  552. package/src/modes/components/segment-track.ts +52 -0
  553. package/src/modes/components/session-selector.ts +625 -0
  554. package/src/modes/components/settings-defs.ts +189 -0
  555. package/src/modes/components/settings-selector.ts +651 -0
  556. package/src/modes/components/show-images-selector.ts +45 -0
  557. package/src/modes/components/skill-message.ts +89 -0
  558. package/src/modes/components/status-line/component.ts +869 -0
  559. package/src/modes/components/status-line/context-thresholds.ts +79 -0
  560. package/src/modes/components/status-line/git-utils.ts +42 -0
  561. package/src/modes/components/status-line/index.ts +5 -0
  562. package/src/modes/components/status-line/presets.ts +106 -0
  563. package/src/modes/components/status-line/segments.ts +584 -0
  564. package/src/modes/components/status-line/separators.ts +55 -0
  565. package/src/modes/components/status-line/token-rate.ts +66 -0
  566. package/src/modes/components/status-line/types.ts +108 -0
  567. package/src/modes/components/theme-selector.ts +63 -0
  568. package/src/modes/components/thinking-selector.ts +52 -0
  569. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  570. package/src/modes/components/tips.txt +19 -0
  571. package/src/modes/components/todo-reminder.ts +38 -0
  572. package/src/modes/components/tool-execution.ts +1024 -0
  573. package/src/modes/components/transcript-container.ts +608 -0
  574. package/src/modes/components/tree-selector.ts +978 -0
  575. package/src/modes/components/ttsr-notification.ts +122 -0
  576. package/src/modes/components/user-message-selector.ts +227 -0
  577. package/src/modes/components/user-message.ts +66 -0
  578. package/src/modes/components/visual-truncate.ts +63 -0
  579. package/src/modes/components/welcome.ts +493 -0
  580. package/src/modes/controllers/btw-controller.ts +105 -0
  581. package/src/modes/controllers/command-controller-shared.ts +109 -0
  582. package/src/modes/controllers/command-controller.ts +1566 -0
  583. package/src/modes/controllers/event-controller.ts +1054 -0
  584. package/src/modes/controllers/extension-ui-controller.ts +886 -0
  585. package/src/modes/controllers/input-controller.ts +1073 -0
  586. package/src/modes/controllers/mcp-command-controller.ts +2017 -0
  587. package/src/modes/controllers/omfg-controller.ts +283 -0
  588. package/src/modes/controllers/omfg-rule.ts +647 -0
  589. package/src/modes/controllers/selector-controller.ts +1108 -0
  590. package/src/modes/controllers/ssh-command-controller.ts +384 -0
  591. package/src/modes/controllers/streaming-reveal.ts +279 -0
  592. package/src/modes/controllers/tan-command-controller.ts +173 -0
  593. package/src/modes/controllers/todo-command-controller.ts +485 -0
  594. package/src/modes/data/emojis.json +1 -0
  595. package/src/modes/emoji-autocomplete.ts +285 -0
  596. package/src/modes/gradient-highlight.ts +87 -0
  597. package/src/modes/image-references.ts +117 -0
  598. package/src/modes/index.ts +17 -0
  599. package/src/modes/interactive-mode.ts +3370 -0
  600. package/src/modes/internal-url-autocomplete.ts +143 -0
  601. package/src/modes/loop-limit.ts +140 -0
  602. package/src/modes/magic-keywords.ts +20 -0
  603. package/src/modes/markdown-prose.ts +247 -0
  604. package/src/modes/oauth-manual-input.ts +69 -0
  605. package/src/modes/orchestrate.ts +42 -0
  606. package/src/modes/print-mode.ts +126 -0
  607. package/src/modes/prompt-action-autocomplete.ts +260 -0
  608. package/src/modes/rpc/host-tools.ts +186 -0
  609. package/src/modes/rpc/host-uris.ts +235 -0
  610. package/src/modes/rpc/rpc-client.ts +963 -0
  611. package/src/modes/rpc/rpc-mode.ts +947 -0
  612. package/src/modes/rpc/rpc-subagents.ts +265 -0
  613. package/src/modes/rpc/rpc-types.ts +458 -0
  614. package/src/modes/runtime-init.ts +116 -0
  615. package/src/modes/session-observer-registry.ts +146 -0
  616. package/src/modes/setup-version.ts +11 -0
  617. package/src/modes/setup-wizard/index.ts +99 -0
  618. package/src/modes/setup-wizard/lazy.ts +16 -0
  619. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  620. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  621. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  622. package/src/modes/setup-wizard/scenes/sign-in.ts +205 -0
  623. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  624. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  625. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  626. package/src/modes/setup-wizard/scenes/web-search.ts +129 -0
  627. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  628. package/src/modes/shared.ts +47 -0
  629. package/src/modes/theme/dark.json +95 -0
  630. package/src/modes/theme/defaults/alabaster.json +93 -0
  631. package/src/modes/theme/defaults/amethyst.json +96 -0
  632. package/src/modes/theme/defaults/anthracite.json +93 -0
  633. package/src/modes/theme/defaults/basalt.json +91 -0
  634. package/src/modes/theme/defaults/birch.json +95 -0
  635. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  636. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  637. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  638. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  639. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  640. package/src/modes/theme/defaults/dark-copper.json +95 -0
  641. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  642. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  643. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  644. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  645. package/src/modes/theme/defaults/dark-ember.json +95 -0
  646. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  647. package/src/modes/theme/defaults/dark-forest.json +96 -0
  648. package/src/modes/theme/defaults/dark-github.json +105 -0
  649. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  650. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  651. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  652. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  653. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  654. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  655. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  656. package/src/modes/theme/defaults/dark-nord.json +97 -0
  657. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  658. package/src/modes/theme/defaults/dark-one.json +100 -0
  659. package/src/modes/theme/defaults/dark-poimandres.json +142 -0
  660. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  661. package/src/modes/theme/defaults/dark-reef.json +91 -0
  662. package/src/modes/theme/defaults/dark-retro.json +92 -0
  663. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  664. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  665. package/src/modes/theme/defaults/dark-slate.json +95 -0
  666. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  667. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  668. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  669. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  670. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  671. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  672. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  673. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  674. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  675. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  676. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  677. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  678. package/src/modes/theme/defaults/graphite.json +92 -0
  679. package/src/modes/theme/defaults/index.ts +199 -0
  680. package/src/modes/theme/defaults/light-arctic.json +107 -0
  681. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  682. package/src/modes/theme/defaults/light-canyon.json +91 -0
  683. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  684. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  685. package/src/modes/theme/defaults/light-coral.json +95 -0
  686. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  687. package/src/modes/theme/defaults/light-dawn.json +90 -0
  688. package/src/modes/theme/defaults/light-dunes.json +91 -0
  689. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  690. package/src/modes/theme/defaults/light-forest.json +100 -0
  691. package/src/modes/theme/defaults/light-frost.json +95 -0
  692. package/src/modes/theme/defaults/light-github.json +115 -0
  693. package/src/modes/theme/defaults/light-glacier.json +91 -0
  694. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  695. package/src/modes/theme/defaults/light-haze.json +90 -0
  696. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  697. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  698. package/src/modes/theme/defaults/light-lavender.json +95 -0
  699. package/src/modes/theme/defaults/light-meadow.json +91 -0
  700. package/src/modes/theme/defaults/light-mint.json +95 -0
  701. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  702. package/src/modes/theme/defaults/light-ocean.json +99 -0
  703. package/src/modes/theme/defaults/light-one.json +99 -0
  704. package/src/modes/theme/defaults/light-opal.json +91 -0
  705. package/src/modes/theme/defaults/light-orchard.json +91 -0
  706. package/src/modes/theme/defaults/light-paper.json +95 -0
  707. package/src/modes/theme/defaults/light-poimandres.json +142 -0
  708. package/src/modes/theme/defaults/light-prism.json +90 -0
  709. package/src/modes/theme/defaults/light-retro.json +98 -0
  710. package/src/modes/theme/defaults/light-sand.json +95 -0
  711. package/src/modes/theme/defaults/light-savanna.json +91 -0
  712. package/src/modes/theme/defaults/light-solarized.json +102 -0
  713. package/src/modes/theme/defaults/light-soleil.json +90 -0
  714. package/src/modes/theme/defaults/light-sunset.json +99 -0
  715. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  716. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  717. package/src/modes/theme/defaults/light-wetland.json +91 -0
  718. package/src/modes/theme/defaults/light-zenith.json +89 -0
  719. package/src/modes/theme/defaults/limestone.json +94 -0
  720. package/src/modes/theme/defaults/mahogany.json +97 -0
  721. package/src/modes/theme/defaults/marble.json +93 -0
  722. package/src/modes/theme/defaults/obsidian.json +91 -0
  723. package/src/modes/theme/defaults/onyx.json +91 -0
  724. package/src/modes/theme/defaults/pearl.json +93 -0
  725. package/src/modes/theme/defaults/porcelain.json +91 -0
  726. package/src/modes/theme/defaults/quartz.json +96 -0
  727. package/src/modes/theme/defaults/sandstone.json +95 -0
  728. package/src/modes/theme/defaults/titanium.json +90 -0
  729. package/src/modes/theme/light.json +93 -0
  730. package/src/modes/theme/mermaid-cache.ts +29 -0
  731. package/src/modes/theme/shimmer.ts +235 -0
  732. package/src/modes/theme/theme-schema.json +459 -0
  733. package/src/modes/theme/theme.ts +2676 -0
  734. package/src/modes/turn-budget.ts +31 -0
  735. package/src/modes/types.ts +359 -0
  736. package/src/modes/ultrathink.ts +41 -0
  737. package/src/modes/utils/context-usage.ts +339 -0
  738. package/src/modes/utils/copy-targets.ts +360 -0
  739. package/src/modes/utils/hotkeys-markdown.ts +61 -0
  740. package/src/modes/utils/keybinding-matchers.ts +51 -0
  741. package/src/modes/utils/tools-markdown.ts +27 -0
  742. package/src/modes/utils/ui-helpers.ts +801 -0
  743. package/src/modes/workflow.ts +42 -0
  744. package/src/plan-mode/approved-plan.ts +186 -0
  745. package/src/plan-mode/plan-handoff.ts +37 -0
  746. package/src/plan-mode/plan-protection.ts +31 -0
  747. package/src/plan-mode/state.ts +6 -0
  748. package/src/priority.json +41 -0
  749. package/src/prompts/agents/designer.md +66 -0
  750. package/src/prompts/agents/explore.md +58 -0
  751. package/src/prompts/agents/frontmatter.md +11 -0
  752. package/src/prompts/agents/init.md +33 -0
  753. package/src/prompts/agents/librarian.md +119 -0
  754. package/src/prompts/agents/oracle.md +55 -0
  755. package/src/prompts/agents/plan.md +48 -0
  756. package/src/prompts/agents/reviewer.md +140 -0
  757. package/src/prompts/agents/task.md +16 -0
  758. package/src/prompts/ci-green-request.md +36 -0
  759. package/src/prompts/dry-balance-bench.md +8 -0
  760. package/src/prompts/goals/goal-budget-limit.md +16 -0
  761. package/src/prompts/goals/goal-continuation.md +28 -0
  762. package/src/prompts/goals/goal-mode-active.md +23 -0
  763. package/src/prompts/memories/consolidation.md +30 -0
  764. package/src/prompts/memories/read-path.md +11 -0
  765. package/src/prompts/memories/stage_one_input.md +6 -0
  766. package/src/prompts/memories/stage_one_system.md +21 -0
  767. package/src/prompts/review-custom-request.md +22 -0
  768. package/src/prompts/review-headless-request.md +16 -0
  769. package/src/prompts/review-request.md +69 -0
  770. package/src/prompts/steering/user-interjection.md +10 -0
  771. package/src/prompts/system/agent-creation-architect.md +50 -0
  772. package/src/prompts/system/agent-creation-user.md +6 -0
  773. package/src/prompts/system/auto-continue.md +1 -0
  774. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  775. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  776. package/src/prompts/system/background-tan-dispatch.md +8 -0
  777. package/src/prompts/system/btw-user.md +8 -0
  778. package/src/prompts/system/commit-message-system.md +14 -0
  779. package/src/prompts/system/custom-system-prompt.md +64 -0
  780. package/src/prompts/system/eager-todo.md +13 -0
  781. package/src/prompts/system/empty-stop-retry.md +6 -0
  782. package/src/prompts/system/irc-incoming.md +7 -0
  783. package/src/prompts/system/manual-continue.md +7 -0
  784. package/src/prompts/system/memory-consolidation-system.md +8 -0
  785. package/src/prompts/system/memory-extraction-system.md +26 -0
  786. package/src/prompts/system/omfg-user.md +50 -0
  787. package/src/prompts/system/orchestrate-notice.md +40 -0
  788. package/src/prompts/system/plan-mode-active.md +109 -0
  789. package/src/prompts/system/plan-mode-approved.md +25 -0
  790. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  791. package/src/prompts/system/plan-mode-reference.md +11 -0
  792. package/src/prompts/system/plan-mode-subagent.md +33 -0
  793. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  794. package/src/prompts/system/project-prompt.md +52 -0
  795. package/src/prompts/system/subagent-system-prompt.md +64 -0
  796. package/src/prompts/system/subagent-user-prompt.md +3 -0
  797. package/src/prompts/system/subagent-yield-reminder.md +12 -0
  798. package/src/prompts/system/system-prompt.md +258 -0
  799. package/src/prompts/system/tiny-title-system.md +8 -0
  800. package/src/prompts/system/title-system.md +16 -0
  801. package/src/prompts/system/ttsr-interrupt.md +7 -0
  802. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  803. package/src/prompts/system/ultrathink-notice.md +3 -0
  804. package/src/prompts/system/web-search.md +25 -0
  805. package/src/prompts/system/workflow-notice.md +70 -0
  806. package/src/prompts/tools/apply-patch.md +65 -0
  807. package/src/prompts/tools/ask.md +30 -0
  808. package/src/prompts/tools/ast-edit.md +39 -0
  809. package/src/prompts/tools/ast-grep.md +42 -0
  810. package/src/prompts/tools/async-result.md +8 -0
  811. package/src/prompts/tools/bash.md +46 -0
  812. package/src/prompts/tools/browser.md +73 -0
  813. package/src/prompts/tools/checkpoint.md +16 -0
  814. package/src/prompts/tools/debug.md +34 -0
  815. package/src/prompts/tools/eval.md +92 -0
  816. package/src/prompts/tools/find.md +36 -0
  817. package/src/prompts/tools/github.md +21 -0
  818. package/src/prompts/tools/goal.md +18 -0
  819. package/src/prompts/tools/image-gen.md +7 -0
  820. package/src/prompts/tools/inspect-image-system.md +20 -0
  821. package/src/prompts/tools/inspect-image.md +32 -0
  822. package/src/prompts/tools/irc.md +59 -0
  823. package/src/prompts/tools/job.md +19 -0
  824. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  825. package/src/prompts/tools/lsp.md +42 -0
  826. package/src/prompts/tools/memory-edit.md +8 -0
  827. package/src/prompts/tools/patch.md +70 -0
  828. package/src/prompts/tools/read.md +84 -0
  829. package/src/prompts/tools/recall.md +5 -0
  830. package/src/prompts/tools/reflect.md +5 -0
  831. package/src/prompts/tools/render-mermaid.md +9 -0
  832. package/src/prompts/tools/replace.md +30 -0
  833. package/src/prompts/tools/resolve.md +9 -0
  834. package/src/prompts/tools/retain.md +6 -0
  835. package/src/prompts/tools/rewind.md +13 -0
  836. package/src/prompts/tools/search-tool-bm25.md +32 -0
  837. package/src/prompts/tools/search.md +24 -0
  838. package/src/prompts/tools/ssh.md +31 -0
  839. package/src/prompts/tools/task-summary.md +17 -0
  840. package/src/prompts/tools/task.md +88 -0
  841. package/src/prompts/tools/todo.md +62 -0
  842. package/src/prompts/tools/web-search.md +10 -0
  843. package/src/prompts/tools/write.md +14 -0
  844. package/src/registry/agent-lifecycle.ts +218 -0
  845. package/src/registry/agent-registry.ts +151 -0
  846. package/src/sdk.ts +2558 -0
  847. package/src/secrets/index.ts +123 -0
  848. package/src/secrets/obfuscator.ts +298 -0
  849. package/src/secrets/regex.ts +21 -0
  850. package/src/session/agent-session.ts +10121 -0
  851. package/src/session/agent-storage.ts +455 -0
  852. package/src/session/artifacts.ts +135 -0
  853. package/src/session/auth-broker-config.ts +131 -0
  854. package/src/session/auth-storage.ts +29 -0
  855. package/src/session/blob-store.ts +255 -0
  856. package/src/session/client-bridge.ts +85 -0
  857. package/src/session/history-storage.ts +348 -0
  858. package/src/session/indexed-session-storage.ts +430 -0
  859. package/src/session/messages.ts +541 -0
  860. package/src/session/redis-session-storage.ts +170 -0
  861. package/src/session/session-dump-format.ts +209 -0
  862. package/src/session/session-history-format.ts +246 -0
  863. package/src/session/session-manager.ts +3676 -0
  864. package/src/session/session-storage.ts +529 -0
  865. package/src/session/shake-types.ts +43 -0
  866. package/src/session/sql-session-storage.ts +314 -0
  867. package/src/session/streaming-output.ts +1330 -0
  868. package/src/session/tool-choice-queue.ts +213 -0
  869. package/src/session/yield-queue.ts +173 -0
  870. package/src/slash-commands/acp-builtins.ts +70 -0
  871. package/src/slash-commands/builtin-registry.ts +1798 -0
  872. package/src/slash-commands/helpers/context-report.ts +39 -0
  873. package/src/slash-commands/helpers/format.ts +46 -0
  874. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  875. package/src/slash-commands/helpers/mcp.ts +532 -0
  876. package/src/slash-commands/helpers/parse.ts +85 -0
  877. package/src/slash-commands/helpers/ssh.ts +195 -0
  878. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  879. package/src/slash-commands/helpers/todo.ts +279 -0
  880. package/src/slash-commands/helpers/usage-report.ts +95 -0
  881. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  882. package/src/slash-commands/types.ts +135 -0
  883. package/src/ssh/config-writer.ts +183 -0
  884. package/src/ssh/connection-manager.ts +509 -0
  885. package/src/ssh/ssh-executor.ts +189 -0
  886. package/src/ssh/sshfs-mount.ts +140 -0
  887. package/src/ssh/utils.ts +8 -0
  888. package/src/stt/downloader.ts +71 -0
  889. package/src/stt/index.ts +3 -0
  890. package/src/stt/recorder.ts +351 -0
  891. package/src/stt/setup.ts +52 -0
  892. package/src/stt/stt-controller.ts +160 -0
  893. package/src/stt/transcribe.py +70 -0
  894. package/src/stt/transcriber.ts +91 -0
  895. package/src/stubs/natives/index.ts +814 -0
  896. package/src/stubs/natives/package.json +7 -0
  897. package/src/stubs/tui/index.ts +282 -0
  898. package/src/stubs/tui/package.json +7 -0
  899. package/src/system-prompt.ts +611 -0
  900. package/src/task/agents.ts +167 -0
  901. package/src/task/commands.ts +132 -0
  902. package/src/task/discovery.ts +122 -0
  903. package/src/task/executor.ts +2133 -0
  904. package/src/task/index.ts +1419 -0
  905. package/src/task/name-generator.ts +1577 -0
  906. package/src/task/omp-command.ts +26 -0
  907. package/src/task/output-manager.ts +88 -0
  908. package/src/task/parallel.ts +116 -0
  909. package/src/task/render.ts +1381 -0
  910. package/src/task/repair-args.ts +129 -0
  911. package/src/task/subprocess-tool-registry.ts +88 -0
  912. package/src/task/types.ts +336 -0
  913. package/src/task/worktree.ts +514 -0
  914. package/src/telemetry-export.ts +144 -0
  915. package/src/thinking.ts +167 -0
  916. package/src/tiny/compiled-runtime.ts +179 -0
  917. package/src/tiny/device.ts +111 -0
  918. package/src/tiny/dtype.ts +101 -0
  919. package/src/tiny/models.ts +242 -0
  920. package/src/tiny/text.ts +165 -0
  921. package/src/tiny/title-client.ts +543 -0
  922. package/src/tiny/title-protocol.ts +56 -0
  923. package/src/tiny/worker.ts +568 -0
  924. package/src/tool-discovery/mode.ts +24 -0
  925. package/src/tool-discovery/tool-index.ts +256 -0
  926. package/src/tools/approval.ts +189 -0
  927. package/src/tools/archive-reader.ts +721 -0
  928. package/src/tools/ask.ts +928 -0
  929. package/src/tools/ast-edit.ts +642 -0
  930. package/src/tools/ast-grep.ts +452 -0
  931. package/src/tools/auto-generated-guard.ts +322 -0
  932. package/src/tools/bash-command-fixup.ts +37 -0
  933. package/src/tools/bash-interactive.ts +408 -0
  934. package/src/tools/bash-interceptor.ts +67 -0
  935. package/src/tools/bash-pty-selection.ts +14 -0
  936. package/src/tools/bash-skill-urls.ts +248 -0
  937. package/src/tools/bash.ts +1386 -0
  938. package/src/tools/browser/attach.ts +175 -0
  939. package/src/tools/browser/launch.ts +660 -0
  940. package/src/tools/browser/readable.ts +112 -0
  941. package/src/tools/browser/registry.ts +197 -0
  942. package/src/tools/browser/render.ts +216 -0
  943. package/src/tools/browser/tab-protocol.ts +105 -0
  944. package/src/tools/browser/tab-supervisor.ts +628 -0
  945. package/src/tools/browser/tab-worker-entry.ts +21 -0
  946. package/src/tools/browser/tab-worker.ts +1226 -0
  947. package/src/tools/browser.ts +343 -0
  948. package/src/tools/checkpoint.ts +136 -0
  949. package/src/tools/conflict-detect.ts +718 -0
  950. package/src/tools/context.ts +39 -0
  951. package/src/tools/debug.ts +1067 -0
  952. package/src/tools/eval-backends.ts +27 -0
  953. package/src/tools/eval-render.ts +752 -0
  954. package/src/tools/eval.ts +577 -0
  955. package/src/tools/fetch.ts +1926 -0
  956. package/src/tools/file-recorder.ts +35 -0
  957. package/src/tools/find.ts +609 -0
  958. package/src/tools/fs-cache-invalidation.ts +28 -0
  959. package/src/tools/gh-cache-invalidation.ts +255 -0
  960. package/src/tools/gh-format.ts +12 -0
  961. package/src/tools/gh-renderer.ts +481 -0
  962. package/src/tools/gh.ts +3720 -0
  963. package/src/tools/github-cache.ts +637 -0
  964. package/src/tools/grouped-file-output.ts +210 -0
  965. package/src/tools/image-gen.ts +1517 -0
  966. package/src/tools/index.ts +599 -0
  967. package/src/tools/inspect-image-renderer.ts +132 -0
  968. package/src/tools/inspect-image.ts +174 -0
  969. package/src/tools/irc.ts +723 -0
  970. package/src/tools/job.ts +557 -0
  971. package/src/tools/json-tree.ts +243 -0
  972. package/src/tools/jtd-to-json-schema.ts +219 -0
  973. package/src/tools/jtd-to-typescript.ts +136 -0
  974. package/src/tools/jtd-utils.ts +102 -0
  975. package/src/tools/list-limit.ts +40 -0
  976. package/src/tools/match-line-format.ts +20 -0
  977. package/src/tools/memory-edit.ts +59 -0
  978. package/src/tools/memory-recall.ts +100 -0
  979. package/src/tools/memory-reflect.ts +88 -0
  980. package/src/tools/memory-render.ts +202 -0
  981. package/src/tools/memory-retain.ts +91 -0
  982. package/src/tools/output-meta.ts +754 -0
  983. package/src/tools/output-schema-validator.ts +132 -0
  984. package/src/tools/path-utils.ts +1054 -0
  985. package/src/tools/plan-mode-guard.ts +108 -0
  986. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  987. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  988. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  989. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  990. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  991. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  992. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  993. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  994. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  995. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  996. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  997. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  998. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  999. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  1000. package/src/tools/read.ts +2929 -0
  1001. package/src/tools/render-mermaid.ts +69 -0
  1002. package/src/tools/render-utils.ts +838 -0
  1003. package/src/tools/renderers.ts +77 -0
  1004. package/src/tools/report-tool-issue.ts +534 -0
  1005. package/src/tools/resolve.ts +276 -0
  1006. package/src/tools/review.ts +253 -0
  1007. package/src/tools/search-tool-bm25.ts +351 -0
  1008. package/src/tools/search.ts +1580 -0
  1009. package/src/tools/sqlite-reader.ts +828 -0
  1010. package/src/tools/ssh.ts +349 -0
  1011. package/src/tools/todo.ts +982 -0
  1012. package/src/tools/tool-errors.ts +62 -0
  1013. package/src/tools/tool-result.ts +94 -0
  1014. package/src/tools/tool-timeouts.ts +30 -0
  1015. package/src/tools/tts.ts +133 -0
  1016. package/src/tools/write.ts +1217 -0
  1017. package/src/tools/yield.ts +269 -0
  1018. package/src/tui/code-cell.ts +216 -0
  1019. package/src/tui/file-list.ts +55 -0
  1020. package/src/tui/hyperlink.ts +175 -0
  1021. package/src/tui/index.ts +12 -0
  1022. package/src/tui/output-block.ts +240 -0
  1023. package/src/tui/status-line.ts +54 -0
  1024. package/src/tui/tree-list.ts +84 -0
  1025. package/src/tui/types.ts +15 -0
  1026. package/src/tui/utils.ts +103 -0
  1027. package/src/utils/block-context.ts +312 -0
  1028. package/src/utils/changelog.ts +132 -0
  1029. package/src/utils/clipboard.ts +193 -0
  1030. package/src/utils/command-args.ts +76 -0
  1031. package/src/utils/commit-message-generator.ts +151 -0
  1032. package/src/utils/edit-mode.ts +41 -0
  1033. package/src/utils/enhanced-paste.ts +230 -0
  1034. package/src/utils/event-bus.ts +33 -0
  1035. package/src/utils/external-editor.ts +65 -0
  1036. package/src/utils/file-display-mode.ts +45 -0
  1037. package/src/utils/file-mentions.ts +281 -0
  1038. package/src/utils/git.ts +1833 -0
  1039. package/src/utils/image-loading.ts +132 -0
  1040. package/src/utils/image-resize.ts +309 -0
  1041. package/src/utils/jj.ts +248 -0
  1042. package/src/utils/lang-from-path.ts +239 -0
  1043. package/src/utils/markit.ts +89 -0
  1044. package/src/utils/open.ts +55 -0
  1045. package/src/utils/session-color.ts +68 -0
  1046. package/src/utils/shell-snapshot.ts +187 -0
  1047. package/src/utils/sixel.ts +69 -0
  1048. package/src/utils/title-generator.ts +373 -0
  1049. package/src/utils/tool-choice.ts +33 -0
  1050. package/src/utils/tools-manager.ts +363 -0
  1051. package/src/web/kagi.ts +305 -0
  1052. package/src/web/parallel.ts +353 -0
  1053. package/src/web/scrapers/artifacthub.ts +207 -0
  1054. package/src/web/scrapers/arxiv.ts +83 -0
  1055. package/src/web/scrapers/aur.ts +162 -0
  1056. package/src/web/scrapers/biorxiv.ts +133 -0
  1057. package/src/web/scrapers/bluesky.ts +262 -0
  1058. package/src/web/scrapers/brew.ts +172 -0
  1059. package/src/web/scrapers/cheatsh.ts +68 -0
  1060. package/src/web/scrapers/chocolatey.ts +196 -0
  1061. package/src/web/scrapers/choosealicense.ts +95 -0
  1062. package/src/web/scrapers/cisa-kev.ts +87 -0
  1063. package/src/web/scrapers/clojars.ts +154 -0
  1064. package/src/web/scrapers/coingecko.ts +177 -0
  1065. package/src/web/scrapers/crates-io.ts +97 -0
  1066. package/src/web/scrapers/crossref.ts +136 -0
  1067. package/src/web/scrapers/devto.ts +147 -0
  1068. package/src/web/scrapers/discogs.ts +306 -0
  1069. package/src/web/scrapers/discourse.ts +197 -0
  1070. package/src/web/scrapers/dockerhub.ts +138 -0
  1071. package/src/web/scrapers/docs-rs.ts +653 -0
  1072. package/src/web/scrapers/fdroid.ts +134 -0
  1073. package/src/web/scrapers/firefox-addons.ts +191 -0
  1074. package/src/web/scrapers/flathub.ts +223 -0
  1075. package/src/web/scrapers/github-gist.ts +58 -0
  1076. package/src/web/scrapers/github.ts +704 -0
  1077. package/src/web/scrapers/gitlab.ts +401 -0
  1078. package/src/web/scrapers/go-pkg.ts +266 -0
  1079. package/src/web/scrapers/hackage.ts +140 -0
  1080. package/src/web/scrapers/hackernews.ts +189 -0
  1081. package/src/web/scrapers/hex.ts +105 -0
  1082. package/src/web/scrapers/huggingface.ts +321 -0
  1083. package/src/web/scrapers/iacr.ts +89 -0
  1084. package/src/web/scrapers/index.ts +252 -0
  1085. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1086. package/src/web/scrapers/lemmy.ts +203 -0
  1087. package/src/web/scrapers/lobsters.ts +175 -0
  1088. package/src/web/scrapers/mastodon.ts +292 -0
  1089. package/src/web/scrapers/maven.ts +138 -0
  1090. package/src/web/scrapers/mdn.ts +173 -0
  1091. package/src/web/scrapers/metacpan.ts +222 -0
  1092. package/src/web/scrapers/musicbrainz.ts +250 -0
  1093. package/src/web/scrapers/npm.ts +98 -0
  1094. package/src/web/scrapers/nuget.ts +183 -0
  1095. package/src/web/scrapers/nvd.ts +222 -0
  1096. package/src/web/scrapers/ollama.ts +239 -0
  1097. package/src/web/scrapers/open-vsx.ts +106 -0
  1098. package/src/web/scrapers/opencorporates.ts +292 -0
  1099. package/src/web/scrapers/openlibrary.ts +336 -0
  1100. package/src/web/scrapers/orcid.ts +286 -0
  1101. package/src/web/scrapers/osv.ts +176 -0
  1102. package/src/web/scrapers/packagist.ts +160 -0
  1103. package/src/web/scrapers/pub-dev.ts +143 -0
  1104. package/src/web/scrapers/pubmed.ts +211 -0
  1105. package/src/web/scrapers/pypi.ts +112 -0
  1106. package/src/web/scrapers/rawg.ts +110 -0
  1107. package/src/web/scrapers/readthedocs.ts +120 -0
  1108. package/src/web/scrapers/reddit.ts +95 -0
  1109. package/src/web/scrapers/repology.ts +251 -0
  1110. package/src/web/scrapers/rfc.ts +201 -0
  1111. package/src/web/scrapers/rubygems.ts +103 -0
  1112. package/src/web/scrapers/searchcode.ts +189 -0
  1113. package/src/web/scrapers/sec-edgar.ts +261 -0
  1114. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1115. package/src/web/scrapers/snapcraft.ts +187 -0
  1116. package/src/web/scrapers/sourcegraph.ts +336 -0
  1117. package/src/web/scrapers/spdx.ts +108 -0
  1118. package/src/web/scrapers/spotify.ts +198 -0
  1119. package/src/web/scrapers/stackoverflow.ts +120 -0
  1120. package/src/web/scrapers/terraform.ts +277 -0
  1121. package/src/web/scrapers/tldr.ts +47 -0
  1122. package/src/web/scrapers/twitter.ts +94 -0
  1123. package/src/web/scrapers/types.ts +397 -0
  1124. package/src/web/scrapers/utils.ts +109 -0
  1125. package/src/web/scrapers/vimeo.ts +133 -0
  1126. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1127. package/src/web/scrapers/w3c.ts +156 -0
  1128. package/src/web/scrapers/wikidata.ts +344 -0
  1129. package/src/web/scrapers/wikipedia.ts +84 -0
  1130. package/src/web/scrapers/youtube.ts +325 -0
  1131. package/src/web/search/index.ts +292 -0
  1132. package/src/web/search/provider.ts +157 -0
  1133. package/src/web/search/providers/anthropic.ts +318 -0
  1134. package/src/web/search/providers/base.ts +89 -0
  1135. package/src/web/search/providers/brave.ts +152 -0
  1136. package/src/web/search/providers/codex.ts +591 -0
  1137. package/src/web/search/providers/exa.ts +400 -0
  1138. package/src/web/search/providers/gemini.ts +460 -0
  1139. package/src/web/search/providers/jina.ts +111 -0
  1140. package/src/web/search/providers/kagi.ts +86 -0
  1141. package/src/web/search/providers/kimi.ts +196 -0
  1142. package/src/web/search/providers/parallel.ts +225 -0
  1143. package/src/web/search/providers/perplexity.ts +730 -0
  1144. package/src/web/search/providers/searxng.ts +313 -0
  1145. package/src/web/search/providers/synthetic.ts +114 -0
  1146. package/src/web/search/providers/tavily.ts +176 -0
  1147. package/src/web/search/providers/utils.ts +128 -0
  1148. package/src/web/search/providers/zai.ts +333 -0
  1149. package/src/web/search/render.ts +262 -0
  1150. package/src/web/search/types.ts +482 -0
  1151. package/src/web/search/utils.ts +17 -0
  1152. package/src/workspace-tree.ts +286 -0
@@ -0,0 +1,2090 @@
1
+ import { execSync } from "node:child_process";
2
+ import * as path from "node:path";
3
+ import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
4
+ import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
5
+ import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
6
+ import { buildModel } from "@oh-my-pi/pi-catalog/build";
7
+ import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
8
+ import { readModelCache } from "@oh-my-pi/pi-catalog/model-cache";
9
+ import {
10
+ createModelManager,
11
+ type ModelManagerOptions,
12
+ type ModelRefreshStrategy,
13
+ } from "@oh-my-pi/pi-catalog/model-manager";
14
+ import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/models";
15
+ import {
16
+ googleAntigravityModelManagerOptions,
17
+ googleGeminiCliModelManagerOptions,
18
+ openaiCodexModelManagerOptions,
19
+ PROVIDER_DESCRIPTORS,
20
+ UNK_CONTEXT_WINDOW,
21
+ UNK_MAX_TOKENS,
22
+ } from "@oh-my-pi/pi-catalog/provider-models";
23
+
24
+ // Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
25
+ // any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
26
+ const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
27
+
28
+ const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
29
+ "google-antigravity",
30
+ "google-gemini-cli",
31
+ "openai-codex",
32
+ ];
33
+
34
+ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
35
+ ...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
36
+ ...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
37
+ ];
38
+
39
+ import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
40
+ import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
41
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
42
+ import {
43
+ buildCanonicalModelIndex,
44
+ buildCanonicalModelOrder,
45
+ buildModelProviderPriorityRank,
46
+ type CanonicalModelIndex,
47
+ type CanonicalModelRecord,
48
+ type CanonicalModelVariant,
49
+ type CanonicalVariantPreferences,
50
+ formatCanonicalVariantSelector,
51
+ getBundledCanonicalReferenceData,
52
+ getBundledModelReferenceIndex,
53
+ type ModelEquivalenceConfig,
54
+ resolveCanonicalVariant,
55
+ resolveModelReference,
56
+ } from "@oh-my-pi/pi-catalog/identity";
57
+ import { isRecord, logger } from "@oh-my-pi/pi-utils";
58
+ import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
59
+ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
60
+ import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
61
+ import type { ConfigError, ConfigFile } from "./config-file";
62
+ import {
63
+ DISCOVERY_DEFAULT_MAX_TOKENS,
64
+ type DiscoveryContext,
65
+ type DiscoveryProviderConfig,
66
+ discoverModelsByProviderType,
67
+ getImplicitOllamaBaseUrl,
68
+ getOllamaContextLengthOverride,
69
+ } from "./model-discovery";
70
+ import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfiguration } from "./models-config";
71
+ import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
72
+ import { settings } from "./settings";
73
+
74
+ export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
75
+
76
+ export const kNoAuth = "N/A";
77
+
78
+ export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
79
+ return Boolean(apiKey) && apiKey !== kNoAuth;
80
+ }
81
+
82
+ /** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
83
+ interface ProviderOverride {
84
+ baseUrl?: string;
85
+ headers?: Record<string, string>;
86
+ apiKey?: string;
87
+ authHeader?: boolean;
88
+ compat?: ModelSpec<Api>["compat"];
89
+ transport?: Model<Api>["transport"];
90
+ }
91
+
92
+ /**
93
+ * Merge a freshly discovered model with the matching bundled/configured entry
94
+ * (or a runtime provider override when no bundled entry exists).
95
+ *
96
+ * `baseUrl` resolution priority:
97
+ * 1. User-set `providerOverride.baseUrl` (explicit override in models.json)
98
+ * 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to
99
+ * `token-plan-sgp.xiaomimimo.com` at discovery time)
100
+ * 3. Existing bundled baseUrl (the host baked into `models.json`)
101
+ *
102
+ * Without (1), the user's override would lose to discovery; without (2)
103
+ * preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
104
+ * tp- token-plan host and produce 401s on the first stream call.
105
+ * See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
106
+ * regression in `model-registry.test.ts`.
107
+ */
108
+ export function mergeDiscoveredModel<TApi extends Api>(
109
+ model: Model<TApi>,
110
+ existing: Model<Api> | undefined,
111
+ providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
112
+ ): Model<TApi> {
113
+ if (existing) {
114
+ return buildModel({
115
+ ...model,
116
+ baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
117
+ headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
118
+ compat: model.compatConfig,
119
+ } as ModelSpec<TApi>);
120
+ }
121
+ if (providerOverride) {
122
+ return buildModel({
123
+ ...model,
124
+ baseUrl: providerOverride.baseUrl ?? model.baseUrl,
125
+ headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
126
+ ...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
127
+ compat: model.compatConfig,
128
+ } as ModelSpec<TApi>);
129
+ }
130
+ return model;
131
+ }
132
+
133
+ const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS = new Set<string>(
134
+ PROVIDER_DESCRIPTORS.filter(descriptor => descriptor.dynamicModelsAuthoritative).map(
135
+ descriptor => descriptor.providerId,
136
+ ),
137
+ );
138
+
139
+ function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
140
+ return (
141
+ model.provider === "google-vertex" &&
142
+ model.api === "openai-completions" &&
143
+ isVertexExpressOpenAIUrl(model.baseUrl)
144
+ );
145
+ }
146
+
147
+ function providersWithAuthoritativeProjectCatalog(models: readonly Model<Api>[]): Set<string> {
148
+ const providers = new Set<string>();
149
+ for (const model of models) {
150
+ if (isAuthoritativeProjectCatalogModel(model)) {
151
+ providers.add(model.provider);
152
+ }
153
+ }
154
+ return providers;
155
+ }
156
+
157
+ function dropProviderModels(models: readonly Model<Api>[], providers: ReadonlySet<string>): Model<Api>[] {
158
+ return models.filter(model => !providers.has(model.provider));
159
+ }
160
+
161
+ /**
162
+ * Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`.
163
+ * Matches are replaced with `combine(existing, entry)`; new entries are
164
+ * appended as `combine(undefined, entry)`.
165
+ */
166
+ function mergeByModelKey<T extends { provider: string; id: string }>(
167
+ base: readonly Model<Api>[],
168
+ incoming: readonly T[],
169
+ combine: (existing: Model<Api> | undefined, entry: T) => Model<Api>,
170
+ ): Model<Api>[] {
171
+ const merged = [...base];
172
+ const indexByKey = new Map<string, number>();
173
+ for (let i = 0; i < merged.length; i += 1) {
174
+ indexByKey.set(`${merged[i].provider}\u0000${merged[i].id}`, i);
175
+ }
176
+ for (const entry of incoming) {
177
+ const key = `${entry.provider}\u0000${entry.id}`;
178
+ const existingIndex = indexByKey.get(key);
179
+ if (existingIndex !== undefined) {
180
+ merged[existingIndex] = combine(merged[existingIndex], entry);
181
+ } else {
182
+ merged.push(combine(undefined, entry));
183
+ indexByKey.set(key, merged.length - 1);
184
+ }
185
+ }
186
+ return merged;
187
+ }
188
+
189
+ interface BuiltInDiscoveryResult {
190
+ models: Model<Api>[];
191
+ authoritativeProviders: Set<string>;
192
+ }
193
+
194
+ export type ProviderDiscoveryStatus = "idle" | "ok" | "empty" | "cached" | "unavailable" | "unauthenticated";
195
+
196
+ export interface ProviderDiscoveryState {
197
+ provider: string;
198
+ status: ProviderDiscoveryStatus;
199
+ optional: boolean;
200
+ stale: boolean;
201
+ fetchedAt?: number;
202
+ models: string[];
203
+ error?: string;
204
+ }
205
+
206
+ export interface CanonicalModelQueryOptions {
207
+ availableOnly?: boolean;
208
+ candidates?: readonly Model<Api>[];
209
+ }
210
+
211
+ /** A canonical record (with query-filtered variants) plus the variant model selected for it. */
212
+ export interface CanonicalModelSelection {
213
+ record: CanonicalModelRecord;
214
+ model: Model<Api>;
215
+ }
216
+
217
+ /** Result of loading custom models from models.json */
218
+ interface CustomModelsResult {
219
+ models?: CustomModelOverlay[];
220
+ overrides?: Map<string, ProviderOverride>;
221
+ modelOverrides?: Map<string, Map<string, ModelOverride>>;
222
+ keylessProviders?: Set<string>;
223
+ discoverableProviders?: DiscoveryProviderConfig[];
224
+ configuredProviders?: Set<string>;
225
+ equivalence?: ModelEquivalenceConfig;
226
+ error?: ConfigError;
227
+ found: boolean;
228
+ }
229
+
230
+ const commandValueCache = new Map<string, string>();
231
+
232
+ function isCommandConfigValue(valueConfig: string | undefined): valueConfig is string {
233
+ return valueConfig?.startsWith("!") === true;
234
+ }
235
+
236
+ function resolveCommandConfig(command: string): string | undefined {
237
+ const cached = commandValueCache.get(command);
238
+ if (cached !== undefined) return cached;
239
+ try {
240
+ const stdout = execSync(command, { encoding: "utf8", timeout: 10_000, windowsHide: true });
241
+ const trimmed = stdout.trim();
242
+ if (trimmed.length === 0) return undefined;
243
+ commandValueCache.set(command, trimmed);
244
+ return trimmed;
245
+ } catch {
246
+ return undefined;
247
+ }
248
+ }
249
+
250
+ interface CommandApiKeyResolution {
251
+ configured: boolean;
252
+ value?: string;
253
+ }
254
+ /**
255
+ * Resolve a models.yml secret/config value to an actual value.
256
+ * `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
257
+ * checked first and the input falls back to a literal value.
258
+ */
259
+ function resolveConfigValue(valueConfig: string): string | undefined {
260
+ if (valueConfig.startsWith("!")) return resolveCommandConfig(valueConfig.slice(1).trim());
261
+ const envValue = Bun.env[valueConfig];
262
+ if (envValue) return envValue;
263
+ return valueConfig;
264
+ }
265
+
266
+ function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
267
+ if (!headers) return undefined;
268
+ const resolved: Record<string, string> = {};
269
+ for (const [key, value] of Object.entries(headers)) {
270
+ const next = resolveConfigValue(value);
271
+ if (next) resolved[key] = next;
272
+ }
273
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
274
+ }
275
+
276
+ function extractGoogleOAuthToken(value: string | undefined): string | undefined {
277
+ if (!isAuthenticated(value)) return undefined;
278
+ try {
279
+ const parsed = JSON.parse(value) as { token?: unknown };
280
+ if (Object.hasOwn(parsed, "token")) {
281
+ if (typeof parsed.token !== "string") {
282
+ return undefined;
283
+ }
284
+ const token = parsed.token.trim();
285
+ return token.length > 0 ? token : undefined;
286
+ }
287
+ } catch {
288
+ // OAuth values for Google providers are expected to be JSON, but custom setups may already provide raw token.
289
+ }
290
+ return value;
291
+ }
292
+
293
+ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: string): OAuthCredential[] {
294
+ const providerEntry = authStorage.getAll()[provider];
295
+ if (!providerEntry) {
296
+ return [];
297
+ }
298
+ const entries = Array.isArray(providerEntry) ? providerEntry : [providerEntry];
299
+ return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
300
+ }
301
+
302
+ function resolveOAuthAccountIdForAccessToken(
303
+ authStorage: AuthStorage,
304
+ provider: string,
305
+ accessToken: string,
306
+ ): string | undefined {
307
+ const oauthCredentials = getOAuthCredentialsForProvider(authStorage, provider);
308
+ const matchingCredential = oauthCredentials.find(credential => credential.access === accessToken);
309
+ if (matchingCredential) {
310
+ return matchingCredential.accountId;
311
+ }
312
+ if (oauthCredentials.length === 1) {
313
+ return oauthCredentials[0].accountId;
314
+ }
315
+ return undefined;
316
+ }
317
+
318
+ function mergeCompat<TBase extends object, TOverride extends object>(
319
+ baseCompat: TBase | null | undefined,
320
+ overrideCompat: TOverride | null | undefined,
321
+ ): (TBase & TOverride) | TBase | TOverride | undefined {
322
+ if (!baseCompat) return overrideCompat ?? undefined;
323
+ if (!overrideCompat) return baseCompat;
324
+
325
+ const merged: Record<string, unknown> = { ...(baseCompat as Record<string, unknown>) };
326
+ for (const [key, overrideValue] of Object.entries(overrideCompat)) {
327
+ const baseValue = (baseCompat as Record<string, unknown>)[key];
328
+ merged[key] =
329
+ isRecord(baseValue) && isRecord(overrideValue) ? mergeCompat(baseValue, overrideValue) : overrideValue;
330
+ }
331
+ return merged as TBase & TOverride;
332
+ }
333
+
334
+ /**
335
+ * Project a built model back to spec shape for the model-manager/cache
336
+ * boundary: sparse compat comes from `compatConfig`, never from the resolved
337
+ * record.
338
+ */
339
+ function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
340
+ return { ...model, compat: model.compatConfig } as ModelSpec<TApi>;
341
+ }
342
+
343
+ /**
344
+ * The patchable subset of `Model` fields shared by `modelOverrides` entries,
345
+ * custom model definitions, and parsed custom-model overlays. `undefined`
346
+ * always means "leave the base value alone".
347
+ */
348
+ interface ModelPatch {
349
+ name?: string;
350
+ reasoning?: boolean;
351
+ thinking?: ThinkingConfig;
352
+ input?: ("text" | "image")[];
353
+ cost?: Partial<Model<Api>["cost"]>;
354
+ contextWindow?: number;
355
+ maxTokens?: number;
356
+ omitMaxOutputTokens?: boolean;
357
+ headers?: Record<string, string>;
358
+ compat?: ModelSpec<Api>["compat"];
359
+ contextPromotionTarget?: string;
360
+ premiumMultiplier?: number;
361
+ }
362
+
363
+ /**
364
+ * How a patch treats the base model's transport metadata (headers/compat):
365
+ * - `merge`: fold the patch into the base's (modelOverrides semantics).
366
+ * - `replace`: the patch owns transport wholesale — same-id custom definitions
367
+ * already folded provider-level headers/compat in during parsing, so bundled
368
+ * transport metadata must not be re-merged (see `#mergeCustomModels`).
369
+ */
370
+ type ModelTransportPolicy = "merge" | "replace";
371
+
372
+ function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
373
+ const result = { ...base };
374
+ if (patch.name !== undefined) result.name = patch.name;
375
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
376
+ if (patch.thinking !== undefined) result.thinking = patch.thinking;
377
+ if (patch.input !== undefined) result.input = patch.input;
378
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
379
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
380
+ if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
381
+ if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
382
+ if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
383
+ if (patch.cost) {
384
+ result.cost = {
385
+ input: patch.cost.input ?? base.cost.input,
386
+ output: patch.cost.output ?? base.cost.output,
387
+ cacheRead: patch.cost.cacheRead ?? base.cost.cacheRead,
388
+ cacheWrite: patch.cost.cacheWrite ?? base.cost.cacheWrite,
389
+ };
390
+ }
391
+ let compat: ModelSpec<Api>["compat"];
392
+ if (transport === "merge") {
393
+ if (patch.headers) {
394
+ result.headers = { ...base.headers, ...patch.headers };
395
+ }
396
+ compat = mergeCompat(base.compatConfig, patch.compat);
397
+ } else {
398
+ result.headers = patch.headers;
399
+ compat = patch.compat;
400
+ }
401
+ return buildModel({ ...result, compat } as ModelSpec<Api>);
402
+ }
403
+
404
+ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
405
+ return applyModelPatch(model, override as ModelPatch, "merge");
406
+ }
407
+
408
+ interface CustomModelDefinitionLike extends ModelPatch {
409
+ id: string;
410
+ api?: Api;
411
+ baseUrl?: string;
412
+ cost?: Model<Api>["cost"];
413
+ }
414
+
415
+ interface CustomModelBuildOptions {
416
+ useDefaults: boolean;
417
+ }
418
+
419
+ interface CustomModelOverlay extends ModelPatch {
420
+ id: string;
421
+ provider: string;
422
+ api: Api;
423
+ baseUrl: string;
424
+ cost?: Model<Api>["cost"];
425
+ isOAuth?: boolean;
426
+ }
427
+
428
+ function mergeCustomModelHeaders(
429
+ providerHeaders: Record<string, string> | undefined,
430
+ modelHeaders: Record<string, string> | undefined,
431
+ authHeader: boolean | undefined,
432
+ apiKeyConfig: string | undefined,
433
+ ): Record<string, string> | undefined {
434
+ const resolvedModelHeaders = resolveConfigHeaders(modelHeaders);
435
+ return mergeAuthHeader({ ...providerHeaders, ...resolvedModelHeaders }, authHeader, apiKeyConfig);
436
+ }
437
+
438
+ function mergeAuthHeader(
439
+ headers: Record<string, string> | undefined,
440
+ authHeader: boolean | undefined,
441
+ apiKeyConfig: string | undefined,
442
+ ): Record<string, string> | undefined {
443
+ const nextHeaders = headers && Object.keys(headers).length > 0 ? { ...headers } : undefined;
444
+ if (!authHeader || !apiKeyConfig) {
445
+ return nextHeaders;
446
+ }
447
+ const resolvedKey = resolveConfigValue(apiKeyConfig);
448
+ return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
449
+ }
450
+
451
+ /**
452
+ * Decide whether a custom-yaml model should force OAuth-style request shaping.
453
+ * - Explicit `auth: oauth` → force on.
454
+ * - Explicit `auth: apiKey` / `auth: none` → leave unset (auto-detect by key prefix).
455
+ * - No `auth` specified and `api: anthropic-messages` → default on. Custom Anthropic
456
+ * endpoints are typically Claude-Code-style proxies (e.g. CLIProxyAPI) that expect
457
+ * the cloaked request shape regardless of how the proxy itself is authenticated.
458
+ * - Otherwise → unset.
459
+ */
460
+ function resolveCustomModelIsOAuth(api: Api, providerAuth: ProviderAuthMode | undefined): boolean | undefined {
461
+ if (providerAuth === "oauth") return true;
462
+ if (providerAuth !== undefined) return undefined;
463
+ if (api === "anthropic-messages") return true;
464
+ return undefined;
465
+ }
466
+
467
+ function buildCustomModelOverlay(
468
+ providerName: string,
469
+ providerBaseUrl: string,
470
+ providerApi: Api | undefined,
471
+ providerHeaders: Record<string, string> | undefined,
472
+ providerApiKey: string | undefined,
473
+ authHeader: boolean | undefined,
474
+ providerCompat: ModelSpec<Api>["compat"] | undefined,
475
+ providerAuth: ProviderAuthMode | undefined,
476
+ modelDef: CustomModelDefinitionLike,
477
+ ): CustomModelOverlay | undefined {
478
+ const api = modelDef.api ?? providerApi;
479
+ if (!api) return undefined;
480
+ return {
481
+ id: modelDef.id,
482
+ provider: providerName,
483
+ api,
484
+ baseUrl: modelDef.baseUrl ?? providerBaseUrl,
485
+ name: modelDef.name,
486
+ reasoning: modelDef.reasoning,
487
+ thinking: modelDef.thinking,
488
+ input: modelDef.input,
489
+ cost: modelDef.cost,
490
+ contextWindow: modelDef.contextWindow,
491
+ maxTokens: modelDef.maxTokens,
492
+ omitMaxOutputTokens: modelDef.omitMaxOutputTokens,
493
+ headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
494
+ compat: mergeCompat(providerCompat, modelDef.compat),
495
+ contextPromotionTarget: modelDef.contextPromotionTarget,
496
+ premiumMultiplier: modelDef.premiumMultiplier,
497
+ isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
498
+ };
499
+ }
500
+
501
+ function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
502
+ if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
503
+ return model;
504
+ }
505
+ return { ...model, contextWindow: 1_000_000 };
506
+ }
507
+
508
+ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
509
+ const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
510
+ const reference = options.useDefaults
511
+ ? resolveModelReference(resolvedModel.id, getBundledModelReferenceIndex())
512
+ : undefined;
513
+ const cost =
514
+ resolvedModel.cost ??
515
+ reference?.cost ??
516
+ (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
517
+ const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
518
+ return buildModel({
519
+ id: resolvedModel.id,
520
+ name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
521
+ api: resolvedModel.api,
522
+ provider: resolvedModel.provider,
523
+ baseUrl: resolvedModel.baseUrl,
524
+ reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
525
+ thinking: resolvedModel.thinking ?? reference?.thinking,
526
+ input: input as ("text" | "image")[],
527
+ cost,
528
+ contextWindow:
529
+ resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
530
+ maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
531
+ headers: resolvedModel.headers,
532
+ omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
533
+ compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
534
+ contextPromotionTarget: resolvedModel.contextPromotionTarget,
535
+ premiumMultiplier: resolvedModel.premiumMultiplier,
536
+ isOAuth: resolvedModel.isOAuth,
537
+ } as ModelSpec<Api>);
538
+ }
539
+
540
+ function normalizeSuppressedSelector(selector: string): string {
541
+ const trimmed = selector.trim();
542
+ if (!trimmed) return trimmed;
543
+ const parsed = parseModelString(trimmed);
544
+ if (!parsed) return trimmed;
545
+ return `${parsed.provider}/${parsed.id}`;
546
+ }
547
+
548
+ function getDisabledProviderIdsFromSettings(): Set<string> {
549
+ try {
550
+ return new Set(settings.get("disabledProviders"));
551
+ } catch {
552
+ return new Set();
553
+ }
554
+ }
555
+
556
+ function getConfiguredProviderOrderFromSettings(): string[] {
557
+ try {
558
+ return settings.get("modelProviderOrder");
559
+ } catch {
560
+ return [];
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Model registry - loads and manages models, resolves API keys via AuthStorage.
566
+ */
567
+ export class ModelRegistry {
568
+ #models: Model<Api>[] = [];
569
+ #canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
570
+ #customProviderApiKeys: Map<string, string> = new Map();
571
+ #keylessProviders: Set<string> = new Set();
572
+ #discoverableProviders: DiscoveryProviderConfig[] = [];
573
+ #customModelOverlays: CustomModelOverlay[] = [];
574
+ #providerOverrides: Map<string, ProviderOverride> = new Map();
575
+ #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
576
+ #equivalenceConfig: ModelEquivalenceConfig | undefined;
577
+ #configError: ConfigError | undefined = undefined;
578
+ #modelsConfigFile: ConfigFile<ModelsConfig>;
579
+ #lastStaticLoadMtime: number | null = null;
580
+ #registeredProviderSources: Set<string> = new Set();
581
+ #providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
582
+ #cacheDbPath?: string;
583
+ #suppressedSelectors: Map<string, number> = new Map();
584
+ #backgroundRefresh?: Promise<void>;
585
+ #lastDiscoveryWarnings: Map<string, string> = new Map();
586
+ // Runtime extension model overlays — persist across refresh() cycles so that
587
+ // models registered by extensions survive the model selector's offline reload.
588
+ #runtimeModelOverlays: CustomModelOverlay[] = [];
589
+ #runtimeProviderApiKeys: Map<string, string> = new Map();
590
+ #runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
591
+ #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
592
+ #runtimeProviderSourceByName: Map<string, string> = new Map();
593
+ // Runtime model managers registered by extensions via fetchDynamicModels.
594
+ // Keyed by provider name; use the same SQLite cache path as builtins.
595
+ #runtimeModelManagers: Map<string, { options: ModelManagerOptions<Api>; sourceId: string }> = new Map();
596
+ #rebuildPending: boolean = false;
597
+ #rebuildSuspended: number = 0;
598
+ #fetch: FetchImpl;
599
+
600
+ #resolveCommandBackedApiKey(provider: string): CommandApiKeyResolution {
601
+ const keyConfig = this.#customProviderApiKeys.get(provider);
602
+ if (!isCommandConfigValue(keyConfig)) return { configured: false };
603
+ const value = resolveConfigValue(keyConfig);
604
+ if (value) {
605
+ this.authStorage.setConfigApiKey(provider, value);
606
+ return { configured: true, value };
607
+ }
608
+ this.authStorage.removeConfigApiKey(provider);
609
+ return { configured: true };
610
+ }
611
+
612
+ #installProviderApiKey(provider: string, keyConfig: string): void {
613
+ this.#customProviderApiKeys.set(provider, keyConfig);
614
+ const resolved = resolveConfigValue(keyConfig);
615
+ if (resolved) {
616
+ this.authStorage.setConfigApiKey(provider, resolved);
617
+ } else if (isCommandConfigValue(keyConfig)) {
618
+ this.authStorage.removeConfigApiKey(provider);
619
+ }
620
+ }
621
+
622
+ /**
623
+ * @param authStorage - Auth storage for API key resolution
624
+ *
625
+ * Sync constructor — eagerly loads bundled + cached models so tests and
626
+ * synchronous callers see a fully-populated registry immediately. Production
627
+ * boot paths SHOULD prefer {@link ModelRegistry.create} so the YAML/JSONC
628
+ * migration step lands off the event loop's hot path before the first
629
+ * `tryLoad()` runs.
630
+ */
631
+ constructor(
632
+ readonly authStorage: AuthStorage,
633
+ modelsPath?: string,
634
+ options?: { fetch?: FetchImpl },
635
+ ) {
636
+ this.#fetch = options?.fetch ?? fetch;
637
+ this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
638
+ this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
639
+ // Set up fallback resolver for custom provider API keys
640
+ this.authStorage.setFallbackResolver(provider => {
641
+ const keyConfig = this.#customProviderApiKeys.get(provider);
642
+ if (!keyConfig) return undefined;
643
+ return resolveConfigValue(keyConfig);
644
+ });
645
+ // Load models synchronously in constructor.
646
+ this.#loadModels();
647
+ }
648
+
649
+ /**
650
+ * Reload models from disk (built-in + custom from models.json).
651
+ */
652
+ async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
653
+ this.#suspendRebuild();
654
+ try {
655
+ this.#reloadStaticModels();
656
+ this.#suppressedSelectors.clear();
657
+ await this.#refreshRuntimeDiscoveries(strategy);
658
+ } finally {
659
+ this.#resumeRebuild();
660
+ }
661
+ }
662
+
663
+ refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
664
+ if (this.#backgroundRefresh) {
665
+ return;
666
+ }
667
+ const refreshPromise = this.refresh(strategy)
668
+ .catch(error => {
669
+ logger.warn("background model refresh failed", {
670
+ error: error instanceof Error ? error.message : String(error),
671
+ });
672
+ })
673
+ .finally(() => {
674
+ if (this.#backgroundRefresh === refreshPromise) {
675
+ this.#backgroundRefresh = undefined;
676
+ }
677
+ });
678
+ this.#backgroundRefresh = refreshPromise;
679
+ }
680
+
681
+ async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
682
+ this.#suspendRebuild();
683
+ try {
684
+ this.#reloadStaticModels();
685
+ for (const selector of this.#suppressedSelectors.keys()) {
686
+ if (selector.startsWith(`${providerId}/`)) {
687
+ this.#suppressedSelectors.delete(selector);
688
+ }
689
+ }
690
+ await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
691
+ } finally {
692
+ this.#resumeRebuild();
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Discover models for providers registered at runtime via `fetchDynamicModels`
698
+ * (extension providers). Merges the discovered catalog into the existing model
699
+ * set without reloading static models, so dynamically-discovered models from
700
+ * other providers are preserved. No-op when no runtime providers are registered.
701
+ *
702
+ * Drives the same SQLite model cache as built-in providers, so the default
703
+ * `online-if-uncached` strategy fetches at most once per cache TTL (24 h).
704
+ */
705
+ async refreshRuntimeProviders(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
706
+ if (this.#runtimeModelManagers.size === 0) {
707
+ return;
708
+ }
709
+ this.#suspendRebuild();
710
+ try {
711
+ await this.#refreshRuntimeDiscoveries(strategy, new Set(this.#runtimeModelManagers.keys()));
712
+ } finally {
713
+ this.#resumeRebuild();
714
+ }
715
+ }
716
+
717
+ #reloadStaticModels(): void {
718
+ const currentMtime = this.#modelsConfigFile.getMtimeMs();
719
+ if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
720
+ // models.json unchanged since last load; reload + canonical rebuild would be redundant.
721
+ return;
722
+ }
723
+ this.#modelsConfigFile.invalidate();
724
+ this.#customProviderApiKeys.clear();
725
+ this.#keylessProviders.clear();
726
+ this.#discoverableProviders = [];
727
+ // Drop config-sourced apiKeys from AuthStorage before reload; entries
728
+ // removed from models.yml must actually disappear from the resolver, not
729
+ // linger from the previous parse. The post-load setters below repopulate.
730
+ this.authStorage.clearConfigApiKeys();
731
+ // Restore runtime API keys before #loadModels — survives because
732
+ // #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
733
+ for (const [k, v] of this.#runtimeProviderApiKeys) {
734
+ this.#installProviderApiKey(k, v);
735
+ }
736
+ this.#providerOverrides.clear();
737
+ this.#modelOverrides.clear();
738
+ this.#equivalenceConfig = undefined;
739
+ this.#configError = undefined;
740
+ this.#providerDiscoveryStates.clear();
741
+ this.#loadModels();
742
+ }
743
+
744
+ /**
745
+ * Get any error from loading models.json (undefined if no error).
746
+ */
747
+ getError(): ConfigError | undefined {
748
+ return this.#configError;
749
+ }
750
+
751
+ #loadModels() {
752
+ // Load custom models from models.json first (to know which providers to override)
753
+ const {
754
+ models: customModels = [],
755
+ overrides = new Map(),
756
+ modelOverrides = new Map(),
757
+ keylessProviders = new Set(),
758
+ discoverableProviders = [],
759
+ configuredProviders = new Set(),
760
+ equivalence,
761
+ error: configError,
762
+ } = this.#loadCustomModels();
763
+ this.#configError = configError;
764
+ this.#keylessProviders = keylessProviders;
765
+ this.#discoverableProviders = discoverableProviders;
766
+ this.#customModelOverlays = customModels;
767
+ this.#providerOverrides = overrides;
768
+ this.#modelOverrides = modelOverrides;
769
+ this.#equivalenceConfig = equivalence;
770
+
771
+ this.#addImplicitDiscoverableProviders(configuredProviders);
772
+ let builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
773
+ const cachedStandardResult = this.#loadCachedStandardProviderModels();
774
+ const cachedStandardModels = this.#applyHardcodedModelPolicies(cachedStandardResult.models);
775
+ const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
776
+ // Only drop bundled fallback models when the cached project-catalog row is
777
+ // itself fresh AND authoritative. A stale or non-authoritative snapshot
778
+ // (e.g. after ADC discovery failure rewrote the row with authoritative=0)
779
+ // must not strip bundled Vertex Gemini entries — that would leave only the
780
+ // stale project-scoped rows in API-key-only environments.
781
+ const cachedAuthoritativeProviders = new Set<string>();
782
+ for (const provider of providersWithAuthoritativeProjectCatalog(cachedStandardModels)) {
783
+ if (cachedStandardResult.authoritativeFreshProviders.has(provider)) {
784
+ cachedAuthoritativeProviders.add(provider);
785
+ }
786
+ }
787
+ for (const provider of cachedStandardResult.authoritativeFreshProviders) {
788
+ if (AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS.has(provider)) {
789
+ cachedAuthoritativeProviders.add(provider);
790
+ }
791
+ }
792
+ if (cachedAuthoritativeProviders.size > 0) {
793
+ builtInModels = dropProviderModels(builtInModels, cachedAuthoritativeProviders);
794
+ }
795
+ const resolvedDefaults = this.#mergeResolvedModels(
796
+ this.#mergeResolvedModels(builtInModels, cachedStandardModels),
797
+ cachedDiscoveries,
798
+ );
799
+ const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
800
+ // Merge runtime extension models so they survive refresh() cycles
801
+ const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
802
+ const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
803
+ this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
804
+ this.#rebuildCanonicalIndex();
805
+ this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
806
+ }
807
+
808
+ /** Load built-in models, applying provider-level overrides only.
809
+ * Per-model overrides are applied later by #applyModelOverrides. */
810
+ #loadBuiltInModels(overrides: Map<string, ProviderOverride>): Model<Api>[] {
811
+ return getBundledProviders().flatMap(provider => {
812
+ const models = getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[];
813
+ const providerOverride = overrides.get(provider);
814
+
815
+ return models.map(m => {
816
+ if (!providerOverride) return m;
817
+ const withTransportOverride = this.#applyProviderTransportOverride(m, providerOverride);
818
+ return buildModel({
819
+ ...withTransportOverride,
820
+ compat: mergeCompat(m.compatConfig, providerOverride.compat),
821
+ } as ModelSpec<Api>);
822
+ });
823
+ });
824
+ }
825
+
826
+ #mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
827
+ return mergeByModelKey(baseModels, replacementModels, (existing, replacementModel) => {
828
+ if (!existing) return replacementModel;
829
+ return {
830
+ ...replacementModel,
831
+ contextWindow:
832
+ replacementModel.contextWindow === UNK_CONTEXT_WINDOW
833
+ ? existing.contextWindow
834
+ : replacementModel.contextWindow,
835
+ maxTokens: replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
836
+ };
837
+ });
838
+ }
839
+
840
+ /** Merge custom models with built-in, replacing by provider+id match */
841
+ #mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
842
+ return mergeByModelKey(builtInModels, customModels, (existingModel, customModel) => {
843
+ if (!existingModel) return finalizeCustomModel(customModel, { useDefaults: true });
844
+ // Same-id custom definitions replace bundled transport behavior, so the
845
+ // patch is applied with the `replace` transport policy.
846
+ return applyModelPatch(
847
+ {
848
+ ...existingModel,
849
+ id: customModel.id,
850
+ provider: customModel.provider,
851
+ api: customModel.api,
852
+ baseUrl: customModel.baseUrl,
853
+ },
854
+ customModel,
855
+ "replace",
856
+ );
857
+ });
858
+ }
859
+
860
+ #loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
861
+ const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
862
+ const cachedModels: Model<Api>[] = [];
863
+ const authoritativeFreshProviders = new Set<string>();
864
+ for (const providerId of STARTUP_MODEL_CACHE_PROVIDER_IDS) {
865
+ if (configuredDiscoveryProviders.has(providerId)) {
866
+ continue;
867
+ }
868
+ const cache = readModelCache<Api>(providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
869
+ if (!cache) {
870
+ continue;
871
+ }
872
+ if (cache.fresh && cache.authoritative) {
873
+ authoritativeFreshProviders.add(providerId);
874
+ }
875
+ const models = cache.models.map(model =>
876
+ model.provider === providerId ? model : { ...model, provider: providerId },
877
+ );
878
+ const providerOverride = this.#providerOverrides.get(providerId);
879
+ const withTransport = providerOverride
880
+ ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
881
+ : models;
882
+ const withCompat = providerOverride?.compat
883
+ ? withTransport.map(model =>
884
+ buildModel({
885
+ ...model,
886
+ compat: mergeCompat(model.compat, providerOverride.compat),
887
+ } as ModelSpec<Api>),
888
+ )
889
+ : withTransport.map(model => buildModel(model));
890
+ cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
891
+ }
892
+ return { models: cachedModels, authoritativeFreshProviders };
893
+ }
894
+
895
+ #loadCachedDiscoverableModels(): Model<Api>[] {
896
+ const cachedModels: Model<Api>[] = [];
897
+ for (const providerConfig of this.#discoverableProviders) {
898
+ const cache = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
899
+ if (!cache) {
900
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
901
+ provider: providerConfig.provider,
902
+ status: "idle",
903
+ optional: providerConfig.optional ?? false,
904
+ stale: false,
905
+ models: [],
906
+ });
907
+ continue;
908
+ }
909
+ const models = this.#applyProviderModelOverrides(
910
+ providerConfig.provider,
911
+ this.#normalizeDiscoverableModels(
912
+ providerConfig,
913
+ this.#applyProviderCompat(
914
+ providerConfig.compat,
915
+ cache.models.map(model => buildModel(model)),
916
+ ),
917
+ ),
918
+ );
919
+ cachedModels.push(...models);
920
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
921
+ provider: providerConfig.provider,
922
+ status: "cached",
923
+ optional: providerConfig.optional ?? false,
924
+ stale: !cache.fresh || !cache.authoritative,
925
+ fetchedAt: cache.updatedAt,
926
+ models: models.map(model => model.id),
927
+ });
928
+ }
929
+ return cachedModels;
930
+ }
931
+
932
+ #applyProviderCompat(compat: ModelSpec<Api>["compat"] | undefined, models: Model<Api>[]): Model<Api>[] {
933
+ if (!compat) return models;
934
+ return models.map(model =>
935
+ buildModel({ ...model, compat: mergeCompat(model.compatConfig, compat) } as ModelSpec<Api>),
936
+ );
937
+ }
938
+
939
+ #normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
940
+ if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
941
+ return models;
942
+ }
943
+
944
+ const contextLengthOverride = getOllamaContextLengthOverride();
945
+ return models.map(model => {
946
+ const normalized =
947
+ model.api === "openai-completions"
948
+ ? buildModel({
949
+ ...model,
950
+ api: "openai-responses" as const,
951
+ compat: model.compatConfig,
952
+ } as ModelSpec<Api>)
953
+ : model;
954
+ if (contextLengthOverride === undefined) {
955
+ return normalized;
956
+ }
957
+ return {
958
+ ...normalized,
959
+ contextWindow: contextLengthOverride,
960
+ maxTokens: Math.min(contextLengthOverride, DISCOVERY_DEFAULT_MAX_TOKENS),
961
+ };
962
+ });
963
+ }
964
+
965
+ #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
966
+ const disabledProviders = getDisabledProviderIdsFromSettings();
967
+ if (!configuredProviders.has("ollama") && !disabledProviders.has("ollama")) {
968
+ this.#discoverableProviders.push({
969
+ provider: "ollama",
970
+ api: "openai-responses",
971
+ baseUrl: getImplicitOllamaBaseUrl(),
972
+ discovery: { type: "ollama" },
973
+ optional: true,
974
+ });
975
+ this.#keylessProviders.add("ollama");
976
+ }
977
+ if (!configuredProviders.has("llama.cpp") && !disabledProviders.has("llama.cpp")) {
978
+ this.#discoverableProviders.push({
979
+ provider: "llama.cpp",
980
+ api: "openai-responses",
981
+ baseUrl: Bun.env.LLAMA_CPP_BASE_URL || "http://127.0.0.1:8080",
982
+ discovery: { type: "llama.cpp" },
983
+ optional: true,
984
+ });
985
+ // Only mark as keyless if no API key is configured
986
+ if (!this.authStorage.hasAuth("llama.cpp")) {
987
+ this.#keylessProviders.add("llama.cpp");
988
+ }
989
+ }
990
+ if (!configuredProviders.has("lm-studio") && !disabledProviders.has("lm-studio")) {
991
+ this.#discoverableProviders.push({
992
+ provider: "lm-studio",
993
+ api: "openai-completions",
994
+ baseUrl: Bun.env.LM_STUDIO_BASE_URL || "http://127.0.0.1:1234/v1",
995
+ discovery: { type: "lm-studio" },
996
+ optional: true,
997
+ });
998
+ this.#keylessProviders.add("lm-studio");
999
+ }
1000
+ }
1001
+
1002
+ #loadCustomModels(): CustomModelsResult {
1003
+ const { value, error, status } = this.#modelsConfigFile.tryLoad();
1004
+
1005
+ if (status === "error") {
1006
+ return {
1007
+ models: [],
1008
+ overrides: new Map(),
1009
+ modelOverrides: new Map(),
1010
+ keylessProviders: new Set(),
1011
+ discoverableProviders: [],
1012
+ configuredProviders: new Set(),
1013
+ error,
1014
+ found: true,
1015
+ };
1016
+ } else if (status === "not-found") {
1017
+ return {
1018
+ models: [],
1019
+ overrides: new Map(),
1020
+ modelOverrides: new Map(),
1021
+ keylessProviders: new Set(),
1022
+ discoverableProviders: [],
1023
+ configuredProviders: new Set(),
1024
+ found: false,
1025
+ };
1026
+ }
1027
+
1028
+ const overrides = new Map<string, ProviderOverride>();
1029
+ const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
1030
+ const keylessProviders = new Set<string>();
1031
+ const discoverableProviders: DiscoveryProviderConfig[] = [];
1032
+ const providerEntries = Object.entries(value.providers ?? {});
1033
+ const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1034
+
1035
+ for (const [providerName, providerConfig] of providerEntries) {
1036
+ const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
1037
+ // Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
1038
+ if (
1039
+ providerConfig.baseUrl ||
1040
+ resolvedProviderHeaders ||
1041
+ providerConfig.apiKey ||
1042
+ providerConfig.authHeader !== undefined ||
1043
+ providerConfig.compat ||
1044
+ providerConfig.disableStrictTools ||
1045
+ providerConfig.transport
1046
+ ) {
1047
+ const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
1048
+ overrides.set(providerName, {
1049
+ baseUrl: providerConfig.baseUrl,
1050
+ headers: resolvedProviderHeaders,
1051
+ apiKey: providerConfig.apiKey,
1052
+ authHeader: providerConfig.authHeader,
1053
+ compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1054
+ transport: providerConfig.transport,
1055
+ });
1056
+ }
1057
+
1058
+ const authMode = (providerConfig.auth ?? "apiKey") as ProviderAuthMode;
1059
+ if (authMode === "none") {
1060
+ keylessProviders.add(providerName);
1061
+ }
1062
+
1063
+ if (providerConfig.discovery && (providerConfig.api || providerConfig.discovery.type === "proxy")) {
1064
+ const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
1065
+ discoverableProviders.push({
1066
+ provider: providerName,
1067
+ // Proxy discovery derives per-model api from /v1/models's
1068
+ // supported_endpoint_types; the provider-level api is only a
1069
+ // fallback for entries that don't advertise one.
1070
+ api: (providerConfig.api ?? "openai-completions") as Api,
1071
+ baseUrl: providerConfig.baseUrl,
1072
+ headers: resolvedProviderHeaders,
1073
+ compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1074
+ discovery: providerConfig.discovery,
1075
+ optional: false,
1076
+ });
1077
+ }
1078
+
1079
+ // Store API key for fallback resolver AND register as config override
1080
+ // so it wins over OAuth tokens from the broker — when the user pins a
1081
+ // bearer in models.yml (e.g. for an auth-gateway baseUrl), that bearer
1082
+ // must authenticate the outbound request.
1083
+ if (providerConfig.apiKey) {
1084
+ this.#installProviderApiKey(providerName, providerConfig.apiKey);
1085
+ }
1086
+
1087
+ // Parse per-model overrides
1088
+ if (providerConfig.modelOverrides) {
1089
+ const perModel = new Map<string, ModelOverride>();
1090
+ for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
1091
+ perModel.set(
1092
+ modelId,
1093
+ override.headers ? { ...override, headers: resolveConfigHeaders(override.headers) } : override,
1094
+ );
1095
+ }
1096
+ allModelOverrides.set(providerName, perModel);
1097
+ }
1098
+ }
1099
+
1100
+ return {
1101
+ models: this.#parseModels(value),
1102
+ overrides,
1103
+ modelOverrides: allModelOverrides,
1104
+ keylessProviders,
1105
+ discoverableProviders,
1106
+ configuredProviders,
1107
+ equivalence: value.equivalence,
1108
+ found: true,
1109
+ };
1110
+ }
1111
+
1112
+ async #refreshRuntimeDiscoveries(
1113
+ strategy: ModelRefreshStrategy,
1114
+ providerFilter?: ReadonlySet<string>,
1115
+ ): Promise<void> {
1116
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1117
+ const selectedDiscoverableProviders = (
1118
+ providerFilter
1119
+ ? this.#discoverableProviders.filter(provider => providerFilter.has(provider.provider))
1120
+ : this.#discoverableProviders
1121
+ ).filter(provider => !disabledProviders.has(provider.provider));
1122
+ const configuredDiscoveriesPromise =
1123
+ selectedDiscoverableProviders.length === 0
1124
+ ? Promise.resolve<Model<Api>[]>([])
1125
+ : Promise.all(
1126
+ selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
1127
+ ).then(results => results.flat());
1128
+ const [configuredDiscovered, builtInDiscovery] = await Promise.all([
1129
+ configuredDiscoveriesPromise,
1130
+ this.#discoverBuiltInProviderModels(strategy, providerFilter),
1131
+ ]);
1132
+ const discovered = [...configuredDiscovered, ...builtInDiscovery.models];
1133
+ if (discovered.length === 0 && builtInDiscovery.authoritativeProviders.size === 0) {
1134
+ return;
1135
+ }
1136
+ const discoveredModels = this.#applyHardcodedModelPolicies(
1137
+ discovered.map(model =>
1138
+ mergeDiscoveredModel(
1139
+ model,
1140
+ this.find(model.provider, model.id),
1141
+ this.#providerOverrides.get(model.provider),
1142
+ ),
1143
+ ),
1144
+ );
1145
+ const authoritativeProviders = providersWithAuthoritativeProjectCatalog(discoveredModels);
1146
+ for (const provider of builtInDiscovery.authoritativeProviders) {
1147
+ authoritativeProviders.add(provider);
1148
+ }
1149
+ const baseModels =
1150
+ authoritativeProviders.size > 0 ? dropProviderModels(this.#models, authoritativeProviders) : this.#models;
1151
+ const resolved = this.#mergeResolvedModels(baseModels, discoveredModels);
1152
+ const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1153
+ // Merge runtime extension models so they survive online discovery completion
1154
+ const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1155
+ const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
1156
+ this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1157
+ this.#rebuildCanonicalIndex();
1158
+ }
1159
+
1160
+ async #discoverProviderModels(
1161
+ providerConfig: DiscoveryProviderConfig,
1162
+ strategy: ModelRefreshStrategy,
1163
+ ): Promise<Model<Api>[]> {
1164
+ const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1165
+ const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1166
+ if (requiresAuth) {
1167
+ const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
1168
+ if (!isAuthenticated(apiKey)) {
1169
+ this.#providerDiscoveryStates.set(providerConfig.provider, {
1170
+ provider: providerConfig.provider,
1171
+ status: "unauthenticated",
1172
+ optional: providerConfig.optional ?? false,
1173
+ stale: cached !== null,
1174
+ fetchedAt: cached?.updatedAt,
1175
+ models: cached?.models.map(model => model.id) ?? [],
1176
+ });
1177
+ this.#lastDiscoveryWarnings.delete(providerConfig.provider);
1178
+ return cached ? cached.models.map(model => buildModel(model)) : [];
1179
+ }
1180
+ }
1181
+
1182
+ const providerId = providerConfig.provider;
1183
+ let discoveryError: string | undefined;
1184
+ const fetchDynamicModels = async (): Promise<readonly ModelSpec<Api>[] | null> => {
1185
+ try {
1186
+ const models = this.#applyProviderModelOverrides(
1187
+ providerId,
1188
+ await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
1189
+ );
1190
+ this.#lastDiscoveryWarnings.delete(providerId);
1191
+ return models.map(toModelSpec);
1192
+ } catch (error) {
1193
+ discoveryError = error instanceof Error ? error.message : String(error);
1194
+ return null;
1195
+ }
1196
+ };
1197
+
1198
+ const manager = createModelManager<Api>({
1199
+ providerId,
1200
+ staticModels: [],
1201
+ cacheDbPath: this.#cacheDbPath,
1202
+ cacheTtlMs: 24 * 60 * 60 * 1000,
1203
+ fetchDynamicModels,
1204
+ });
1205
+ const result = await manager.refresh(strategy);
1206
+ const status = discoveryError
1207
+ ? result.models.length > 0
1208
+ ? "cached"
1209
+ : "unavailable"
1210
+ : strategy === "offline"
1211
+ ? cached
1212
+ ? "cached"
1213
+ : "idle"
1214
+ : result.models.length > 0
1215
+ ? "ok"
1216
+ : "empty";
1217
+ this.#providerDiscoveryStates.set(providerId, {
1218
+ provider: providerId,
1219
+ status,
1220
+ optional: providerConfig.optional ?? false,
1221
+ stale: result.stale || status === "cached",
1222
+ fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
1223
+ models: result.models.map(model => model.id),
1224
+ error: discoveryError,
1225
+ });
1226
+ if (discoveryError) {
1227
+ this.#warnProviderDiscoveryFailure(providerConfig, discoveryError);
1228
+ }
1229
+ return this.#applyProviderModelOverrides(
1230
+ providerId,
1231
+ this.#normalizeDiscoverableModels(
1232
+ providerConfig,
1233
+ this.#applyProviderCompat(providerConfig.compat, result.models),
1234
+ ),
1235
+ );
1236
+ }
1237
+
1238
+ #discoveryContext(): DiscoveryContext {
1239
+ return {
1240
+ fetch: this.#fetch,
1241
+ getBearerApiKey: async provider => {
1242
+ const apiKey = await this.getApiKeyForProvider(provider);
1243
+ return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
1244
+ },
1245
+ };
1246
+ }
1247
+
1248
+ #warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
1249
+ const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
1250
+ if (previous === error) {
1251
+ return;
1252
+ }
1253
+ this.#lastDiscoveryWarnings.set(providerConfig.provider, error);
1254
+ logger.warn("model discovery failed for provider", {
1255
+ provider: providerConfig.provider,
1256
+ url: providerConfig.baseUrl,
1257
+ error,
1258
+ });
1259
+ }
1260
+
1261
+ async #discoverBuiltInProviderModels(
1262
+ strategy: ModelRefreshStrategy,
1263
+ providerFilter?: ReadonlySet<string>,
1264
+ ): Promise<BuiltInDiscoveryResult> {
1265
+ // Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
1266
+ const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
1267
+ const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
1268
+ if (configuredDiscoveryProviders.has(opts.providerId)) {
1269
+ return false;
1270
+ }
1271
+ return providerFilter ? providerFilter.has(opts.providerId) : true;
1272
+ });
1273
+ if (managerOptions.length === 0) {
1274
+ return { models: [], authoritativeProviders: new Set() };
1275
+ }
1276
+ const discoveries = await Promise.all(
1277
+ managerOptions.map(options => this.#discoverWithModelManager(options, strategy)),
1278
+ );
1279
+ const authoritativeProviders = new Set<string>();
1280
+ const models: Model<Api>[] = [];
1281
+ for (const discovery of discoveries) {
1282
+ models.push(...discovery.models);
1283
+ for (const provider of discovery.authoritativeProviders) {
1284
+ authoritativeProviders.add(provider);
1285
+ }
1286
+ }
1287
+ return { models, authoritativeProviders };
1288
+ }
1289
+
1290
+ async #collectBuiltInModelManagerOptions(): Promise<ModelManagerOptions<Api>[]> {
1291
+ const specialProviderDescriptors: Array<{
1292
+ providerId: string;
1293
+ resolveKey: (value: string | undefined) => string | undefined;
1294
+ createOptions: (key: string) => ModelManagerOptions<Api>;
1295
+ }> = [
1296
+ {
1297
+ providerId: "google-antigravity",
1298
+ resolveKey: extractGoogleOAuthToken,
1299
+ createOptions: oauthToken =>
1300
+ googleAntigravityModelManagerOptions({
1301
+ oauthToken,
1302
+ endpoint: this.getProviderBaseUrl("google-antigravity"),
1303
+ fetch: this.#fetch,
1304
+ }),
1305
+ },
1306
+ {
1307
+ providerId: "google-gemini-cli",
1308
+ resolveKey: extractGoogleOAuthToken,
1309
+ createOptions: oauthToken =>
1310
+ googleGeminiCliModelManagerOptions({
1311
+ oauthToken,
1312
+ endpoint: this.getProviderBaseUrl("google-gemini-cli"),
1313
+ fetch: this.#fetch,
1314
+ }),
1315
+ },
1316
+ {
1317
+ providerId: "openai-codex",
1318
+ resolveKey: value => value,
1319
+ createOptions: accessToken => {
1320
+ const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
1321
+ return openaiCodexModelManagerOptions({
1322
+ accessToken,
1323
+ accountId,
1324
+ });
1325
+ },
1326
+ },
1327
+ ];
1328
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1329
+ const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(
1330
+ descriptor => !disabledProviders.has(descriptor.providerId),
1331
+ );
1332
+ const enabledSpecialProviderDescriptors = specialProviderDescriptors.filter(
1333
+ descriptor => !disabledProviders.has(descriptor.providerId),
1334
+ );
1335
+ // Use peekApiKey to avoid OAuth token refresh during discovery.
1336
+ // The token is only needed if the dynamic fetch fires (cache miss),
1337
+ // and failures there are handled gracefully.
1338
+ const peekKey = (descriptor: { providerId: string }) => this.#peekApiKeyForProvider(descriptor.providerId);
1339
+ const [standardProviderKeys, specialKeys] = await Promise.all([
1340
+ Promise.all(standardProviderDescriptors.map(peekKey)),
1341
+ Promise.all(enabledSpecialProviderDescriptors.map(peekKey)),
1342
+ ]);
1343
+ const options: ModelManagerOptions<Api>[] = [];
1344
+ for (let i = 0; i < standardProviderDescriptors.length; i++) {
1345
+ const descriptor = standardProviderDescriptors[i];
1346
+ const apiKey = standardProviderKeys[i];
1347
+ if (isAuthenticated(apiKey) || descriptor.allowUnauthenticated) {
1348
+ options.push(
1349
+ descriptor.createModelManagerOptions({
1350
+ apiKey: isAuthenticated(apiKey) ? apiKey : undefined,
1351
+ baseUrl: this.getProviderBaseUrl(descriptor.providerId),
1352
+ fetch: this.#fetch,
1353
+ }),
1354
+ );
1355
+ }
1356
+ }
1357
+
1358
+ for (let i = 0; i < enabledSpecialProviderDescriptors.length; i++) {
1359
+ const descriptor = enabledSpecialProviderDescriptors[i];
1360
+ const key = descriptor.resolveKey(specialKeys[i]);
1361
+ if (!isAuthenticated(key)) {
1362
+ continue;
1363
+ }
1364
+ options.push(descriptor.createOptions(key));
1365
+ }
1366
+ // Append runtime model managers registered by extensions via fetchDynamicModels.
1367
+ for (const { options: managerOpts } of this.#runtimeModelManagers.values()) {
1368
+ options.push(managerOpts);
1369
+ }
1370
+ return options;
1371
+ }
1372
+
1373
+ async #discoverWithModelManager(
1374
+ options: ModelManagerOptions<Api>,
1375
+ strategy: ModelRefreshStrategy,
1376
+ ): Promise<BuiltInDiscoveryResult> {
1377
+ try {
1378
+ const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
1379
+ const result = await manager.refresh(strategy);
1380
+ const models = result.models.map(model =>
1381
+ model.provider === options.providerId ? model : { ...model, provider: options.providerId },
1382
+ );
1383
+ const authoritativeProviders = new Set<string>();
1384
+ if (options.dynamicModelsAuthoritative && !result.stale) {
1385
+ authoritativeProviders.add(options.providerId);
1386
+ }
1387
+ return { models, authoritativeProviders };
1388
+ } catch (error) {
1389
+ logger.warn("model discovery failed for provider", {
1390
+ provider: options.providerId,
1391
+ error: error instanceof Error ? error.message : String(error),
1392
+ });
1393
+ return { models: [], authoritativeProviders: new Set() };
1394
+ }
1395
+ }
1396
+
1397
+ #applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
1398
+ const overrides = this.#modelOverrides.get(provider);
1399
+ if (!overrides || overrides.size === 0) return models;
1400
+ return models.map(model => {
1401
+ const override = overrides.get(model.id);
1402
+ if (!override) return model;
1403
+ return applyModelOverride(model, override);
1404
+ });
1405
+ }
1406
+
1407
+ #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
1408
+ return {
1409
+ baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
1410
+ apiKey: override.apiKey ?? baseOverride?.apiKey,
1411
+ authHeader: override.authHeader ?? baseOverride?.authHeader,
1412
+ headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
1413
+ compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
1414
+ transport: override.transport ?? baseOverride?.transport,
1415
+ };
1416
+ }
1417
+ #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
1418
+ entry: T,
1419
+ override: Pick<ProviderOverride, "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport">,
1420
+ ): T {
1421
+ const headers = mergeAuthHeader(
1422
+ override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
1423
+ override.authHeader,
1424
+ override.apiKey,
1425
+ );
1426
+ return {
1427
+ ...entry,
1428
+ baseUrl: override.baseUrl ?? entry.baseUrl,
1429
+ headers,
1430
+ // Preserve the model's existing transport when the override omits one;
1431
+ // providers without a `transport` field keep the default per-API dispatch.
1432
+ ...(override.transport !== undefined ? { transport: override.transport } : {}),
1433
+ };
1434
+ }
1435
+ #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
1436
+ if (this.#runtimeProviderOverrides.size === 0) return models;
1437
+ return models.map(model => {
1438
+ const override = this.#runtimeProviderOverrides.get(model.provider);
1439
+ if (!override) return model;
1440
+ return this.#applyProviderTransportOverride(model, override);
1441
+ });
1442
+ }
1443
+ #applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
1444
+ if (overrides.size === 0) return models;
1445
+ return models.map(model => {
1446
+ const providerOverrides = overrides.get(model.provider);
1447
+ if (!providerOverrides) return model;
1448
+ const override = providerOverrides.get(model.id);
1449
+ if (!override) return model;
1450
+ return applyModelOverride(model, override);
1451
+ });
1452
+ }
1453
+ #applyHardcodedModelPolicies(models: Model<Api>[]): Model<Api>[] {
1454
+ return models.map(model => {
1455
+ if (model.id !== "gpt-5.4" || model.provider === "github-copilot") {
1456
+ return model;
1457
+ }
1458
+ const overrides = this.#modelOverrides.get(model.provider)?.get(model.id);
1459
+ if (!overrides) {
1460
+ return applyModelOverride(model, { contextWindow: 1_000_000 });
1461
+ }
1462
+ return applyModelOverride(model, {
1463
+ contextWindow: overrides.contextWindow ?? 1_000_000,
1464
+ ...overrides,
1465
+ });
1466
+ });
1467
+ }
1468
+
1469
+ #rebuildCanonicalIndex(): void {
1470
+ if (this.#rebuildSuspended > 0) {
1471
+ this.#rebuildPending = true;
1472
+ return;
1473
+ }
1474
+ this.#canonicalIndex = buildCanonicalModelIndex(
1475
+ this.#models,
1476
+ getBundledCanonicalReferenceData(),
1477
+ this.#equivalenceConfig,
1478
+ );
1479
+ this.#rebuildPending = false;
1480
+ }
1481
+
1482
+ #suspendRebuild(): void {
1483
+ this.#rebuildSuspended += 1;
1484
+ }
1485
+
1486
+ #resumeRebuild(): void {
1487
+ if (this.#rebuildSuspended > 0) {
1488
+ this.#rebuildSuspended -= 1;
1489
+ }
1490
+ if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
1491
+ this.#rebuildPending = false;
1492
+ this.#canonicalIndex = buildCanonicalModelIndex(
1493
+ this.#models,
1494
+ getBundledCanonicalReferenceData(),
1495
+ this.#equivalenceConfig,
1496
+ );
1497
+ }
1498
+ }
1499
+
1500
+ #parseModels(config: ModelsConfig): CustomModelOverlay[] {
1501
+ const models: CustomModelOverlay[] = [];
1502
+
1503
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1504
+ const modelDefs = providerConfig.models ?? [];
1505
+ if (modelDefs.length === 0) continue; // Override-only, no custom models
1506
+ const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
1507
+ if (providerConfig.apiKey) {
1508
+ this.#installProviderApiKey(providerName, providerConfig.apiKey);
1509
+ }
1510
+ for (const modelDef of modelDefs) {
1511
+ const providerCompat = providerConfig.disableStrictTools
1512
+ ? mergeCompat(providerConfig.compat, { disableStrictTools: true })
1513
+ : providerConfig.compat;
1514
+ const model = buildCustomModelOverlay(
1515
+ providerName,
1516
+ providerConfig.baseUrl!,
1517
+ providerConfig.api as Api | undefined,
1518
+ resolvedProviderHeaders,
1519
+ providerConfig.apiKey,
1520
+ providerConfig.authHeader,
1521
+ providerCompat,
1522
+ (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
1523
+ modelDef as CustomModelDefinitionLike,
1524
+ );
1525
+ if (!model) continue;
1526
+ models.push(model);
1527
+ }
1528
+ }
1529
+ return models;
1530
+ }
1531
+
1532
+ /**
1533
+ * Get all models (built-in + custom).
1534
+ * If models.json had errors, returns only built-in models.
1535
+ */
1536
+ getAll(): Model<Api>[] {
1537
+ return this.#models;
1538
+ }
1539
+
1540
+ /**
1541
+ * Availability predicate with per-provider memoization. Auth lookups
1542
+ * (`authStorage.hasAuth`) and the disabled-provider set are resolved once
1543
+ * per provider instead of once per model, which matters when filtering the
1544
+ * full bundled catalog (thousands of models, ~50 providers).
1545
+ */
1546
+ #createAvailabilityCheck(): (model: Model<Api>) => boolean {
1547
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1548
+ const byProvider = new Map<string, boolean>();
1549
+ return model => {
1550
+ let available = byProvider.get(model.provider);
1551
+ if (available === undefined) {
1552
+ available =
1553
+ !disabledProviders.has(model.provider) &&
1554
+ (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider));
1555
+ byProvider.set(model.provider, available);
1556
+ }
1557
+ return available;
1558
+ };
1559
+ }
1560
+
1561
+ /**
1562
+ * Build the shared per-query filter state for canonical model queries.
1563
+ * Hoisted out of the per-record loop: building the candidate-selector set
1564
+ * and availability memo once per query instead of once per record is what
1565
+ * keeps `getCanonicalModelSelections` linear instead of O(records × candidates).
1566
+ */
1567
+ #canonicalQueryFilters(options: CanonicalModelQueryOptions | undefined): {
1568
+ candidateKeys: Set<string> | undefined;
1569
+ isAvailable: ((model: Model<Api>) => boolean) | undefined;
1570
+ } {
1571
+ return {
1572
+ candidateKeys: options?.candidates
1573
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
1574
+ : undefined,
1575
+ isAvailable: options?.availableOnly ? this.#createAvailabilityCheck() : undefined,
1576
+ };
1577
+ }
1578
+
1579
+ #filterCanonicalVariants(
1580
+ record: CanonicalModelRecord,
1581
+ candidateKeys: ReadonlySet<string> | undefined,
1582
+ isAvailable: ((model: Model<Api>) => boolean) | undefined,
1583
+ ): CanonicalModelVariant[] {
1584
+ return record.variants.filter(variant => {
1585
+ if (candidateKeys && !candidateKeys.has(variant.selector)) {
1586
+ return false;
1587
+ }
1588
+ if (isAvailable && !isAvailable(variant.model)) {
1589
+ return false;
1590
+ }
1591
+ return true;
1592
+ });
1593
+ }
1594
+
1595
+ #variantPreferences(candidates: readonly Model<Api>[]): CanonicalVariantPreferences {
1596
+ return {
1597
+ modelOrder: buildCanonicalModelOrder(candidates),
1598
+ providerRank: buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings()),
1599
+ };
1600
+ }
1601
+
1602
+ getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
1603
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
1604
+ const records: CanonicalModelRecord[] = [];
1605
+ for (const record of this.#canonicalIndex.records) {
1606
+ const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
1607
+ if (variants.length === 0) {
1608
+ continue;
1609
+ }
1610
+ records.push({
1611
+ id: record.id,
1612
+ name: record.name,
1613
+ variants,
1614
+ });
1615
+ }
1616
+ return records;
1617
+ }
1618
+
1619
+ /**
1620
+ * One-pass equivalent of `getCanonicalModels` + `resolveCanonicalModel` per
1621
+ * record. The per-query state (candidate-selector set, availability memo,
1622
+ * provider rank, candidate order) is built once, so the whole catalog
1623
+ * resolves in O(records + candidates) instead of O(records × candidates).
1624
+ * This is the path the model selector hydrates from synchronously on open.
1625
+ */
1626
+ getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
1627
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
1628
+ const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
1629
+ const preferences = this.#variantPreferences(candidates);
1630
+ const selections: CanonicalModelSelection[] = [];
1631
+ for (const record of this.#canonicalIndex.records) {
1632
+ const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
1633
+ if (variants.length === 0) {
1634
+ continue;
1635
+ }
1636
+ const resolved = resolveCanonicalVariant(variants, preferences);
1637
+ if (!resolved) {
1638
+ continue;
1639
+ }
1640
+ selections.push({
1641
+ record: { id: record.id, name: record.name, variants },
1642
+ model: resolved.model,
1643
+ });
1644
+ }
1645
+ return selections;
1646
+ }
1647
+
1648
+ getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
1649
+ const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
1650
+ if (!record) {
1651
+ return [];
1652
+ }
1653
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
1654
+ return this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
1655
+ }
1656
+
1657
+ resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
1658
+ const variants = this.getCanonicalVariants(canonicalId, options);
1659
+ if (variants.length === 0) {
1660
+ return undefined;
1661
+ }
1662
+ const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
1663
+ return resolveCanonicalVariant(variants, this.#variantPreferences(candidates))?.model;
1664
+ }
1665
+
1666
+ getCanonicalId(model: Model<Api>): string | undefined {
1667
+ return this.#canonicalIndex.bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
1668
+ }
1669
+
1670
+ /**
1671
+ * Get only models that have auth configured.
1672
+ * This is a fast check that doesn't refresh OAuth tokens.
1673
+ */
1674
+ getAvailable(): Model<Api>[] {
1675
+ return this.#models.filter(this.#createAvailabilityCheck());
1676
+ }
1677
+
1678
+ /**
1679
+ * Check whether auth is configured for a model's provider.
1680
+ *
1681
+ * Mirrors the upstream `@mariozechner/pi-coding-agent` API surface so that
1682
+ * external plugins/extensions and downstream wrappers (e.g. subagent launch
1683
+ * paths that pre-flight auth before model resolution) can probe a model
1684
+ * without resolving an API key. Returns true for keyless providers as well
1685
+ * as providers with stored credentials. See issue #993.
1686
+ */
1687
+ hasConfiguredAuth(model: Model<Api>): boolean {
1688
+ const commandKey = this.#resolveCommandBackedApiKey(model.provider);
1689
+ return (
1690
+ commandKey.configured || this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider)
1691
+ );
1692
+ }
1693
+
1694
+ getDiscoverableProviders(): string[] {
1695
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1696
+ return this.#discoverableProviders
1697
+ .filter(provider => !disabledProviders.has(provider.provider))
1698
+ .map(provider => provider.provider);
1699
+ }
1700
+
1701
+ getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
1702
+ return this.#providerDiscoveryStates.get(provider);
1703
+ }
1704
+
1705
+ /**
1706
+ * Find a model by provider and ID.
1707
+ */
1708
+ find(provider: string, modelId: string): Model<Api> | undefined {
1709
+ return resolveProviderModelReference(provider, modelId, this.#models);
1710
+ }
1711
+
1712
+ /**
1713
+ * Get the base URL associated with a provider, if any model defines one.
1714
+ */
1715
+ getProviderBaseUrl(provider: string): string | undefined {
1716
+ return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
1717
+ }
1718
+
1719
+ /**
1720
+ * Get API key for a model.
1721
+ */
1722
+ async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
1723
+ const commandKey = this.#resolveCommandBackedApiKey(model.provider);
1724
+ if (commandKey.configured) return commandKey.value;
1725
+ if (this.#keylessProviders.has(model.provider) && !this.authStorage.hasAuth(model.provider)) {
1726
+ return kNoAuth;
1727
+ }
1728
+ return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl, modelId: model.id });
1729
+ }
1730
+
1731
+ /**
1732
+ * Get API key for a provider (e.g., "openai").
1733
+ *
1734
+ * `options.forceRefresh` powers step (b) of the auth-retry policy — it
1735
+ * re-mints the session-sticky OAuth token even when the cached copy still
1736
+ * looks valid. `options.signal` is threaded into any broker-bound refresh.
1737
+ */
1738
+ async getApiKeyForProvider(
1739
+ provider: string,
1740
+ sessionId?: string,
1741
+ options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
1742
+ ): Promise<string | undefined> {
1743
+ const commandKey = this.#resolveCommandBackedApiKey(provider);
1744
+ if (commandKey.configured) return commandKey.value;
1745
+ if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
1746
+ return kNoAuth;
1747
+ }
1748
+ return this.authStorage.getApiKey(provider, sessionId, {
1749
+ baseUrl: options?.baseUrl,
1750
+ modelId: options?.modelId,
1751
+ forceRefresh: options?.forceRefresh,
1752
+ signal: options?.signal,
1753
+ });
1754
+ }
1755
+
1756
+ /**
1757
+ * Build an {@link ApiKeyResolver} for this provider, implementing the
1758
+ * central a/b/c auth-retry policy. Callers that need the initial key for
1759
+ * a guard can call `resolveApiKeyOnce(resolver)`.
1760
+ */
1761
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver {
1762
+ return createApiKeyResolver(this, provider, options);
1763
+ }
1764
+
1765
+ async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
1766
+ const commandKey = this.#resolveCommandBackedApiKey(provider);
1767
+ if (commandKey.configured) return commandKey.value;
1768
+ if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
1769
+ return kNoAuth;
1770
+ }
1771
+ return this.authStorage.peekApiKey(provider);
1772
+ }
1773
+
1774
+ /**
1775
+ * Check if a model is using OAuth credentials (subscription).
1776
+ */
1777
+ isUsingOAuth(model: Model<Api>): boolean {
1778
+ return this.authStorage.hasOAuth(model.provider);
1779
+ }
1780
+
1781
+ #clearRuntimeProviderState(providerName: string): void {
1782
+ this.#runtimeProviderApiKeys.delete(providerName);
1783
+ this.#runtimeProviderOverrides.delete(providerName);
1784
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
1785
+ this.#runtimeModelManagers.delete(providerName);
1786
+ this.authStorage.removeConfigApiKey(providerName);
1787
+ }
1788
+
1789
+ /**
1790
+ * Remove custom API/OAuth registrations for a specific extension source.
1791
+ */
1792
+ clearSourceRegistrations(sourceId: string): void {
1793
+ unregisterCustomApis(sourceId);
1794
+ unregisterOAuthProviders(sourceId);
1795
+ const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
1796
+ if (!sourceProviders || sourceProviders.size === 0) {
1797
+ return;
1798
+ }
1799
+ this.#runtimeProvidersBySource.delete(sourceId);
1800
+ for (const providerName of sourceProviders) {
1801
+ if (this.#runtimeProviderSourceByName.get(providerName) !== sourceId) {
1802
+ continue;
1803
+ }
1804
+ this.#runtimeProviderSourceByName.delete(providerName);
1805
+ this.#clearRuntimeProviderState(providerName);
1806
+ }
1807
+ this.#lastStaticLoadMtime = null;
1808
+ this.#reloadStaticModels();
1809
+ this.#rebuildCanonicalIndex();
1810
+ }
1811
+
1812
+ /**
1813
+ * Remove registrations for extension sources that are no longer active.
1814
+ */
1815
+ syncExtensionSources(activeSourceIds: string[]): void {
1816
+ const activeSources = new Set(activeSourceIds);
1817
+ for (const sourceId of this.#registeredProviderSources) {
1818
+ if (activeSources.has(sourceId)) {
1819
+ continue;
1820
+ }
1821
+ this.clearSourceRegistrations(sourceId);
1822
+ this.#registeredProviderSources.delete(sourceId);
1823
+ }
1824
+ }
1825
+
1826
+ /**
1827
+ * Register a provider dynamically (from extensions).
1828
+ *
1829
+ * If provider has models: replaces all existing models for this provider.
1830
+ * If provider has only baseUrl/headers: overrides existing models' URLs.
1831
+ * If provider has streamSimple: registers a custom API streaming function.
1832
+ * If provider has oauth: registers OAuth provider for /login support.
1833
+ */
1834
+ registerProvider(providerName: string, config: ProviderConfigInput, sourceId?: string): void {
1835
+ if (config.streamSimple && !config.api) {
1836
+ throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
1837
+ }
1838
+
1839
+ validateProviderConfiguration(
1840
+ providerName,
1841
+ {
1842
+ baseUrl: config.baseUrl,
1843
+ headers: config.headers,
1844
+ apiKey: config.apiKey,
1845
+ api: config.api,
1846
+ oauthConfigured: Boolean(config.oauth),
1847
+ models: (config.models ?? []) as ProviderValidationModel[],
1848
+ },
1849
+ "runtime-register",
1850
+ );
1851
+
1852
+ if (config.streamSimple && config.api) {
1853
+ const streamSimple = config.streamSimple;
1854
+ registerCustomApi(config.api, streamSimple, sourceId, (model, context, options) =>
1855
+ streamSimple(model, context, options as SimpleStreamOptions),
1856
+ );
1857
+ }
1858
+
1859
+ if (config.oauth) {
1860
+ registerOAuthProvider({
1861
+ ...config.oauth,
1862
+ id: providerName,
1863
+ sourceId,
1864
+ });
1865
+ }
1866
+
1867
+ let sourceHandoff = false;
1868
+ if (sourceId) {
1869
+ this.#registeredProviderSources.add(sourceId);
1870
+ const previousSourceId = this.#runtimeProviderSourceByName.get(providerName);
1871
+ if (previousSourceId && previousSourceId !== sourceId) {
1872
+ const previousProviders = this.#runtimeProvidersBySource.get(previousSourceId);
1873
+ previousProviders?.delete(providerName);
1874
+ if (previousProviders && previousProviders.size === 0) {
1875
+ this.#runtimeProvidersBySource.delete(previousSourceId);
1876
+ }
1877
+ this.#clearRuntimeProviderState(providerName);
1878
+ sourceHandoff = true;
1879
+ }
1880
+ const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set<string>();
1881
+ sourceProviders.add(providerName);
1882
+ this.#runtimeProvidersBySource.set(sourceId, sourceProviders);
1883
+ this.#runtimeProviderSourceByName.set(providerName, sourceId);
1884
+ }
1885
+ if (sourceHandoff) {
1886
+ this.#lastStaticLoadMtime = null;
1887
+ this.#reloadStaticModels();
1888
+ }
1889
+
1890
+ if (config.apiKey) {
1891
+ this.#installProviderApiKey(providerName, config.apiKey);
1892
+ // Persist runtime API keys so they survive #reloadStaticModels() cycles
1893
+ this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
1894
+ }
1895
+
1896
+ if (config.models && config.models.length > 0) {
1897
+ // Build model overlays that persist across refresh() cycles
1898
+ const newOverlays: CustomModelOverlay[] = [];
1899
+ for (const modelDef of config.models) {
1900
+ const overlay = buildCustomModelOverlay(
1901
+ providerName,
1902
+ config.baseUrl!,
1903
+ config.api,
1904
+ config.headers,
1905
+ config.apiKey,
1906
+ config.authHeader,
1907
+ config.compat,
1908
+ undefined,
1909
+ modelDef as CustomModelDefinitionLike,
1910
+ );
1911
+ if (!overlay) {
1912
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
1913
+ }
1914
+ newOverlays.push(overlay);
1915
+ }
1916
+ // Store as runtime overlays so they survive #reloadStaticModels()
1917
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
1918
+ this.#runtimeModelOverlays.push(...newOverlays);
1919
+
1920
+ // Also update #models immediately for the current cycle
1921
+ const nextModels = this.#models.filter(m => m.provider !== providerName);
1922
+ for (const overlay of newOverlays) {
1923
+ nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
1924
+ }
1925
+ const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
1926
+ const withRuntimeTransportOverride = runtimeTransportOverride
1927
+ ? nextModels.map(model => {
1928
+ if (model.provider !== providerName) return model;
1929
+ return this.#applyProviderTransportOverride(model, runtimeTransportOverride);
1930
+ })
1931
+ : nextModels;
1932
+
1933
+ if (config.oauth?.modifyModels) {
1934
+ const credential = this.authStorage.getOAuthCredential(providerName);
1935
+ if (credential) {
1936
+ this.#models = config.oauth.modifyModels(withRuntimeTransportOverride, credential);
1937
+ this.#rebuildCanonicalIndex();
1938
+ return;
1939
+ }
1940
+ }
1941
+
1942
+ this.#models = withRuntimeTransportOverride;
1943
+ this.#rebuildCanonicalIndex();
1944
+ return;
1945
+ }
1946
+
1947
+ if (config.fetchDynamicModels) {
1948
+ const fetcher = config.fetchDynamicModels;
1949
+ const providerBaseUrl = config.baseUrl ?? "";
1950
+ const providerApi = config.api;
1951
+ const providerHeaders = config.headers;
1952
+ const providerApiKey = config.apiKey;
1953
+ const providerAuthHeader = config.authHeader;
1954
+ const providerCompat = config.compat;
1955
+ const managerOptions: ModelManagerOptions<Api> = {
1956
+ providerId: providerName as Parameters<typeof createModelManager>[0]["providerId"],
1957
+ staticModels: [],
1958
+ cacheDbPath: this.#cacheDbPath,
1959
+ cacheTtlMs: 24 * 60 * 60 * 1000,
1960
+ dynamicModelsAuthoritative: true,
1961
+ fetchDynamicModels: async () => {
1962
+ const apiKey = await this.#peekApiKeyForProvider(providerName);
1963
+ const resolvedKey = isAuthenticated(apiKey) ? apiKey : undefined;
1964
+ const modelDefs = await fetcher(resolvedKey);
1965
+ const results: Model<Api>[] = [];
1966
+ for (const modelDef of modelDefs) {
1967
+ const overlay = buildCustomModelOverlay(
1968
+ providerName,
1969
+ modelDef.baseUrl ?? providerBaseUrl,
1970
+ modelDef.api ?? providerApi,
1971
+ providerHeaders,
1972
+ providerApiKey,
1973
+ providerAuthHeader,
1974
+ providerCompat,
1975
+ undefined,
1976
+ modelDef as CustomModelDefinitionLike,
1977
+ );
1978
+ if (overlay) results.push(finalizeCustomModel(overlay, { useDefaults: true }));
1979
+ }
1980
+ return results.map(toModelSpec);
1981
+ },
1982
+ };
1983
+ this.#runtimeModelManagers.set(providerName, { options: managerOptions, sourceId: sourceId ?? "" });
1984
+ // Discovery is driven by refreshRuntimeProviders() after the drain — not
1985
+ // here, so registration has no network side effect and callers can await.
1986
+ }
1987
+
1988
+ if (
1989
+ config.baseUrl ||
1990
+ config.headers ||
1991
+ config.apiKey ||
1992
+ config.authHeader !== undefined ||
1993
+ config.transport !== undefined
1994
+ ) {
1995
+ const transportOverride = {
1996
+ baseUrl: config.baseUrl,
1997
+ headers: config.headers,
1998
+ apiKey: config.apiKey,
1999
+ authHeader: config.authHeader,
2000
+ transport: config.transport,
2001
+ };
2002
+ const nextRuntimeOverride = this.#mergeProviderOverride(
2003
+ this.#runtimeProviderOverrides.get(providerName),
2004
+ transportOverride,
2005
+ );
2006
+ this.#runtimeProviderOverrides.set(providerName, nextRuntimeOverride);
2007
+ this.#models = this.#models.map(m => {
2008
+ if (m.provider !== providerName) return m;
2009
+ return this.#applyProviderTransportOverride(m, transportOverride);
2010
+ });
2011
+ this.#rebuildCanonicalIndex();
2012
+ }
2013
+ }
2014
+
2015
+ /**
2016
+ * Suppress a specific model selector (e.g., "provider/id") until a specific timestamp.
2017
+ */
2018
+ suppressSelector(selector: string, untilMs: number): void {
2019
+ this.#suppressedSelectors.set(normalizeSuppressedSelector(selector), untilMs);
2020
+ }
2021
+
2022
+ /**
2023
+ * Check if a model selector is currently suppressed due to rate limits.
2024
+ */
2025
+ isSelectorSuppressed(selector: string): boolean {
2026
+ const normalizedSelector = normalizeSuppressedSelector(selector);
2027
+ const suppressedUntil = this.#suppressedSelectors.get(normalizedSelector);
2028
+ if (!suppressedUntil) return false;
2029
+ if (suppressedUntil <= Date.now()) {
2030
+ this.#suppressedSelectors.delete(normalizedSelector);
2031
+ return false;
2032
+ }
2033
+ return true;
2034
+ }
2035
+
2036
+ /**
2037
+ * Clear all cooldown suppressions recorded via {@link suppressSelector}.
2038
+ * Used to reset retry-fallback cooldown state without a full {@link refresh}.
2039
+ */
2040
+ clearSuppressedSelectors(): void {
2041
+ this.#suppressedSelectors.clear();
2042
+ }
2043
+ }
2044
+
2045
+ /**
2046
+ * Input type for registerProvider API (from extensions).
2047
+ */
2048
+ export interface ProviderConfigInput {
2049
+ baseUrl?: string;
2050
+ apiKey?: string;
2051
+ api?: Api;
2052
+ streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
2053
+ headers?: Record<string, string>;
2054
+ compat?: ModelSpec<Api>["compat"];
2055
+ authHeader?: boolean;
2056
+ /** Streaming transport override — see {@link Model.transport}. */
2057
+ transport?: Model<Api>["transport"];
2058
+ oauth?: {
2059
+ name: string;
2060
+ login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
2061
+ refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
2062
+ getApiKey?(credentials: OAuthCredentials): string;
2063
+ modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
2064
+ };
2065
+ /**
2066
+ * Async factory that fetches the live model list from the provider endpoint.
2067
+ * When present, the result is run through the same SQLite model-cache as
2068
+ * built-in providers (keyed by provider name, default 24 h TTL).
2069
+ * The factory receives the resolved API key (undefined when unauthenticated).
2070
+ */
2071
+ fetchDynamicModels?: (
2072
+ apiKey: string | undefined,
2073
+ ) => Promise<readonly NonNullable<ProviderConfigInput["models"]>[number][]>;
2074
+ models?: Array<{
2075
+ id: string;
2076
+ name: string;
2077
+ api?: Api;
2078
+ baseUrl?: string;
2079
+ reasoning: boolean;
2080
+ thinking?: ThinkingConfig;
2081
+ input: ("text" | "image")[];
2082
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
2083
+ contextWindow: number;
2084
+ maxTokens: number;
2085
+ headers?: Record<string, string>;
2086
+ compat?: ModelSpec<Api>["compat"];
2087
+ contextPromotionTarget?: string;
2088
+ premiumMultiplier?: number;
2089
+ }>;
2090
+ }