@oh-my-pi/pi-coding-agent 0.1.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Model registry - manages built-in and custom models, provides API key resolution.
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import {
7
+ type Api,
8
+ getGitHubCopilotBaseUrl,
9
+ getModels,
10
+ getProviders,
11
+ type KnownProvider,
12
+ type Model,
13
+ normalizeDomain,
14
+ } from "@oh-my-pi/pi-ai";
15
+ import { type Static, Type } from "@sinclair/typebox";
16
+ import AjvModule from "ajv";
17
+ import type { AuthStorage } from "./auth-storage";
18
+ import { logger } from "./logger";
19
+
20
+ const Ajv = (AjvModule as any).default || AjvModule;
21
+
22
+ // Schema for OpenAI compatibility settings
23
+ const OpenAICompatSchema = Type.Object({
24
+ supportsStore: Type.Optional(Type.Boolean()),
25
+ supportsDeveloperRole: Type.Optional(Type.Boolean()),
26
+ supportsReasoningEffort: Type.Optional(Type.Boolean()),
27
+ maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
28
+ });
29
+
30
+ // Schema for custom model definition
31
+ const ModelDefinitionSchema = Type.Object({
32
+ id: Type.String({ minLength: 1 }),
33
+ name: Type.String({ minLength: 1 }),
34
+ api: Type.Optional(
35
+ Type.Union([
36
+ Type.Literal("openai-completions"),
37
+ Type.Literal("openai-responses"),
38
+ Type.Literal("anthropic-messages"),
39
+ Type.Literal("google-generative-ai"),
40
+ ]),
41
+ ),
42
+ reasoning: Type.Boolean(),
43
+ input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
44
+ cost: Type.Object({
45
+ input: Type.Number(),
46
+ output: Type.Number(),
47
+ cacheRead: Type.Number(),
48
+ cacheWrite: Type.Number(),
49
+ }),
50
+ contextWindow: Type.Number(),
51
+ maxTokens: Type.Number(),
52
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
53
+ compat: Type.Optional(OpenAICompatSchema),
54
+ });
55
+
56
+ const ProviderConfigSchema = Type.Object({
57
+ baseUrl: Type.String({ minLength: 1 }),
58
+ apiKey: Type.String({ minLength: 1 }),
59
+ api: Type.Optional(
60
+ Type.Union([
61
+ Type.Literal("openai-completions"),
62
+ Type.Literal("openai-responses"),
63
+ Type.Literal("anthropic-messages"),
64
+ Type.Literal("google-generative-ai"),
65
+ ]),
66
+ ),
67
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
68
+ authHeader: Type.Optional(Type.Boolean()),
69
+ models: Type.Array(ModelDefinitionSchema),
70
+ });
71
+
72
+ const ModelsConfigSchema = Type.Object({
73
+ providers: Type.Record(Type.String(), ProviderConfigSchema),
74
+ });
75
+
76
+ type ModelsConfig = Static<typeof ModelsConfigSchema>;
77
+
78
+ /**
79
+ * Resolve an API key config value to an actual key.
80
+ * Checks environment variable first, then treats as literal.
81
+ */
82
+ function resolveApiKeyConfig(keyConfig: string): string | undefined {
83
+ const envValue = process.env[keyConfig];
84
+ if (envValue) return envValue;
85
+ return keyConfig;
86
+ }
87
+
88
+ /**
89
+ * Model registry - loads and manages models, resolves API keys via AuthStorage.
90
+ */
91
+ export class ModelRegistry {
92
+ private models: Model<Api>[] = [];
93
+ private customProviderApiKeys: Map<string, string> = new Map();
94
+ private loadError: string | undefined = undefined;
95
+
96
+ /**
97
+ * @param authStorage - Auth storage for API key resolution
98
+ * @param modelsJsonPath - Primary path for models.json
99
+ * @param fallbackPaths - Additional paths to check (legacy support)
100
+ */
101
+ constructor(
102
+ readonly authStorage: AuthStorage,
103
+ private modelsJsonPath: string | undefined = undefined,
104
+ private fallbackPaths: string[] = [],
105
+ ) {
106
+ // Set up fallback resolver for custom provider API keys
107
+ this.authStorage.setFallbackResolver((provider) => {
108
+ const keyConfig = this.customProviderApiKeys.get(provider);
109
+ if (keyConfig) {
110
+ return resolveApiKeyConfig(keyConfig);
111
+ }
112
+ return undefined;
113
+ });
114
+
115
+ // Load models
116
+ this.loadModels();
117
+ }
118
+
119
+ /**
120
+ * Reload models from disk (built-in + custom from models.json).
121
+ */
122
+ refresh(): void {
123
+ this.customProviderApiKeys.clear();
124
+ this.loadError = undefined;
125
+ this.loadModels();
126
+ }
127
+
128
+ /**
129
+ * Get any error from loading models.json (undefined if no error).
130
+ */
131
+ getError(): string | undefined {
132
+ return this.loadError;
133
+ }
134
+
135
+ private loadModels(): void {
136
+ // Load built-in models
137
+ const builtInModels: Model<Api>[] = [];
138
+ for (const provider of getProviders()) {
139
+ const providerModels = getModels(provider as KnownProvider);
140
+ builtInModels.push(...(providerModels as Model<Api>[]));
141
+ }
142
+
143
+ // Load custom models from models.json (check primary path, then fallbacks)
144
+ let customModels: Model<Api>[] = [];
145
+ const pathsToCheck = this.modelsJsonPath ? [this.modelsJsonPath, ...this.fallbackPaths] : this.fallbackPaths;
146
+
147
+ if (pathsToCheck.length > 0) {
148
+ logger.debug("ModelRegistry.loadModels checking paths", { paths: pathsToCheck });
149
+ }
150
+
151
+ for (const modelsPath of pathsToCheck) {
152
+ if (existsSync(modelsPath)) {
153
+ logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
154
+ const result = this.loadCustomModels(modelsPath);
155
+ if (result.error) {
156
+ this.loadError = result.error;
157
+ // Keep built-in models even if custom models failed to load
158
+ } else {
159
+ customModels = result.models;
160
+ }
161
+ break; // Use first existing file
162
+ }
163
+ }
164
+
165
+ const combined = [...builtInModels, ...customModels];
166
+
167
+ // Update github-copilot base URL based on OAuth credentials
168
+ const copilotCred = this.authStorage.get("github-copilot");
169
+ if (copilotCred?.type === "oauth") {
170
+ const domain = copilotCred.enterpriseUrl
171
+ ? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
172
+ : undefined;
173
+ const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
174
+ this.models = combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
175
+ } else {
176
+ this.models = combined;
177
+ }
178
+ }
179
+
180
+ private loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | undefined } {
181
+ if (!existsSync(modelsJsonPath)) {
182
+ return { models: [], error: undefined };
183
+ }
184
+
185
+ try {
186
+ const content = readFileSync(modelsJsonPath, "utf-8");
187
+ const config: ModelsConfig = JSON.parse(content);
188
+
189
+ // Validate schema
190
+ const ajv = new Ajv();
191
+ const validate = ajv.compile(ModelsConfigSchema);
192
+ if (!validate(config)) {
193
+ const errors =
194
+ validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
195
+ "Unknown schema error";
196
+ return {
197
+ models: [],
198
+ error: `Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`,
199
+ };
200
+ }
201
+
202
+ // Additional validation
203
+ this.validateConfig(config);
204
+
205
+ // Parse models
206
+ return { models: this.parseModels(config), error: undefined };
207
+ } catch (error) {
208
+ if (error instanceof SyntaxError) {
209
+ return {
210
+ models: [],
211
+ error: `Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`,
212
+ };
213
+ }
214
+ return {
215
+ models: [],
216
+ error: `Failed to load models.json: ${
217
+ error instanceof Error ? error.message : error
218
+ }\n\nFile: ${modelsJsonPath}`,
219
+ };
220
+ }
221
+ }
222
+
223
+ private validateConfig(config: ModelsConfig): void {
224
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
225
+ const hasProviderApi = !!providerConfig.api;
226
+
227
+ for (const modelDef of providerConfig.models) {
228
+ const hasModelApi = !!modelDef.api;
229
+
230
+ if (!hasProviderApi && !hasModelApi) {
231
+ throw new Error(
232
+ `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
233
+ );
234
+ }
235
+
236
+ if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
237
+ if (!modelDef.name) throw new Error(`Provider ${providerName}: model missing "name"`);
238
+ if (modelDef.contextWindow <= 0)
239
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
240
+ if (modelDef.maxTokens <= 0)
241
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
242
+ }
243
+ }
244
+ }
245
+
246
+ private parseModels(config: ModelsConfig): Model<Api>[] {
247
+ const models: Model<Api>[] = [];
248
+
249
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
250
+ // Store API key config for fallback resolver
251
+ this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
252
+
253
+ for (const modelDef of providerConfig.models) {
254
+ const api = modelDef.api || providerConfig.api;
255
+ if (!api) continue;
256
+
257
+ // Merge headers: provider headers are base, model headers override
258
+ let headers =
259
+ providerConfig.headers || modelDef.headers
260
+ ? { ...providerConfig.headers, ...modelDef.headers }
261
+ : undefined;
262
+
263
+ // If authHeader is true, add Authorization header with resolved API key
264
+ if (providerConfig.authHeader) {
265
+ const resolvedKey = resolveApiKeyConfig(providerConfig.apiKey);
266
+ if (resolvedKey) {
267
+ headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
268
+ }
269
+ }
270
+
271
+ models.push({
272
+ id: modelDef.id,
273
+ name: modelDef.name,
274
+ api: api as Api,
275
+ provider: providerName,
276
+ baseUrl: providerConfig.baseUrl,
277
+ reasoning: modelDef.reasoning,
278
+ input: modelDef.input as ("text" | "image")[],
279
+ cost: modelDef.cost,
280
+ contextWindow: modelDef.contextWindow,
281
+ maxTokens: modelDef.maxTokens,
282
+ headers,
283
+ compat: modelDef.compat,
284
+ } as Model<Api>);
285
+ }
286
+ }
287
+
288
+ return models;
289
+ }
290
+
291
+ /**
292
+ * Get all models (built-in + custom).
293
+ * If models.json had errors, returns only built-in models.
294
+ */
295
+ getAll(): Model<Api>[] {
296
+ return this.models;
297
+ }
298
+
299
+ /**
300
+ * Get only models that have valid API keys available.
301
+ */
302
+ async getAvailable(): Promise<Model<Api>[]> {
303
+ const available: Model<Api>[] = [];
304
+ for (const model of this.models) {
305
+ const apiKey = await this.authStorage.getApiKey(model.provider);
306
+ if (apiKey) {
307
+ available.push(model);
308
+ }
309
+ }
310
+ return available;
311
+ }
312
+
313
+ /**
314
+ * Find a model by provider and ID.
315
+ */
316
+ find(provider: string, modelId: string): Model<Api> | undefined {
317
+ return this.models.find((m) => m.provider === provider && m.id === modelId) ?? undefined;
318
+ }
319
+
320
+ /**
321
+ * Get API key for a model.
322
+ */
323
+ async getApiKey(model: Model<Api>): Promise<string | undefined> {
324
+ return this.authStorage.getApiKey(model.provider);
325
+ }
326
+
327
+ /**
328
+ * Check if a model is using OAuth credentials (subscription).
329
+ */
330
+ isUsingOAuth(model: Model<Api>): boolean {
331
+ const cred = this.authStorage.get(model.provider);
332
+ return cred?.type === "oauth";
333
+ }
334
+ }