jonsoc 1.1.50 → 1.1.51

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 (420) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +18 -0
  3. package/PUBLISHING_GUIDE.md +151 -0
  4. package/README.md +58 -0
  5. package/bin/jonsoc +256 -256
  6. package/bunfig.toml +7 -0
  7. package/package.json +142 -8
  8. package/package.json.placeholder +11 -0
  9. package/parsers-config.ts +253 -0
  10. package/script/build.ts +115 -0
  11. package/script/publish-registries.ts +197 -0
  12. package/script/publish.ts +149 -0
  13. package/script/schema.ts +47 -0
  14. package/script/seed-e2e.ts +50 -0
  15. package/src/acp/README.md +164 -0
  16. package/src/acp/agent.ts +1437 -0
  17. package/src/acp/session.ts +105 -0
  18. package/src/acp/types.ts +22 -0
  19. package/src/agent/agent.ts +345 -0
  20. package/src/agent/generate.txt +75 -0
  21. package/src/agent/prompt/compaction.txt +12 -0
  22. package/src/agent/prompt/explore.txt +18 -0
  23. package/src/agent/prompt/summary.txt +11 -0
  24. package/src/agent/prompt/title.txt +44 -0
  25. package/src/auth/index.ts +73 -0
  26. package/src/brand/index.ts +89 -0
  27. package/src/bun/index.ts +139 -0
  28. package/src/bus/bus-event.ts +43 -0
  29. package/src/bus/global.ts +10 -0
  30. package/src/bus/index.ts +105 -0
  31. package/src/cli/bootstrap.ts +17 -0
  32. package/src/cli/cmd/acp.ts +69 -0
  33. package/src/cli/cmd/agent.ts +257 -0
  34. package/src/cli/cmd/auth.ts +405 -0
  35. package/src/cli/cmd/cmd.ts +7 -0
  36. package/src/cli/cmd/debug/agent.ts +166 -0
  37. package/src/cli/cmd/debug/config.ts +16 -0
  38. package/src/cli/cmd/debug/file.ts +97 -0
  39. package/src/cli/cmd/debug/index.ts +48 -0
  40. package/src/cli/cmd/debug/lsp.ts +52 -0
  41. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  42. package/src/cli/cmd/debug/scrap.ts +16 -0
  43. package/src/cli/cmd/debug/skill.ts +16 -0
  44. package/src/cli/cmd/debug/snapshot.ts +52 -0
  45. package/src/cli/cmd/export.ts +88 -0
  46. package/src/cli/cmd/generate.ts +38 -0
  47. package/src/cli/cmd/github.ts +1547 -0
  48. package/src/cli/cmd/import.ts +99 -0
  49. package/src/cli/cmd/mcp.ts +765 -0
  50. package/src/cli/cmd/models.ts +77 -0
  51. package/src/cli/cmd/pr.ts +112 -0
  52. package/src/cli/cmd/run.ts +395 -0
  53. package/src/cli/cmd/serve.ts +20 -0
  54. package/src/cli/cmd/session.ts +135 -0
  55. package/src/cli/cmd/stats.ts +402 -0
  56. package/src/cli/cmd/tui/app.tsx +923 -0
  57. package/src/cli/cmd/tui/attach.ts +39 -0
  58. package/src/cli/cmd/tui/component/border.tsx +21 -0
  59. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  60. package/src/cli/cmd/tui/component/dialog-command.tsx +162 -0
  61. package/src/cli/cmd/tui/component/dialog-error-log.tsx +155 -0
  62. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  63. package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
  64. package/src/cli/cmd/tui/component/dialog-provider.tsx +256 -0
  65. package/src/cli/cmd/tui/component/dialog-session-list.tsx +114 -0
  66. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  67. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  68. package/src/cli/cmd/tui/component/dialog-status.tsx +164 -0
  69. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  70. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  71. package/src/cli/cmd/tui/component/dynamic-layout.tsx +86 -0
  72. package/src/cli/cmd/tui/component/inspector-overlay.tsx +247 -0
  73. package/src/cli/cmd/tui/component/logo.tsx +88 -0
  74. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +653 -0
  75. package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
  76. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  77. package/src/cli/cmd/tui/component/prompt/index.tsx +1347 -0
  78. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  79. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  80. package/src/cli/cmd/tui/component/tips.tsx +153 -0
  81. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  82. package/src/cli/cmd/tui/context/args.tsx +14 -0
  83. package/src/cli/cmd/tui/context/directory.ts +13 -0
  84. package/src/cli/cmd/tui/context/error-log.tsx +56 -0
  85. package/src/cli/cmd/tui/context/exit.tsx +26 -0
  86. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  87. package/src/cli/cmd/tui/context/inspector.tsx +57 -0
  88. package/src/cli/cmd/tui/context/keybind.tsx +108 -0
  89. package/src/cli/cmd/tui/context/kv.tsx +53 -0
  90. package/src/cli/cmd/tui/context/layout.tsx +240 -0
  91. package/src/cli/cmd/tui/context/local.tsx +402 -0
  92. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  93. package/src/cli/cmd/tui/context/route.tsx +51 -0
  94. package/src/cli/cmd/tui/context/sdk.tsx +94 -0
  95. package/src/cli/cmd/tui/context/sync.tsx +449 -0
  96. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  97. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  98. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  99. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  100. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  101. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  102. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  103. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  104. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  105. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  106. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  107. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  108. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  109. package/src/cli/cmd/tui/context/theme/jonsoc.json +245 -0
  110. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  111. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  112. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  113. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  114. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  115. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  116. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  117. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  118. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  119. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  120. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  121. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  122. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  123. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  124. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  125. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  126. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  127. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  128. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  129. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  130. package/src/cli/cmd/tui/event.ts +48 -0
  131. package/src/cli/cmd/tui/hooks/use-command-registry.tsx +184 -0
  132. package/src/cli/cmd/tui/routes/home.tsx +198 -0
  133. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  134. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  135. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  136. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  137. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  138. package/src/cli/cmd/tui/routes/session/git-commit.tsx +59 -0
  139. package/src/cli/cmd/tui/routes/session/git-history.tsx +122 -0
  140. package/src/cli/cmd/tui/routes/session/header.tsx +185 -0
  141. package/src/cli/cmd/tui/routes/session/index.tsx +2363 -0
  142. package/src/cli/cmd/tui/routes/session/navigator-ui.tsx +214 -0
  143. package/src/cli/cmd/tui/routes/session/navigator.tsx +1124 -0
  144. package/src/cli/cmd/tui/routes/session/panel-explorer.tsx +553 -0
  145. package/src/cli/cmd/tui/routes/session/panel-viewer.tsx +386 -0
  146. package/src/cli/cmd/tui/routes/session/permission.tsx +501 -0
  147. package/src/cli/cmd/tui/routes/session/question.tsx +507 -0
  148. package/src/cli/cmd/tui/routes/session/sidebar.tsx +365 -0
  149. package/src/cli/cmd/tui/routes/session/vcs-diff-viewer.tsx +37 -0
  150. package/src/cli/cmd/tui/routes/ui-settings.tsx +449 -0
  151. package/src/cli/cmd/tui/thread.ts +172 -0
  152. package/src/cli/cmd/tui/ui/dialog-alert.tsx +90 -0
  153. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  154. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
  155. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  156. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  157. package/src/cli/cmd/tui/ui/dialog-select.tsx +384 -0
  158. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  159. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  160. package/src/cli/cmd/tui/ui/spinner.ts +375 -0
  161. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  162. package/src/cli/cmd/tui/util/clipboard.ts +255 -0
  163. package/src/cli/cmd/tui/util/editor.ts +32 -0
  164. package/src/cli/cmd/tui/util/signal.ts +7 -0
  165. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  166. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  167. package/src/cli/cmd/tui/worker.ts +152 -0
  168. package/src/cli/cmd/uninstall.ts +362 -0
  169. package/src/cli/cmd/upgrade.ts +73 -0
  170. package/src/cli/cmd/web.ts +81 -0
  171. package/src/cli/error.ts +57 -0
  172. package/src/cli/network.ts +53 -0
  173. package/src/cli/ui.ts +119 -0
  174. package/src/cli/upgrade.ts +25 -0
  175. package/src/command/index.ts +131 -0
  176. package/src/command/template/initialize.txt +10 -0
  177. package/src/command/template/review.txt +99 -0
  178. package/src/config/config.ts +1404 -0
  179. package/src/config/markdown.ts +93 -0
  180. package/src/env/index.ts +26 -0
  181. package/src/file/ignore.ts +83 -0
  182. package/src/file/index.ts +432 -0
  183. package/src/file/ripgrep.ts +407 -0
  184. package/src/file/time.ts +69 -0
  185. package/src/file/watcher.ts +127 -0
  186. package/src/flag/flag.ts +80 -0
  187. package/src/format/formatter.ts +357 -0
  188. package/src/format/index.ts +137 -0
  189. package/src/global/index.ts +58 -0
  190. package/src/id/id.ts +83 -0
  191. package/src/ide/index.ts +76 -0
  192. package/src/index.ts +208 -0
  193. package/src/installation/index.ts +258 -0
  194. package/src/lsp/client.ts +252 -0
  195. package/src/lsp/index.ts +485 -0
  196. package/src/lsp/language.ts +119 -0
  197. package/src/lsp/server.ts +2046 -0
  198. package/src/mcp/auth.ts +135 -0
  199. package/src/mcp/index.ts +934 -0
  200. package/src/mcp/oauth-callback.ts +200 -0
  201. package/src/mcp/oauth-provider.ts +155 -0
  202. package/src/patch/index.ts +680 -0
  203. package/src/permission/arity.ts +163 -0
  204. package/src/permission/index.ts +210 -0
  205. package/src/permission/next.ts +280 -0
  206. package/src/plugin/codex.ts +500 -0
  207. package/src/plugin/copilot.ts +283 -0
  208. package/src/plugin/index.ts +135 -0
  209. package/src/project/bootstrap.ts +35 -0
  210. package/src/project/instance.ts +91 -0
  211. package/src/project/project.ts +371 -0
  212. package/src/project/state.ts +66 -0
  213. package/src/project/vcs.ts +151 -0
  214. package/src/provider/auth.ts +147 -0
  215. package/src/provider/models-macro.ts +14 -0
  216. package/src/provider/models.ts +114 -0
  217. package/src/provider/provider.ts +1220 -0
  218. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  219. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  220. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  221. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  222. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  223. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  224. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  225. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  226. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1732 -0
  227. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  228. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  229. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  230. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  231. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  232. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  233. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  234. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  235. package/src/provider/transform.ts +742 -0
  236. package/src/pty/index.ts +241 -0
  237. package/src/question/index.ts +176 -0
  238. package/src/scheduler/index.ts +61 -0
  239. package/src/server/error.ts +36 -0
  240. package/src/server/event.ts +7 -0
  241. package/src/server/mdns.ts +59 -0
  242. package/src/server/routes/config.ts +92 -0
  243. package/src/server/routes/experimental.ts +208 -0
  244. package/src/server/routes/file.ts +227 -0
  245. package/src/server/routes/global.ts +135 -0
  246. package/src/server/routes/mcp.ts +225 -0
  247. package/src/server/routes/permission.ts +68 -0
  248. package/src/server/routes/project.ts +82 -0
  249. package/src/server/routes/provider.ts +165 -0
  250. package/src/server/routes/pty.ts +169 -0
  251. package/src/server/routes/question.ts +98 -0
  252. package/src/server/routes/session.ts +939 -0
  253. package/src/server/routes/tui.ts +379 -0
  254. package/src/server/server.ts +663 -0
  255. package/src/session/compaction.ts +225 -0
  256. package/src/session/index.ts +498 -0
  257. package/src/session/llm.ts +288 -0
  258. package/src/session/message-v2.ts +740 -0
  259. package/src/session/message.ts +189 -0
  260. package/src/session/processor.ts +406 -0
  261. package/src/session/prompt/anthropic-20250930.txt +168 -0
  262. package/src/session/prompt/anthropic.txt +172 -0
  263. package/src/session/prompt/anthropic_spoof.txt +1 -0
  264. package/src/session/prompt/beast.txt +149 -0
  265. package/src/session/prompt/build-switch.txt +5 -0
  266. package/src/session/prompt/codex_header.txt +81 -0
  267. package/src/session/prompt/copilot-gpt-5.txt +145 -0
  268. package/src/session/prompt/gemini.txt +157 -0
  269. package/src/session/prompt/max-steps.txt +16 -0
  270. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  271. package/src/session/prompt/plan.txt +26 -0
  272. package/src/session/prompt/qwen.txt +111 -0
  273. package/src/session/prompt.ts +1815 -0
  274. package/src/session/retry.ts +90 -0
  275. package/src/session/revert.ts +121 -0
  276. package/src/session/status.ts +76 -0
  277. package/src/session/summary.ts +150 -0
  278. package/src/session/system.ts +156 -0
  279. package/src/session/todo.ts +37 -0
  280. package/src/share/share-next.ts +205 -0
  281. package/src/share/share.ts +95 -0
  282. package/src/shell/shell.ts +67 -0
  283. package/src/skill/index.ts +1 -0
  284. package/src/skill/skill.ts +135 -0
  285. package/src/snapshot/index.ts +236 -0
  286. package/src/storage/storage.ts +227 -0
  287. package/src/tool/apply_patch.ts +279 -0
  288. package/src/tool/apply_patch.txt +33 -0
  289. package/src/tool/bash.ts +258 -0
  290. package/src/tool/bash.txt +115 -0
  291. package/src/tool/batch.ts +175 -0
  292. package/src/tool/batch.txt +24 -0
  293. package/src/tool/codesearch.ts +132 -0
  294. package/src/tool/codesearch.txt +12 -0
  295. package/src/tool/edit.ts +645 -0
  296. package/src/tool/edit.txt +10 -0
  297. package/src/tool/external-directory.ts +32 -0
  298. package/src/tool/glob.ts +77 -0
  299. package/src/tool/glob.txt +6 -0
  300. package/src/tool/grep.ts +154 -0
  301. package/src/tool/grep.txt +8 -0
  302. package/src/tool/invalid.ts +17 -0
  303. package/src/tool/ls.ts +121 -0
  304. package/src/tool/ls.txt +1 -0
  305. package/src/tool/lsp.ts +96 -0
  306. package/src/tool/lsp.txt +19 -0
  307. package/src/tool/multiedit.ts +46 -0
  308. package/src/tool/multiedit.txt +41 -0
  309. package/src/tool/plan-enter.txt +14 -0
  310. package/src/tool/plan-exit.txt +13 -0
  311. package/src/tool/plan.ts +130 -0
  312. package/src/tool/question.ts +33 -0
  313. package/src/tool/question.txt +10 -0
  314. package/src/tool/read.ts +202 -0
  315. package/src/tool/read.txt +12 -0
  316. package/src/tool/registry.ts +162 -0
  317. package/src/tool/skill.ts +82 -0
  318. package/src/tool/task.ts +188 -0
  319. package/src/tool/task.txt +60 -0
  320. package/src/tool/todo.ts +53 -0
  321. package/src/tool/todoread.txt +14 -0
  322. package/src/tool/todowrite.txt +167 -0
  323. package/src/tool/tool.ts +88 -0
  324. package/src/tool/truncation.ts +106 -0
  325. package/src/tool/webfetch.ts +182 -0
  326. package/src/tool/webfetch.txt +13 -0
  327. package/src/tool/websearch.ts +150 -0
  328. package/src/tool/websearch.txt +14 -0
  329. package/src/tool/write.ts +80 -0
  330. package/src/tool/write.txt +8 -0
  331. package/src/util/archive.ts +16 -0
  332. package/src/util/color.ts +19 -0
  333. package/src/util/context.ts +25 -0
  334. package/src/util/defer.ts +12 -0
  335. package/src/util/eventloop.ts +20 -0
  336. package/src/util/filesystem.ts +93 -0
  337. package/src/util/fn.ts +11 -0
  338. package/src/util/format.ts +20 -0
  339. package/src/util/iife.ts +3 -0
  340. package/src/util/keybind.ts +103 -0
  341. package/src/util/lazy.ts +18 -0
  342. package/src/util/locale.ts +81 -0
  343. package/src/util/lock.ts +98 -0
  344. package/src/util/log.ts +180 -0
  345. package/src/util/queue.ts +32 -0
  346. package/src/util/rpc.ts +66 -0
  347. package/src/util/scrap.ts +10 -0
  348. package/src/util/signal.ts +12 -0
  349. package/src/util/timeout.ts +14 -0
  350. package/src/util/token.ts +7 -0
  351. package/src/util/wildcard.ts +56 -0
  352. package/src/worktree/index.ts +524 -0
  353. package/sst-env.d.ts +9 -0
  354. package/test/acp/agent-interface.test.ts +51 -0
  355. package/test/acp/event-subscription.test.ts +436 -0
  356. package/test/agent/agent.test.ts +638 -0
  357. package/test/bun.test.ts +53 -0
  358. package/test/cli/cmd/tui/fileref.test.ts +30 -0
  359. package/test/cli/github-action.test.ts +129 -0
  360. package/test/cli/github-remote.test.ts +80 -0
  361. package/test/cli/tui/navigator_logic.test.ts +99 -0
  362. package/test/cli/tui/transcript.test.ts +297 -0
  363. package/test/cli/ui.test.ts +80 -0
  364. package/test/config/agent-color.test.ts +66 -0
  365. package/test/config/config.test.ts +1613 -0
  366. package/test/config/fixtures/empty-frontmatter.md +4 -0
  367. package/test/config/fixtures/frontmatter.md +28 -0
  368. package/test/config/fixtures/no-frontmatter.md +1 -0
  369. package/test/config/markdown.test.ts +192 -0
  370. package/test/file/ignore.test.ts +10 -0
  371. package/test/file/path-traversal.test.ts +198 -0
  372. package/test/fixture/fixture.ts +45 -0
  373. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  374. package/test/ide/ide.test.ts +82 -0
  375. package/test/keybind.test.ts +421 -0
  376. package/test/lsp/client.test.ts +95 -0
  377. package/test/mcp/headers.test.ts +153 -0
  378. package/test/mcp/oauth-browser.test.ts +261 -0
  379. package/test/patch/patch.test.ts +348 -0
  380. package/test/permission/arity.test.ts +33 -0
  381. package/test/permission/next.test.ts +690 -0
  382. package/test/permission-task.test.ts +319 -0
  383. package/test/plugin/codex.test.ts +123 -0
  384. package/test/preload.ts +67 -0
  385. package/test/project/project.test.ts +120 -0
  386. package/test/provider/amazon-bedrock.test.ts +268 -0
  387. package/test/provider/gitlab-duo.test.ts +286 -0
  388. package/test/provider/provider.test.ts +2149 -0
  389. package/test/provider/transform.test.ts +1631 -0
  390. package/test/question/question.test.ts +300 -0
  391. package/test/scheduler.test.ts +73 -0
  392. package/test/server/session-list.test.ts +39 -0
  393. package/test/server/session-select.test.ts +78 -0
  394. package/test/session/compaction.test.ts +293 -0
  395. package/test/session/llm.test.ts +90 -0
  396. package/test/session/message-v2.test.ts +786 -0
  397. package/test/session/retry.test.ts +131 -0
  398. package/test/session/revert-compact.test.ts +285 -0
  399. package/test/session/session.test.ts +71 -0
  400. package/test/skill/skill.test.ts +185 -0
  401. package/test/snapshot/snapshot.test.ts +939 -0
  402. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  403. package/test/tool/apply_patch.test.ts +499 -0
  404. package/test/tool/bash.test.ts +320 -0
  405. package/test/tool/external-directory.test.ts +126 -0
  406. package/test/tool/fixtures/large-image.png +0 -0
  407. package/test/tool/fixtures/models-api.json +33453 -0
  408. package/test/tool/grep.test.ts +109 -0
  409. package/test/tool/question.test.ts +105 -0
  410. package/test/tool/read.test.ts +332 -0
  411. package/test/tool/registry.test.ts +76 -0
  412. package/test/tool/truncation.test.ts +159 -0
  413. package/test/util/filesystem.test.ts +39 -0
  414. package/test/util/format.test.ts +59 -0
  415. package/test/util/iife.test.ts +36 -0
  416. package/test/util/lazy.test.ts +50 -0
  417. package/test/util/lock.test.ts +72 -0
  418. package/test/util/timeout.test.ts +21 -0
  419. package/test/util/wildcard.test.ts +75 -0
  420. package/tsconfig.json +16 -0
@@ -0,0 +1,1220 @@
1
+ import z from "zod"
2
+ import fuzzysort from "fuzzysort"
3
+ import { Config } from "../config/config"
4
+ import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
5
+ import { NoSuchModelError, type Provider as SDK } from "ai"
6
+ import { Log } from "../util/log"
7
+ import { BunProc } from "../bun"
8
+ import { Plugin } from "../plugin"
9
+ import { ModelsDev } from "./models"
10
+ import { NamedError } from "@jonsoc/util/error"
11
+ import { Auth } from "../auth"
12
+ import { Env } from "../env"
13
+ import { Instance } from "../project/instance"
14
+ import { Flag } from "../flag/flag"
15
+ import { iife } from "@/util/iife"
16
+ import { Brand } from "../brand"
17
+
18
+ // Direct imports for bundled providers
19
+ import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
20
+ import { createAnthropic } from "@ai-sdk/anthropic"
21
+ import { createAzure } from "@ai-sdk/azure"
22
+ import { createGoogleGenerativeAI } from "@ai-sdk/google"
23
+ import { createVertex } from "@ai-sdk/google-vertex"
24
+ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
25
+ import { createOpenAI } from "@ai-sdk/openai"
26
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
27
+ import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
28
+ import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
29
+ import { createXai } from "@ai-sdk/xai"
30
+ import { createMistral } from "@ai-sdk/mistral"
31
+ import { createGroq } from "@ai-sdk/groq"
32
+ import { createDeepInfra } from "@ai-sdk/deepinfra"
33
+ import { createCerebras } from "@ai-sdk/cerebras"
34
+ import { createCohere } from "@ai-sdk/cohere"
35
+ import { createGateway } from "@ai-sdk/gateway"
36
+ import { createTogetherAI } from "@ai-sdk/togetherai"
37
+ import { createPerplexity } from "@ai-sdk/perplexity"
38
+ import { createVercel } from "@ai-sdk/vercel"
39
+ import { createGitLab } from "@gitlab/gitlab-ai-provider"
40
+ import { ProviderTransform } from "./transform"
41
+
42
+ export namespace Provider {
43
+ const log = Log.create({ service: "provider" })
44
+
45
+ function isGpt5OrLater(modelID: string): boolean {
46
+ const match = /^gpt-(\d+)/.exec(modelID)
47
+ if (!match) {
48
+ return false
49
+ }
50
+ return Number(match[1]) >= 5
51
+ }
52
+
53
+ function shouldUseCopilotResponsesApi(modelID: string): boolean {
54
+ return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")
55
+ }
56
+
57
+ const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
58
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
59
+ "@ai-sdk/anthropic": createAnthropic,
60
+ "@ai-sdk/azure": createAzure,
61
+ "@ai-sdk/google": createGoogleGenerativeAI,
62
+ "@ai-sdk/google-vertex": createVertex,
63
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
64
+ "@ai-sdk/openai": createOpenAI,
65
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
66
+ "@openrouter/ai-sdk-provider": createOpenRouter,
67
+ "@ai-sdk/xai": createXai,
68
+ "@ai-sdk/mistral": createMistral,
69
+ "@ai-sdk/groq": createGroq,
70
+ "@ai-sdk/deepinfra": createDeepInfra,
71
+ "@ai-sdk/cerebras": createCerebras,
72
+ "@ai-sdk/cohere": createCohere,
73
+ "@ai-sdk/gateway": createGateway,
74
+ "@ai-sdk/togetherai": createTogetherAI,
75
+ "@ai-sdk/perplexity": createPerplexity,
76
+ "@ai-sdk/vercel": createVercel,
77
+ "@gitlab/gitlab-ai-provider": createGitLab,
78
+ // @ts-ignore (TODO: kill this code so we dont have to maintain it)
79
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
80
+ }
81
+
82
+ type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
83
+ type CustomLoader = (provider: Info) => Promise<{
84
+ autoload: boolean
85
+ getModel?: CustomModelLoader
86
+ options?: Record<string, any>
87
+ }>
88
+
89
+ const CUSTOM_LOADERS: Record<string, CustomLoader> = {
90
+ async anthropic() {
91
+ return {
92
+ autoload: false,
93
+ options: {
94
+ headers: {
95
+ "anthropic-beta":
96
+ "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
97
+ },
98
+ },
99
+ }
100
+ },
101
+ async jonsoc(input) {
102
+ const hasKey = await (async () => {
103
+ const env = Env.all()
104
+ if (input.env.some((item) => env[item])) return true
105
+ if (await Auth.get(input.id)) return true
106
+ const config = await Config.get()
107
+ if (config.provider?.["jonsoc"]?.options?.apiKey) return true
108
+ return false
109
+ })()
110
+
111
+ if (!hasKey) {
112
+ for (const [key, value] of Object.entries(input.models)) {
113
+ if (value.cost.input === 0) continue
114
+ delete input.models[key]
115
+ }
116
+ }
117
+
118
+ return {
119
+ autoload: Object.keys(input.models).length > 0,
120
+ options: hasKey ? {} : { apiKey: "public" },
121
+ }
122
+ },
123
+ openai: async () => {
124
+ return {
125
+ autoload: false,
126
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
127
+ return sdk.responses(modelID)
128
+ },
129
+ options: {},
130
+ }
131
+ },
132
+ "github-copilot": async () => {
133
+ return {
134
+ autoload: false,
135
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
136
+ return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
137
+ },
138
+ options: {},
139
+ }
140
+ },
141
+ "github-copilot-enterprise": async () => {
142
+ return {
143
+ autoload: false,
144
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
145
+ return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
146
+ },
147
+ options: {},
148
+ }
149
+ },
150
+ azure: async () => {
151
+ return {
152
+ autoload: false,
153
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
154
+ if (options?.["useCompletionUrls"]) {
155
+ return sdk.chat(modelID)
156
+ } else {
157
+ return sdk.responses(modelID)
158
+ }
159
+ },
160
+ options: {},
161
+ }
162
+ },
163
+ "azure-cognitive-services": async () => {
164
+ const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
165
+ return {
166
+ autoload: false,
167
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
168
+ if (options?.["useCompletionUrls"]) {
169
+ return sdk.chat(modelID)
170
+ } else {
171
+ return sdk.responses(modelID)
172
+ }
173
+ },
174
+ options: {
175
+ baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
176
+ },
177
+ }
178
+ },
179
+ "amazon-bedrock": async () => {
180
+ const config = await Config.get()
181
+ const providerConfig = config.provider?.["amazon-bedrock"]
182
+
183
+ const auth = await Auth.get("amazon-bedrock")
184
+
185
+ // Region precedence: 1) config file, 2) env var, 3) default
186
+ const configRegion = providerConfig?.options?.region
187
+ const envRegion = Env.get("AWS_REGION")
188
+ const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
189
+
190
+ // Profile: config file takes precedence over env var
191
+ const configProfile = providerConfig?.options?.profile
192
+ const envProfile = Env.get("AWS_PROFILE")
193
+ const profile = configProfile ?? envProfile
194
+
195
+ const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
196
+
197
+ const awsBearerToken = iife(() => {
198
+ const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")
199
+ if (envToken) return envToken
200
+ if (auth?.type === "api") {
201
+ Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)
202
+ return auth.key
203
+ }
204
+ return undefined
205
+ })
206
+
207
+ const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE")
208
+
209
+ if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile) return { autoload: false }
210
+
211
+ const providerOptions: AmazonBedrockProviderSettings = {
212
+ region: defaultRegion,
213
+ }
214
+
215
+ // Only use credential chain if no bearer token exists
216
+ // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)
217
+ if (!awsBearerToken) {
218
+ const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
219
+
220
+ // Build credential provider options (only pass profile if specified)
221
+ const credentialProviderOptions = profile ? { profile } : {}
222
+
223
+ providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
224
+ }
225
+
226
+ // Add custom endpoint if specified (endpoint takes precedence over baseURL)
227
+ const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
228
+ if (endpoint) {
229
+ providerOptions.baseURL = endpoint
230
+ }
231
+
232
+ return {
233
+ autoload: true,
234
+ options: providerOptions,
235
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
236
+ // Skip region prefixing if model already has a cross-region inference profile prefix
237
+ if (modelID.startsWith("global.") || modelID.startsWith("jp.")) {
238
+ return sdk.languageModel(modelID)
239
+ }
240
+
241
+ // Region resolution precedence (highest to lowest):
242
+ // 1. options.region from jonsoc.json provider config
243
+ // 2. defaultRegion from AWS_REGION environment variable
244
+ // 3. Default "us-east-1" (baked into defaultRegion)
245
+ const region = options?.region ?? defaultRegion
246
+
247
+ let regionPrefix = region.split("-")[0]
248
+
249
+ switch (regionPrefix) {
250
+ case "us": {
251
+ const modelRequiresPrefix = [
252
+ "nova-micro",
253
+ "nova-lite",
254
+ "nova-pro",
255
+ "nova-premier",
256
+ "nova-2",
257
+ "claude",
258
+ "deepseek",
259
+ ].some((m) => modelID.includes(m))
260
+ const isGovCloud = region.startsWith("us-gov")
261
+ if (modelRequiresPrefix && !isGovCloud) {
262
+ modelID = `${regionPrefix}.${modelID}`
263
+ }
264
+ break
265
+ }
266
+ case "eu": {
267
+ const regionRequiresPrefix = [
268
+ "eu-west-1",
269
+ "eu-west-2",
270
+ "eu-west-3",
271
+ "eu-north-1",
272
+ "eu-central-1",
273
+ "eu-south-1",
274
+ "eu-south-2",
275
+ ].some((r) => region.includes(r))
276
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
277
+ modelID.includes(m),
278
+ )
279
+ if (regionRequiresPrefix && modelRequiresPrefix) {
280
+ modelID = `${regionPrefix}.${modelID}`
281
+ }
282
+ break
283
+ }
284
+ case "ap": {
285
+ const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
286
+ const isTokyoRegion = region === "ap-northeast-1"
287
+ if (
288
+ isAustraliaRegion &&
289
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
290
+ ) {
291
+ regionPrefix = "au"
292
+ modelID = `${regionPrefix}.${modelID}`
293
+ } else if (isTokyoRegion) {
294
+ // Tokyo region uses jp. prefix for cross-region inference
295
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
296
+ modelID.includes(m),
297
+ )
298
+ if (modelRequiresPrefix) {
299
+ regionPrefix = "jp"
300
+ modelID = `${regionPrefix}.${modelID}`
301
+ }
302
+ } else {
303
+ // Other APAC regions use apac. prefix
304
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
305
+ modelID.includes(m),
306
+ )
307
+ if (modelRequiresPrefix) {
308
+ regionPrefix = "apac"
309
+ modelID = `${regionPrefix}.${modelID}`
310
+ }
311
+ }
312
+ break
313
+ }
314
+ }
315
+
316
+ return sdk.languageModel(modelID)
317
+ },
318
+ }
319
+ },
320
+ openrouter: async () => {
321
+ return {
322
+ autoload: false,
323
+ options: {
324
+ headers: {
325
+ "HTTP-Referer": `${Brand.DOMAIN_WITH_PROTOCOL}/`,
326
+ "X-Title": Brand.BRAND_LOWER,
327
+ },
328
+ },
329
+ }
330
+ },
331
+ vercel: async () => {
332
+ return {
333
+ autoload: false,
334
+ options: {
335
+ headers: {
336
+ "http-referer": `${Brand.DOMAIN_WITH_PROTOCOL}/`,
337
+ "x-title": Brand.BRAND_LOWER,
338
+ },
339
+ },
340
+ }
341
+ },
342
+ "google-vertex": async () => {
343
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
344
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
345
+ const autoload = Boolean(project)
346
+ if (!autoload) return { autoload: false }
347
+ return {
348
+ autoload: true,
349
+ options: {
350
+ project,
351
+ location,
352
+ },
353
+ async getModel(sdk: any, modelID: string) {
354
+ const id = String(modelID).trim()
355
+ return sdk.languageModel(id)
356
+ },
357
+ }
358
+ },
359
+ "google-vertex-anthropic": async () => {
360
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
361
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
362
+ const autoload = Boolean(project)
363
+ if (!autoload) return { autoload: false }
364
+ return {
365
+ autoload: true,
366
+ options: {
367
+ project,
368
+ location,
369
+ },
370
+ async getModel(sdk: any, modelID) {
371
+ const id = String(modelID).trim()
372
+ return sdk.languageModel(id)
373
+ },
374
+ }
375
+ },
376
+ "sap-ai-core": async () => {
377
+ const auth = await Auth.get("sap-ai-core")
378
+ const envServiceKey = iife(() => {
379
+ const envAICoreServiceKey = Env.get("AICORE_SERVICE_KEY")
380
+ if (envAICoreServiceKey) return envAICoreServiceKey
381
+ if (auth?.type === "api") {
382
+ Env.set("AICORE_SERVICE_KEY", auth.key)
383
+ return auth.key
384
+ }
385
+ return undefined
386
+ })
387
+ const deploymentId = Env.get("AICORE_DEPLOYMENT_ID")
388
+ const resourceGroup = Env.get("AICORE_RESOURCE_GROUP")
389
+
390
+ return {
391
+ autoload: !!envServiceKey,
392
+ options: envServiceKey ? { deploymentId, resourceGroup } : {},
393
+ async getModel(sdk: any, modelID: string) {
394
+ return sdk(modelID)
395
+ },
396
+ }
397
+ },
398
+ zenmux: async () => {
399
+ return {
400
+ autoload: false,
401
+ options: {
402
+ headers: {
403
+ "HTTP-Referer": `${Brand.DOMAIN_WITH_PROTOCOL}/`,
404
+ "X-Title": Brand.BRAND_LOWER,
405
+ },
406
+ },
407
+ }
408
+ },
409
+ gitlab: async (input) => {
410
+ const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com"
411
+
412
+ const auth = await Auth.get(input.id)
413
+ const apiKey = await (async () => {
414
+ if (auth?.type === "oauth") return auth.access
415
+ if (auth?.type === "api") return auth.key
416
+ return Env.get("GITLAB_TOKEN")
417
+ })()
418
+
419
+ const config = await Config.get()
420
+ const providerConfig = config.provider?.["gitlab"]
421
+
422
+ return {
423
+ autoload: !!apiKey,
424
+ options: {
425
+ instanceUrl,
426
+ apiKey,
427
+ featureFlags: {
428
+ duo_agent_platform_agentic_chat: true,
429
+ duo_agent_platform: true,
430
+ ...(providerConfig?.options?.featureFlags || {}),
431
+ },
432
+ },
433
+ async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
434
+ return sdk.agenticChat(modelID, {
435
+ featureFlags: {
436
+ duo_agent_platform_agentic_chat: true,
437
+ duo_agent_platform: true,
438
+ ...(providerConfig?.options?.featureFlags || {}),
439
+ },
440
+ })
441
+ },
442
+ }
443
+ },
444
+ "cloudflare-ai-gateway": async (input) => {
445
+ const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
446
+ const gateway = Env.get("CLOUDFLARE_GATEWAY_ID")
447
+
448
+ if (!accountId || !gateway) return { autoload: false }
449
+
450
+ // Get API token from env or auth prompt
451
+ const apiToken = await (async () => {
452
+ const envToken = Env.get("CLOUDFLARE_API_TOKEN")
453
+ if (envToken) return envToken
454
+ const auth = await Auth.get(input.id)
455
+ if (auth?.type === "api") return auth.key
456
+ return undefined
457
+ })()
458
+
459
+ return {
460
+ autoload: true,
461
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
462
+ return sdk.languageModel(modelID)
463
+ },
464
+ options: {
465
+ baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}/compat`,
466
+ headers: {
467
+ // Cloudflare AI Gateway uses cf-aig-authorization for authenticated gateways
468
+ // This enables Unified Billing where Cloudflare handles upstream provider auth
469
+ ...(apiToken ? { "cf-aig-authorization": `Bearer ${apiToken}` } : {}),
470
+ "HTTP-Referer": `${Brand.DOMAIN_WITH_PROTOCOL}/`,
471
+ "X-Title": Brand.BRAND_LOWER,
472
+ },
473
+ // Custom fetch to handle parameter transformation and auth
474
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
475
+ const headers = new Headers(init?.headers)
476
+ // Strip Authorization header - AI Gateway uses cf-aig-authorization instead
477
+ headers.delete("Authorization")
478
+
479
+ // Transform max_tokens to max_completion_tokens for newer models
480
+ if (init?.body && init.method === "POST") {
481
+ try {
482
+ const body = JSON.parse(init.body as string)
483
+ if (body.max_tokens !== undefined && !body.max_completion_tokens) {
484
+ body.max_completion_tokens = body.max_tokens
485
+ delete body.max_tokens
486
+ init = { ...init, body: JSON.stringify(body) }
487
+ }
488
+ } catch (e) {
489
+ // If body parsing fails, continue with original request
490
+ }
491
+ }
492
+
493
+ return fetch(input, { ...init, headers })
494
+ },
495
+ },
496
+ }
497
+ },
498
+ cerebras: async () => {
499
+ return {
500
+ autoload: false,
501
+ options: {
502
+ headers: {
503
+ "X-Cerebras-3rd-Party-Integration": "jonsoc",
504
+ },
505
+ },
506
+ }
507
+ },
508
+ }
509
+
510
+ export const Model = z
511
+ .object({
512
+ id: z.string(),
513
+ providerID: z.string(),
514
+ api: z.object({
515
+ id: z.string(),
516
+ url: z.string(),
517
+ npm: z.string(),
518
+ }),
519
+ name: z.string(),
520
+ family: z.string().optional(),
521
+ capabilities: z.object({
522
+ temperature: z.boolean(),
523
+ reasoning: z.boolean(),
524
+ attachment: z.boolean(),
525
+ toolcall: z.boolean(),
526
+ input: z.object({
527
+ text: z.boolean(),
528
+ audio: z.boolean(),
529
+ image: z.boolean(),
530
+ video: z.boolean(),
531
+ pdf: z.boolean(),
532
+ }),
533
+ output: z.object({
534
+ text: z.boolean(),
535
+ audio: z.boolean(),
536
+ image: z.boolean(),
537
+ video: z.boolean(),
538
+ pdf: z.boolean(),
539
+ }),
540
+ interleaved: z.union([
541
+ z.boolean(),
542
+ z.object({
543
+ field: z.enum(["reasoning_content", "reasoning_details"]),
544
+ }),
545
+ ]),
546
+ }),
547
+ cost: z.object({
548
+ input: z.number(),
549
+ output: z.number(),
550
+ cache: z.object({
551
+ read: z.number(),
552
+ write: z.number(),
553
+ }),
554
+ experimentalOver200K: z
555
+ .object({
556
+ input: z.number(),
557
+ output: z.number(),
558
+ cache: z.object({
559
+ read: z.number(),
560
+ write: z.number(),
561
+ }),
562
+ })
563
+ .optional(),
564
+ }),
565
+ limit: z.object({
566
+ context: z.number(),
567
+ input: z.number().optional(),
568
+ output: z.number(),
569
+ }),
570
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
571
+ options: z.record(z.string(), z.any()),
572
+ headers: z.record(z.string(), z.string()),
573
+ release_date: z.string(),
574
+ variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
575
+ })
576
+ .meta({
577
+ ref: "Model",
578
+ })
579
+ export type Model = z.infer<typeof Model>
580
+
581
+ export const Info = z
582
+ .object({
583
+ id: z.string(),
584
+ name: z.string(),
585
+ source: z.enum(["env", "config", "custom", "api"]),
586
+ env: z.string().array(),
587
+ key: z.string().optional(),
588
+ options: z.record(z.string(), z.any()),
589
+ models: z.record(z.string(), Model),
590
+ })
591
+ .meta({
592
+ ref: "Provider",
593
+ })
594
+ export type Info = z.infer<typeof Info>
595
+
596
+ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
597
+ const m: Model = {
598
+ id: model.id,
599
+ providerID: provider.id,
600
+ name: model.name,
601
+ family: model.family,
602
+ api: {
603
+ id: model.id,
604
+ url: provider.api!,
605
+ npm: iife(() => {
606
+ if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot"
607
+ return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible"
608
+ }),
609
+ },
610
+ status: model.status ?? "active",
611
+ headers: model.headers ?? {},
612
+ options: model.options ?? {},
613
+ cost: {
614
+ input: model.cost?.input ?? 0,
615
+ output: model.cost?.output ?? 0,
616
+ cache: {
617
+ read: model.cost?.cache_read ?? 0,
618
+ write: model.cost?.cache_write ?? 0,
619
+ },
620
+ experimentalOver200K: model.cost?.context_over_200k
621
+ ? {
622
+ cache: {
623
+ read: model.cost.context_over_200k.cache_read ?? 0,
624
+ write: model.cost.context_over_200k.cache_write ?? 0,
625
+ },
626
+ input: model.cost.context_over_200k.input,
627
+ output: model.cost.context_over_200k.output,
628
+ }
629
+ : undefined,
630
+ },
631
+ limit: {
632
+ context: model.limit.context,
633
+ input: model.limit.input,
634
+ output: model.limit.output,
635
+ },
636
+ capabilities: {
637
+ temperature: model.temperature,
638
+ reasoning: model.reasoning,
639
+ attachment: model.attachment,
640
+ toolcall: model.tool_call,
641
+ input: {
642
+ text: model.modalities?.input?.includes("text") ?? false,
643
+ audio: model.modalities?.input?.includes("audio") ?? false,
644
+ image: model.modalities?.input?.includes("image") ?? false,
645
+ video: model.modalities?.input?.includes("video") ?? false,
646
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
647
+ },
648
+ output: {
649
+ text: model.modalities?.output?.includes("text") ?? false,
650
+ audio: model.modalities?.output?.includes("audio") ?? false,
651
+ image: model.modalities?.output?.includes("image") ?? false,
652
+ video: model.modalities?.output?.includes("video") ?? false,
653
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
654
+ },
655
+ interleaved: model.interleaved ?? false,
656
+ },
657
+ release_date: model.release_date,
658
+ variants: {},
659
+ }
660
+
661
+ m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
662
+
663
+ return m
664
+ }
665
+
666
+ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
667
+ return {
668
+ id: provider.id,
669
+ source: "custom",
670
+ name: provider.name,
671
+ env: provider.env ?? [],
672
+ options: {},
673
+ models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
674
+ }
675
+ }
676
+
677
+ const state = Instance.state(async () => {
678
+ using _ = log.time("state")
679
+ const config = await Config.get()
680
+ const modelsDev = await ModelsDev.get()
681
+ const database = mapValues(modelsDev, fromModelsDevProvider)
682
+
683
+ const disabled = new Set(config.disabled_providers ?? [])
684
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
685
+
686
+ function isProviderAllowed(providerID: string): boolean {
687
+ if (enabled && !enabled.has(providerID)) return false
688
+ if (disabled.has(providerID)) return false
689
+ return true
690
+ }
691
+
692
+ const providers: { [providerID: string]: Info } = {}
693
+ const languages = new Map<string, LanguageModelV2>()
694
+ const modelLoaders: {
695
+ [providerID: string]: CustomModelLoader
696
+ } = {}
697
+ const sdk = new Map<number, SDK>()
698
+
699
+ log.info("init")
700
+
701
+ const configProviders = Object.entries(config.provider ?? {})
702
+
703
+ // Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
704
+ if (database["github-copilot"]) {
705
+ const githubCopilot = database["github-copilot"]
706
+ database["github-copilot-enterprise"] = {
707
+ ...githubCopilot,
708
+ id: "github-copilot-enterprise",
709
+ name: "GitHub Copilot Enterprise",
710
+ models: mapValues(githubCopilot.models, (model) => ({
711
+ ...model,
712
+ providerID: "github-copilot-enterprise",
713
+ })),
714
+ }
715
+ }
716
+
717
+ function mergeProvider(providerID: string, provider: Partial<Info>) {
718
+ const existing = providers[providerID]
719
+ if (existing) {
720
+ // @ts-expect-error
721
+ providers[providerID] = mergeDeep(existing, provider)
722
+ return
723
+ }
724
+ const match = database[providerID]
725
+ if (!match) return
726
+ // @ts-expect-error
727
+ providers[providerID] = mergeDeep(match, provider)
728
+ }
729
+
730
+ // extend database from config
731
+ for (const [providerID, provider] of configProviders) {
732
+ const existing = database[providerID]
733
+ const parsed: Info = {
734
+ id: providerID,
735
+ name: provider.name ?? existing?.name ?? providerID,
736
+ env: provider.env ?? existing?.env ?? [],
737
+ options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
738
+ source: "config",
739
+ models: existing?.models ?? {},
740
+ }
741
+
742
+ for (const [modelID, model] of Object.entries(provider.models ?? {})) {
743
+ const existingModel = parsed.models[model.id ?? modelID]
744
+ const name = iife(() => {
745
+ if (model.name) return model.name
746
+ if (model.id && model.id !== modelID) return modelID
747
+ return existingModel?.name ?? modelID
748
+ })
749
+ const parsedModel: Model = {
750
+ id: modelID,
751
+ api: {
752
+ id: model.id ?? existingModel?.api.id ?? modelID,
753
+ npm:
754
+ model.provider?.npm ??
755
+ provider.npm ??
756
+ existingModel?.api.npm ??
757
+ modelsDev[providerID]?.npm ??
758
+ "@ai-sdk/openai-compatible",
759
+ url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
760
+ },
761
+ status: model.status ?? existingModel?.status ?? "active",
762
+ name,
763
+ providerID,
764
+ capabilities: {
765
+ temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
766
+ reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
767
+ attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
768
+ toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
769
+ input: {
770
+ text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
771
+ audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
772
+ image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
773
+ video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
774
+ pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
775
+ },
776
+ output: {
777
+ text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
778
+ audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
779
+ image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
780
+ video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
781
+ pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
782
+ },
783
+ interleaved: model.interleaved ?? false,
784
+ },
785
+ cost: {
786
+ input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
787
+ output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
788
+ cache: {
789
+ read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
790
+ write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
791
+ },
792
+ },
793
+ options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
794
+ limit: {
795
+ context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
796
+ output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
797
+ },
798
+ headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
799
+ family: model.family ?? existingModel?.family ?? "",
800
+ release_date: model.release_date ?? existingModel?.release_date ?? "",
801
+ variants: {},
802
+ }
803
+ const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
804
+ parsedModel.variants = mapValues(
805
+ pickBy(merged, (v) => !v.disabled),
806
+ (v) => omit(v, ["disabled"]),
807
+ )
808
+ parsed.models[modelID] = parsedModel
809
+ }
810
+ database[providerID] = parsed
811
+ }
812
+
813
+ // load env
814
+ const env = Env.all()
815
+ for (const [providerID, provider] of Object.entries(database)) {
816
+ if (disabled.has(providerID)) continue
817
+ const apiKey = provider.env.map((item) => env[item]).find(Boolean)
818
+ if (!apiKey) continue
819
+ mergeProvider(providerID, {
820
+ source: "env",
821
+ key: provider.env.length === 1 ? apiKey : undefined,
822
+ })
823
+ }
824
+
825
+ // load apikeys
826
+ for (const [providerID, provider] of Object.entries(await Auth.all())) {
827
+ if (disabled.has(providerID)) continue
828
+ if (provider.type === "api") {
829
+ mergeProvider(providerID, {
830
+ source: "api",
831
+ key: provider.key,
832
+ })
833
+ }
834
+ }
835
+
836
+ for (const plugin of await Plugin.list()) {
837
+ if (!plugin.auth) continue
838
+ const providerID = plugin.auth.provider
839
+ if (disabled.has(providerID)) continue
840
+
841
+ // For github-copilot plugin, check if auth exists for either github-copilot or github-copilot-enterprise
842
+ let hasAuth = false
843
+ const auth = await Auth.get(providerID)
844
+ if (auth) hasAuth = true
845
+
846
+ // Special handling for github-copilot: also check for enterprise auth
847
+ if (providerID === "github-copilot" && !hasAuth) {
848
+ const enterpriseAuth = await Auth.get("github-copilot-enterprise")
849
+ if (enterpriseAuth) hasAuth = true
850
+ }
851
+
852
+ if (!hasAuth) continue
853
+ if (!plugin.auth.loader) continue
854
+
855
+ // Load for the main provider if auth exists
856
+ if (auth) {
857
+ const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
858
+ mergeProvider(plugin.auth.provider, {
859
+ source: "custom",
860
+ options: options,
861
+ })
862
+ }
863
+
864
+ // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
865
+ if (providerID === "github-copilot") {
866
+ const enterpriseProviderID = "github-copilot-enterprise"
867
+ if (!disabled.has(enterpriseProviderID)) {
868
+ const enterpriseAuth = await Auth.get(enterpriseProviderID)
869
+ if (enterpriseAuth) {
870
+ const enterpriseOptions = await plugin.auth.loader(
871
+ () => Auth.get(enterpriseProviderID) as any,
872
+ database[enterpriseProviderID],
873
+ )
874
+ mergeProvider(enterpriseProviderID, {
875
+ source: "custom",
876
+ options: enterpriseOptions,
877
+ })
878
+ }
879
+ }
880
+ }
881
+ }
882
+
883
+ for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
884
+ if (disabled.has(providerID)) continue
885
+ const data = database[providerID]
886
+ if (!data) {
887
+ log.error("Provider does not exist in model list " + providerID)
888
+ continue
889
+ }
890
+ const result = await fn(data)
891
+ if (result && (result.autoload || providers[providerID])) {
892
+ if (result.getModel) modelLoaders[providerID] = result.getModel
893
+ mergeProvider(providerID, {
894
+ source: "custom",
895
+ options: result.options,
896
+ })
897
+ }
898
+ }
899
+
900
+ // load config
901
+ for (const [providerID, provider] of configProviders) {
902
+ const partial: Partial<Info> = { source: "config" }
903
+ if (provider.env) partial.env = provider.env
904
+ if (provider.name) partial.name = provider.name
905
+ if (provider.options) partial.options = provider.options
906
+ mergeProvider(providerID, partial)
907
+ }
908
+
909
+ for (const [providerID, provider] of Object.entries(providers)) {
910
+ if (!isProviderAllowed(providerID)) {
911
+ delete providers[providerID]
912
+ continue
913
+ }
914
+
915
+ const configProvider = config.provider?.[providerID]
916
+
917
+ for (const [modelID, model] of Object.entries(provider.models)) {
918
+ model.api.id = model.api.id ?? model.id ?? modelID
919
+ if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
920
+ delete provider.models[modelID]
921
+ if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
922
+ if (model.status === "deprecated") delete provider.models[modelID]
923
+ if (
924
+ (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
925
+ (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
926
+ )
927
+ delete provider.models[modelID]
928
+
929
+ // Filter out disabled variants from config
930
+ const configVariants = configProvider?.models?.[modelID]?.variants
931
+ if (configVariants && model.variants) {
932
+ const merged = mergeDeep(model.variants, configVariants)
933
+ model.variants = mapValues(
934
+ pickBy(merged, (v) => !v.disabled),
935
+ (v) => omit(v, ["disabled"]),
936
+ )
937
+ }
938
+ }
939
+
940
+ if (Object.keys(provider.models).length === 0) {
941
+ delete providers[providerID]
942
+ continue
943
+ }
944
+
945
+ log.info("found", { providerID })
946
+ }
947
+
948
+ return {
949
+ models: languages,
950
+ providers,
951
+ sdk,
952
+ modelLoaders,
953
+ }
954
+ })
955
+
956
+ export async function list() {
957
+ return state().then((state) => state.providers)
958
+ }
959
+
960
+ async function getSDK(model: Model) {
961
+ try {
962
+ using _ = log.time("getSDK", {
963
+ providerID: model.providerID,
964
+ })
965
+ const s = await state()
966
+ const provider = s.providers[model.providerID]
967
+ const options = { ...provider.options }
968
+
969
+ if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
970
+ options["includeUsage"] = true
971
+ }
972
+
973
+ if (!options["baseURL"]) options["baseURL"] = model.api.url
974
+ if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
975
+ if (model.headers)
976
+ options["headers"] = {
977
+ ...options["headers"],
978
+ ...model.headers,
979
+ }
980
+
981
+ const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
982
+ const existing = s.sdk.get(key)
983
+ if (existing) return existing
984
+
985
+ const customFetch = options["fetch"]
986
+
987
+ options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
988
+ // Preserve custom fetch if it exists, wrap it with timeout logic
989
+ const fetchFn = customFetch ?? fetch
990
+ const opts = init ?? {}
991
+
992
+ if (options["timeout"] !== undefined && options["timeout"] !== null) {
993
+ const signals: AbortSignal[] = []
994
+ if (opts.signal) signals.push(opts.signal)
995
+ if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
996
+
997
+ const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
998
+
999
+ opts.signal = combined
1000
+ }
1001
+
1002
+ // Strip openai itemId metadata following what codex does
1003
+ // Codex uses #[serde(skip_serializing)] on id fields for all item types:
1004
+ // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall
1005
+ // IDs are only re-attached for Azure with store=true
1006
+ if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
1007
+ const body = JSON.parse(opts.body as string)
1008
+ const isAzure = model.providerID.includes("azure")
1009
+ const keepIds = isAzure && body.store === true
1010
+ if (!keepIds && Array.isArray(body.input)) {
1011
+ for (const item of body.input) {
1012
+ if ("id" in item) {
1013
+ delete item.id
1014
+ }
1015
+ }
1016
+ opts.body = JSON.stringify(body)
1017
+ }
1018
+ }
1019
+
1020
+ return fetchFn(input, {
1021
+ ...opts,
1022
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
1023
+ timeout: false,
1024
+ })
1025
+ }
1026
+
1027
+ // Special case: google-vertex-anthropic uses a subpath import
1028
+ const bundledKey =
1029
+ model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
1030
+ const bundledFn = BUNDLED_PROVIDERS[bundledKey]
1031
+ if (bundledFn) {
1032
+ log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
1033
+ const loaded = bundledFn({
1034
+ name: model.providerID,
1035
+ ...options,
1036
+ })
1037
+ s.sdk.set(key, loaded)
1038
+ return loaded as SDK
1039
+ }
1040
+
1041
+ let installedPath: string
1042
+ if (!model.api.npm.startsWith("file://")) {
1043
+ installedPath = await BunProc.install(model.api.npm, "latest")
1044
+ } else {
1045
+ log.info("loading local provider", { pkg: model.api.npm })
1046
+ installedPath = model.api.npm
1047
+ }
1048
+
1049
+ const mod = await import(installedPath)
1050
+
1051
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
1052
+ const loaded = fn({
1053
+ name: model.providerID,
1054
+ ...options,
1055
+ })
1056
+ s.sdk.set(key, loaded)
1057
+ return loaded as SDK
1058
+ } catch (e) {
1059
+ throw new InitError({ providerID: model.providerID }, { cause: e })
1060
+ }
1061
+ }
1062
+
1063
+ export async function getProvider(providerID: string) {
1064
+ return state().then((s) => s.providers[providerID])
1065
+ }
1066
+
1067
+ export async function getModel(providerID: string, modelID: string) {
1068
+ const s = await state()
1069
+ const provider = s.providers[providerID]
1070
+ if (!provider) {
1071
+ const availableProviders = Object.keys(s.providers)
1072
+ const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 })
1073
+ const suggestions = matches.map((m) => m.target)
1074
+ throw new ModelNotFoundError({ providerID, modelID, suggestions })
1075
+ }
1076
+
1077
+ const info = provider.models[modelID]
1078
+ if (!info) {
1079
+ const availableModels = Object.keys(provider.models)
1080
+ const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
1081
+ const suggestions = matches.map((m) => m.target)
1082
+ throw new ModelNotFoundError({ providerID, modelID, suggestions })
1083
+ }
1084
+ return info
1085
+ }
1086
+
1087
+ export async function getLanguage(model: Model): Promise<LanguageModelV2> {
1088
+ const s = await state()
1089
+ const key = `${model.providerID}/${model.id}`
1090
+ if (s.models.has(key)) return s.models.get(key)!
1091
+
1092
+ const provider = s.providers[model.providerID]
1093
+ const sdk = await getSDK(model)
1094
+
1095
+ try {
1096
+ const language = s.modelLoaders[model.providerID]
1097
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
1098
+ : sdk.languageModel(model.api.id)
1099
+ s.models.set(key, language)
1100
+ return language
1101
+ } catch (e) {
1102
+ if (e instanceof NoSuchModelError)
1103
+ throw new ModelNotFoundError(
1104
+ {
1105
+ modelID: model.id,
1106
+ providerID: model.providerID,
1107
+ },
1108
+ { cause: e },
1109
+ )
1110
+ throw e
1111
+ }
1112
+ }
1113
+
1114
+ export async function closest(providerID: string, query: string[]) {
1115
+ const s = await state()
1116
+ const provider = s.providers[providerID]
1117
+ if (!provider) return undefined
1118
+ for (const item of query) {
1119
+ for (const modelID of Object.keys(provider.models)) {
1120
+ if (modelID.includes(item))
1121
+ return {
1122
+ providerID,
1123
+ modelID,
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ export async function getSmallModel(providerID: string) {
1130
+ const cfg = await Config.get()
1131
+
1132
+ if (cfg.small_model) {
1133
+ const parsed = parseModel(cfg.small_model)
1134
+ return getModel(parsed.providerID, parsed.modelID)
1135
+ }
1136
+
1137
+ const provider = await state().then((state) => state.providers[providerID])
1138
+ if (provider) {
1139
+ let priority = [
1140
+ "claude-haiku-4-5",
1141
+ "claude-haiku-4.5",
1142
+ "3-5-haiku",
1143
+ "3.5-haiku",
1144
+ "gemini-3-flash",
1145
+ "gemini-2.5-flash",
1146
+ "gpt-5-nano",
1147
+ ]
1148
+ if (providerID.startsWith("jonsoc")) {
1149
+ priority = ["gpt-5-nano"]
1150
+ }
1151
+ if (providerID.startsWith("github-copilot")) {
1152
+ // prioritize free models for github copilot
1153
+ priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
1154
+ }
1155
+ for (const item of priority) {
1156
+ for (const model of Object.keys(provider.models)) {
1157
+ if (model.includes(item)) return getModel(providerID, model)
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ // Check if jonsoc provider is available before using it
1163
+ const jonsocProvider = await state().then((state) => state.providers["jonsoc"])
1164
+ if (jonsocProvider && jonsocProvider.models["gpt-5-nano"]) {
1165
+ return getModel("jonsoc", "gpt-5-nano")
1166
+ }
1167
+
1168
+ return undefined
1169
+ }
1170
+
1171
+ const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
1172
+ export function sort(models: Model[]) {
1173
+ return sortBy(
1174
+ models,
1175
+ [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
1176
+ [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
1177
+ [(model) => model.id, "desc"],
1178
+ )
1179
+ }
1180
+
1181
+ export async function defaultModel() {
1182
+ const cfg = await Config.get()
1183
+ if (cfg.model) return parseModel(cfg.model)
1184
+
1185
+ const provider = await list()
1186
+ .then((val) => Object.values(val))
1187
+ .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
1188
+ if (!provider) throw new Error("no providers found")
1189
+ const [model] = sort(Object.values(provider.models))
1190
+ if (!model) throw new Error("no models found")
1191
+ return {
1192
+ providerID: provider.id,
1193
+ modelID: model.id,
1194
+ }
1195
+ }
1196
+
1197
+ export function parseModel(model: string) {
1198
+ const [providerID, ...rest] = model.split("/")
1199
+ return {
1200
+ providerID: providerID,
1201
+ modelID: rest.join("/"),
1202
+ }
1203
+ }
1204
+
1205
+ export const ModelNotFoundError = NamedError.create(
1206
+ "ProviderModelNotFoundError",
1207
+ z.object({
1208
+ providerID: z.string(),
1209
+ modelID: z.string(),
1210
+ suggestions: z.array(z.string()).optional(),
1211
+ }),
1212
+ )
1213
+
1214
+ export const InitError = NamedError.create(
1215
+ "ProviderInitError",
1216
+ z.object({
1217
+ providerID: z.string(),
1218
+ }),
1219
+ )
1220
+ }