cerebras-cli 1.0.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 (314) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +10 -0
  3. package/README.md +15 -0
  4. package/bin/opencode +84 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +128 -0
  7. package/parsers-config.ts +239 -0
  8. package/script/build.ts +151 -0
  9. package/script/postinstall.mjs +122 -0
  10. package/script/publish.ts +256 -0
  11. package/script/schema.ts +47 -0
  12. package/src/acp/README.md +164 -0
  13. package/src/acp/agent.ts +812 -0
  14. package/src/acp/session.ts +70 -0
  15. package/src/acp/types.ts +22 -0
  16. package/src/agent/agent.ts +310 -0
  17. package/src/agent/generate.txt +75 -0
  18. package/src/auth/index.ts +70 -0
  19. package/src/bun/index.ts +152 -0
  20. package/src/bus/global.ts +10 -0
  21. package/src/bus/index.ts +142 -0
  22. package/src/cli/bootstrap.ts +17 -0
  23. package/src/cli/cmd/acp.ts +88 -0
  24. package/src/cli/cmd/agent.ts +165 -0
  25. package/src/cli/cmd/auth.ts +369 -0
  26. package/src/cli/cmd/cmd.ts +7 -0
  27. package/src/cli/cmd/debug/config.ts +15 -0
  28. package/src/cli/cmd/debug/file.ts +91 -0
  29. package/src/cli/cmd/debug/index.ts +41 -0
  30. package/src/cli/cmd/debug/lsp.ts +47 -0
  31. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  32. package/src/cli/cmd/debug/scrap.ts +15 -0
  33. package/src/cli/cmd/debug/snapshot.ts +48 -0
  34. package/src/cli/cmd/export.ts +88 -0
  35. package/src/cli/cmd/generate.ts +38 -0
  36. package/src/cli/cmd/github.ts +1200 -0
  37. package/src/cli/cmd/import.ts +98 -0
  38. package/src/cli/cmd/mcp.ts +400 -0
  39. package/src/cli/cmd/models.ts +77 -0
  40. package/src/cli/cmd/pr.ts +112 -0
  41. package/src/cli/cmd/run.ts +342 -0
  42. package/src/cli/cmd/serve.ts +31 -0
  43. package/src/cli/cmd/session.ts +106 -0
  44. package/src/cli/cmd/stats.ts +298 -0
  45. package/src/cli/cmd/tui/app.tsx +732 -0
  46. package/src/cli/cmd/tui/attach.ts +25 -0
  47. package/src/cli/cmd/tui/component/border.tsx +21 -0
  48. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  49. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  50. package/src/cli/cmd/tui/component/dialog-feedback.tsx +160 -0
  51. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  52. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  53. package/src/cli/cmd/tui/component/dialog-notification.tsx +78 -0
  54. package/src/cli/cmd/tui/component/dialog-provider.tsx +222 -0
  55. package/src/cli/cmd/tui/component/dialog-session-list.tsx +97 -0
  56. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  57. package/src/cli/cmd/tui/component/dialog-status.tsx +114 -0
  58. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  59. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  60. package/src/cli/cmd/tui/component/logo.tsx +37 -0
  61. package/src/cli/cmd/tui/component/notification-banner.tsx +58 -0
  62. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +530 -0
  63. package/src/cli/cmd/tui/component/prompt/history.tsx +107 -0
  64. package/src/cli/cmd/tui/component/prompt/index.tsx +931 -0
  65. package/src/cli/cmd/tui/context/args.tsx +14 -0
  66. package/src/cli/cmd/tui/context/directory.ts +12 -0
  67. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  68. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  69. package/src/cli/cmd/tui/context/keybind.tsx +111 -0
  70. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  71. package/src/cli/cmd/tui/context/local.tsx +339 -0
  72. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/route.tsx +45 -0
  74. package/src/cli/cmd/tui/context/sdk.tsx +75 -0
  75. package/src/cli/cmd/tui/context/sync.tsx +374 -0
  76. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  77. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  78. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  79. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  80. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  81. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  82. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  83. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  84. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  85. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  86. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  87. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  88. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  89. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  90. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  91. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  92. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  93. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  94. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  95. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  96. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  97. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  98. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  99. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  100. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  101. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  102. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  103. package/src/cli/cmd/tui/context/theme.tsx +1077 -0
  104. package/src/cli/cmd/tui/event.ts +39 -0
  105. package/src/cli/cmd/tui/routes/home.tsx +104 -0
  106. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +93 -0
  107. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +37 -0
  108. package/src/cli/cmd/tui/routes/session/footer.tsx +76 -0
  109. package/src/cli/cmd/tui/routes/session/header.tsx +183 -0
  110. package/src/cli/cmd/tui/routes/session/index.tsx +1703 -0
  111. package/src/cli/cmd/tui/routes/session/sidebar.tsx +586 -0
  112. package/src/cli/cmd/tui/spawn.ts +60 -0
  113. package/src/cli/cmd/tui/thread.ts +120 -0
  114. package/src/cli/cmd/tui/ui/dialog-alert.tsx +55 -0
  115. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +81 -0
  116. package/src/cli/cmd/tui/ui/dialog-help.tsx +36 -0
  117. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +75 -0
  118. package/src/cli/cmd/tui/ui/dialog-select.tsx +317 -0
  119. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  120. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  121. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  122. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  123. package/src/cli/cmd/tui/util/editor.ts +32 -0
  124. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  125. package/src/cli/cmd/tui/worker.ts +63 -0
  126. package/src/cli/cmd/uninstall.ts +344 -0
  127. package/src/cli/cmd/upgrade.ts +67 -0
  128. package/src/cli/cmd/web.ts +84 -0
  129. package/src/cli/error.ts +55 -0
  130. package/src/cli/ui.ts +84 -0
  131. package/src/cli/upgrade.ts +25 -0
  132. package/src/command/index.ts +79 -0
  133. package/src/command/template/initialize.txt +10 -0
  134. package/src/command/template/review.txt +73 -0
  135. package/src/config/config.ts +886 -0
  136. package/src/config/markdown.ts +41 -0
  137. package/src/env/index.ts +26 -0
  138. package/src/file/fzf.ts +124 -0
  139. package/src/file/ignore.ts +83 -0
  140. package/src/file/index.ts +326 -0
  141. package/src/file/ripgrep.ts +391 -0
  142. package/src/file/time.ts +38 -0
  143. package/src/file/watcher.ts +89 -0
  144. package/src/flag/flag.ts +28 -0
  145. package/src/format/formatter.ts +277 -0
  146. package/src/format/index.ts +137 -0
  147. package/src/global/index.ts +52 -0
  148. package/src/id/id.ts +73 -0
  149. package/src/ide/index.ts +75 -0
  150. package/src/index.ts +158 -0
  151. package/src/installation/index.ts +194 -0
  152. package/src/lsp/client.ts +215 -0
  153. package/src/lsp/index.ts +370 -0
  154. package/src/lsp/language.ts +111 -0
  155. package/src/lsp/server.ts +1327 -0
  156. package/src/mcp/auth.ts +82 -0
  157. package/src/mcp/index.ts +576 -0
  158. package/src/mcp/oauth-callback.ts +203 -0
  159. package/src/mcp/oauth-provider.ts +132 -0
  160. package/src/notification/index.ts +101 -0
  161. package/src/patch/index.ts +622 -0
  162. package/src/permission/index.ts +198 -0
  163. package/src/plugin/index.ts +95 -0
  164. package/src/project/bootstrap.ts +31 -0
  165. package/src/project/instance.ts +68 -0
  166. package/src/project/project.ts +133 -0
  167. package/src/project/state.ts +65 -0
  168. package/src/project/vcs.ts +77 -0
  169. package/src/provider/auth.ts +143 -0
  170. package/src/provider/models-macro.ts +11 -0
  171. package/src/provider/models.ts +93 -0
  172. package/src/provider/provider.ts +996 -0
  173. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  174. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  175. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  176. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  177. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +27 -0
  178. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  179. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  180. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  181. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  182. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  183. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  184. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  185. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  190. package/src/provider/transform.ts +406 -0
  191. package/src/pty/index.ts +226 -0
  192. package/src/ratelimit/index.ts +185 -0
  193. package/src/server/error.ts +36 -0
  194. package/src/server/project.ts +50 -0
  195. package/src/server/server.ts +2463 -0
  196. package/src/server/tui.ts +71 -0
  197. package/src/session/compaction.ts +257 -0
  198. package/src/session/index.ts +470 -0
  199. package/src/session/message-v2.ts +641 -0
  200. package/src/session/message.ts +189 -0
  201. package/src/session/processor.ts +443 -0
  202. package/src/session/prompt/anthropic-20250930.txt +166 -0
  203. package/src/session/prompt/anthropic.txt +105 -0
  204. package/src/session/prompt/anthropic_spoof.txt +1 -0
  205. package/src/session/prompt/beast.txt +147 -0
  206. package/src/session/prompt/build-switch.txt +5 -0
  207. package/src/session/prompt/codex.txt +318 -0
  208. package/src/session/prompt/compaction.txt +12 -0
  209. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  210. package/src/session/prompt/gemini.txt +155 -0
  211. package/src/session/prompt/max-steps.txt +16 -0
  212. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  213. package/src/session/prompt/plan.txt +26 -0
  214. package/src/session/prompt/polaris.txt +107 -0
  215. package/src/session/prompt/qwen.txt +109 -0
  216. package/src/session/prompt/summarize.txt +4 -0
  217. package/src/session/prompt/title.txt +36 -0
  218. package/src/session/prompt.ts +1541 -0
  219. package/src/session/retry.ts +82 -0
  220. package/src/session/revert.ts +108 -0
  221. package/src/session/status.ts +75 -0
  222. package/src/session/summary.ts +203 -0
  223. package/src/session/system.ts +148 -0
  224. package/src/session/todo.ts +36 -0
  225. package/src/share/share-next.ts +195 -0
  226. package/src/share/share.ts +87 -0
  227. package/src/snapshot/index.ts +197 -0
  228. package/src/storage/storage.ts +226 -0
  229. package/src/telemetry/index.ts +232 -0
  230. package/src/tool/bash.ts +365 -0
  231. package/src/tool/bash.txt +128 -0
  232. package/src/tool/batch.ts +173 -0
  233. package/src/tool/batch.txt +28 -0
  234. package/src/tool/codesearch.ts +138 -0
  235. package/src/tool/codesearch.txt +12 -0
  236. package/src/tool/edit.ts +674 -0
  237. package/src/tool/edit.txt +10 -0
  238. package/src/tool/glob.ts +65 -0
  239. package/src/tool/glob.txt +6 -0
  240. package/src/tool/grep.ts +120 -0
  241. package/src/tool/grep.txt +8 -0
  242. package/src/tool/invalid.ts +17 -0
  243. package/src/tool/ls.ts +110 -0
  244. package/src/tool/ls.txt +1 -0
  245. package/src/tool/lsp-diagnostics.ts +26 -0
  246. package/src/tool/lsp-diagnostics.txt +1 -0
  247. package/src/tool/lsp-hover.ts +31 -0
  248. package/src/tool/lsp-hover.txt +1 -0
  249. package/src/tool/multiedit.ts +46 -0
  250. package/src/tool/multiedit.txt +41 -0
  251. package/src/tool/patch.ts +233 -0
  252. package/src/tool/patch.txt +1 -0
  253. package/src/tool/read.ts +217 -0
  254. package/src/tool/read.txt +12 -0
  255. package/src/tool/registry.ts +148 -0
  256. package/src/tool/task.ts +135 -0
  257. package/src/tool/task.txt +60 -0
  258. package/src/tool/todo.ts +39 -0
  259. package/src/tool/todoread.txt +14 -0
  260. package/src/tool/todowrite.txt +167 -0
  261. package/src/tool/tool.ts +66 -0
  262. package/src/tool/webfetch.ts +187 -0
  263. package/src/tool/webfetch.txt +14 -0
  264. package/src/tool/websearch.ts +150 -0
  265. package/src/tool/websearch.txt +11 -0
  266. package/src/tool/write.ts +99 -0
  267. package/src/tool/write.txt +8 -0
  268. package/src/types/shims.d.ts +3 -0
  269. package/src/util/color.ts +19 -0
  270. package/src/util/context.ts +25 -0
  271. package/src/util/defer.ts +12 -0
  272. package/src/util/eventloop.ts +20 -0
  273. package/src/util/filesystem.ts +69 -0
  274. package/src/util/fn.ts +11 -0
  275. package/src/util/iife.ts +3 -0
  276. package/src/util/keybind.ts +79 -0
  277. package/src/util/lazy.ts +11 -0
  278. package/src/util/locale.ts +81 -0
  279. package/src/util/lock.ts +98 -0
  280. package/src/util/log.ts +177 -0
  281. package/src/util/queue.ts +32 -0
  282. package/src/util/rpc.ts +42 -0
  283. package/src/util/scrap.ts +10 -0
  284. package/src/util/signal.ts +12 -0
  285. package/src/util/timeout.ts +14 -0
  286. package/src/util/token.ts +7 -0
  287. package/src/util/wildcard.ts +54 -0
  288. package/sst-env.d.ts +9 -0
  289. package/test/bun.test.ts +53 -0
  290. package/test/config/agent-color.test.ts +66 -0
  291. package/test/config/config.test.ts +503 -0
  292. package/test/config/markdown.test.ts +89 -0
  293. package/test/file/ignore.test.ts +10 -0
  294. package/test/fixture/fixture.ts +28 -0
  295. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  296. package/test/ide/ide.test.ts +82 -0
  297. package/test/keybind.test.ts +317 -0
  298. package/test/lsp/client.test.ts +95 -0
  299. package/test/patch/patch.test.ts +348 -0
  300. package/test/preload.ts +38 -0
  301. package/test/project/project.test.ts +42 -0
  302. package/test/provider/provider.test.ts +1809 -0
  303. package/test/provider/transform.test.ts +305 -0
  304. package/test/session/retry.test.ts +61 -0
  305. package/test/session/session.test.ts +71 -0
  306. package/test/snapshot/snapshot.test.ts +939 -0
  307. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  308. package/test/tool/bash.test.ts +55 -0
  309. package/test/tool/patch.test.ts +259 -0
  310. package/test/util/iife.test.ts +36 -0
  311. package/test/util/lazy.test.ts +50 -0
  312. package/test/util/timeout.test.ts +21 -0
  313. package/test/util/wildcard.test.ts +55 -0
  314. package/tsconfig.json +17 -0
@@ -0,0 +1,996 @@
1
+ import z from "zod"
2
+ import fuzzysort from "fuzzysort"
3
+ import { Config } from "../config/config"
4
+ import { mapValues, mergeDeep, 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 "@opencode-ai/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 { RateLimit } from "@/ratelimit"
17
+
18
+ // Direct imports for bundled providers
19
+ import { createAmazonBedrock } 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
+
30
+ export namespace Provider {
31
+ const log = Log.create({ service: "provider" })
32
+
33
+ const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
34
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
35
+ "@ai-sdk/anthropic": createAnthropic,
36
+ "@ai-sdk/azure": createAzure,
37
+ "@ai-sdk/google": createGoogleGenerativeAI,
38
+ "@ai-sdk/google-vertex": createVertex,
39
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
40
+ "@ai-sdk/openai": createOpenAI,
41
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
42
+ "@openrouter/ai-sdk-provider": createOpenRouter,
43
+ // @ts-ignore (TODO: kill this code so we dont have to maintain it)
44
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
45
+ // Use OpenAI-compatible SDK for Cerebras to handle response format differences
46
+ "@ai-sdk/cerebras": createOpenAICompatible,
47
+ }
48
+
49
+ type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
50
+ type CustomLoader = (provider: Info) => Promise<{
51
+ autoload: boolean
52
+ getModel?: CustomModelLoader
53
+ options?: Record<string, any>
54
+ }>
55
+
56
+ const CUSTOM_LOADERS: Record<string, CustomLoader> = {
57
+ async anthropic() {
58
+ return {
59
+ autoload: false,
60
+ options: {
61
+ headers: {
62
+ "anthropic-beta":
63
+ "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
64
+ },
65
+ },
66
+ }
67
+ },
68
+ async opencode(input) {
69
+ const hasKey = await (async () => {
70
+ const env = Env.all()
71
+ if (input.env.some((item) => env[item])) return true
72
+ if (await Auth.get(input.id)) return true
73
+ return false
74
+ })()
75
+
76
+ if (!hasKey) {
77
+ for (const [key, value] of Object.entries(input.models)) {
78
+ if (value.cost.input === 0) continue
79
+ delete input.models[key]
80
+ }
81
+ }
82
+
83
+ return {
84
+ autoload: Object.keys(input.models).length > 0,
85
+ options: hasKey ? {} : { apiKey: "public" },
86
+ }
87
+ },
88
+ openai: async () => {
89
+ return {
90
+ autoload: false,
91
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
92
+ return sdk.responses(modelID)
93
+ },
94
+ options: {},
95
+ }
96
+ },
97
+ cerebras: async () => {
98
+ return {
99
+ autoload: false,
100
+ options: {
101
+ baseURL: "https://api.cerebras.ai/v1",
102
+ },
103
+ }
104
+ },
105
+ "github-copilot": async () => {
106
+ return {
107
+ autoload: false,
108
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
109
+ if (modelID.includes("codex")) {
110
+ return sdk.responses(modelID)
111
+ }
112
+ return sdk.chat(modelID)
113
+ },
114
+ options: {},
115
+ }
116
+ },
117
+ "github-copilot-enterprise": async () => {
118
+ return {
119
+ autoload: false,
120
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
121
+ if (modelID.includes("codex")) {
122
+ return sdk.responses(modelID)
123
+ }
124
+ return sdk.chat(modelID)
125
+ },
126
+ options: {},
127
+ }
128
+ },
129
+ azure: async () => {
130
+ return {
131
+ autoload: false,
132
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
133
+ if (options?.["useCompletionUrls"]) {
134
+ return sdk.chat(modelID)
135
+ } else {
136
+ return sdk.responses(modelID)
137
+ }
138
+ },
139
+ options: {},
140
+ }
141
+ },
142
+ "azure-cognitive-services": async () => {
143
+ const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
144
+ return {
145
+ autoload: false,
146
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
147
+ if (options?.["useCompletionUrls"]) {
148
+ return sdk.chat(modelID)
149
+ } else {
150
+ return sdk.responses(modelID)
151
+ }
152
+ },
153
+ options: {
154
+ baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
155
+ },
156
+ }
157
+ },
158
+ "amazon-bedrock": async () => {
159
+ const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
160
+ Env.get("AWS_PROFILE"),
161
+ Env.get("AWS_ACCESS_KEY_ID"),
162
+ Env.get("AWS_BEARER_TOKEN_BEDROCK"),
163
+ Env.get("AWS_REGION"),
164
+ ])
165
+ if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
166
+
167
+ const region = awsRegion ?? "us-east-1"
168
+
169
+ const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
170
+ return {
171
+ autoload: true,
172
+ options: {
173
+ region,
174
+ credentialProvider: fromNodeProviderChain(),
175
+ },
176
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
177
+ // Skip region prefixing if model already has global prefix
178
+ if (modelID.startsWith("global.")) {
179
+ return sdk.languageModel(modelID)
180
+ }
181
+
182
+ let regionPrefix = region.split("-")[0]
183
+
184
+ switch (regionPrefix) {
185
+ case "us": {
186
+ const modelRequiresPrefix = [
187
+ "nova-micro",
188
+ "nova-lite",
189
+ "nova-pro",
190
+ "nova-premier",
191
+ "claude",
192
+ "deepseek",
193
+ ].some((m) => modelID.includes(m))
194
+ const isGovCloud = region.startsWith("us-gov")
195
+ if (modelRequiresPrefix && !isGovCloud) {
196
+ modelID = `${regionPrefix}.${modelID}`
197
+ }
198
+ break
199
+ }
200
+ case "eu": {
201
+ const regionRequiresPrefix = [
202
+ "eu-west-1",
203
+ "eu-west-2",
204
+ "eu-west-3",
205
+ "eu-north-1",
206
+ "eu-central-1",
207
+ "eu-south-1",
208
+ "eu-south-2",
209
+ ].some((r) => region.includes(r))
210
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
211
+ modelID.includes(m),
212
+ )
213
+ if (regionRequiresPrefix && modelRequiresPrefix) {
214
+ modelID = `${regionPrefix}.${modelID}`
215
+ }
216
+ break
217
+ }
218
+ case "ap": {
219
+ const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
220
+ if (
221
+ isAustraliaRegion &&
222
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
223
+ ) {
224
+ regionPrefix = "au"
225
+ modelID = `${regionPrefix}.${modelID}`
226
+ } else {
227
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
228
+ modelID.includes(m),
229
+ )
230
+ if (modelRequiresPrefix) {
231
+ regionPrefix = "apac"
232
+ modelID = `${regionPrefix}.${modelID}`
233
+ }
234
+ }
235
+ break
236
+ }
237
+ }
238
+
239
+ return sdk.languageModel(modelID)
240
+ },
241
+ }
242
+ },
243
+ openrouter: async () => {
244
+ return {
245
+ autoload: false,
246
+ options: {
247
+ headers: {
248
+ "HTTP-Referer": "https://opencode.ai/",
249
+ "X-Title": "opencode",
250
+ },
251
+ },
252
+ }
253
+ },
254
+ vercel: async () => {
255
+ return {
256
+ autoload: false,
257
+ options: {
258
+ headers: {
259
+ "http-referer": "https://opencode.ai/",
260
+ "x-title": "opencode",
261
+ },
262
+ },
263
+ }
264
+ },
265
+ "google-vertex": async () => {
266
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
267
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
268
+ const autoload = Boolean(project)
269
+ if (!autoload) return { autoload: false }
270
+ return {
271
+ autoload: true,
272
+ options: {
273
+ project,
274
+ location,
275
+ },
276
+ async getModel(sdk: any, modelID: string) {
277
+ const id = String(modelID).trim()
278
+ return sdk.languageModel(id)
279
+ },
280
+ }
281
+ },
282
+ "google-vertex-anthropic": async () => {
283
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
284
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
285
+ const autoload = Boolean(project)
286
+ if (!autoload) return { autoload: false }
287
+ return {
288
+ autoload: true,
289
+ options: {
290
+ project,
291
+ location,
292
+ },
293
+ async getModel(sdk: any, modelID) {
294
+ const id = String(modelID).trim()
295
+ return sdk.languageModel(id)
296
+ },
297
+ }
298
+ },
299
+ "sap-ai-core": async () => {
300
+ const auth = await Auth.get("sap-ai-core")
301
+ const envServiceKey = iife(() => {
302
+ const envAICoreServiceKey = Env.get("AICORE_SERVICE_KEY")
303
+ if (envAICoreServiceKey) return envAICoreServiceKey
304
+ if (auth?.type === "api") {
305
+ Env.set("AICORE_SERVICE_KEY", auth.key)
306
+ return auth.key
307
+ }
308
+ return undefined
309
+ })
310
+ const deploymentId = Env.get("AICORE_DEPLOYMENT_ID")
311
+ const resourceGroup = Env.get("AICORE_RESOURCE_GROUP")
312
+
313
+ return {
314
+ autoload: !!envServiceKey,
315
+ options: envServiceKey ? { deploymentId, resourceGroup } : {},
316
+ async getModel(sdk: any, modelID: string) {
317
+ return sdk(modelID)
318
+ },
319
+ }
320
+ },
321
+ zenmux: async () => {
322
+ return {
323
+ autoload: false,
324
+ options: {
325
+ headers: {
326
+ "HTTP-Referer": "https://opencode.ai/",
327
+ "X-Title": "opencode",
328
+ },
329
+ },
330
+ }
331
+ },
332
+ }
333
+
334
+ export const Model = z
335
+ .object({
336
+ id: z.string(),
337
+ providerID: z.string(),
338
+ api: z.object({
339
+ id: z.string(),
340
+ url: z.string(),
341
+ npm: z.string(),
342
+ }),
343
+ name: z.string(),
344
+ capabilities: z.object({
345
+ temperature: z.boolean(),
346
+ reasoning: z.boolean(),
347
+ attachment: z.boolean(),
348
+ toolcall: z.boolean(),
349
+ input: z.object({
350
+ text: z.boolean(),
351
+ audio: z.boolean(),
352
+ image: z.boolean(),
353
+ video: z.boolean(),
354
+ pdf: z.boolean(),
355
+ }),
356
+ output: z.object({
357
+ text: z.boolean(),
358
+ audio: z.boolean(),
359
+ image: z.boolean(),
360
+ video: z.boolean(),
361
+ pdf: z.boolean(),
362
+ }),
363
+ }),
364
+ cost: z.object({
365
+ input: z.number(),
366
+ output: z.number(),
367
+ cache: z.object({
368
+ read: z.number(),
369
+ write: z.number(),
370
+ }),
371
+ experimentalOver200K: z
372
+ .object({
373
+ input: z.number(),
374
+ output: z.number(),
375
+ cache: z.object({
376
+ read: z.number(),
377
+ write: z.number(),
378
+ }),
379
+ })
380
+ .optional(),
381
+ }),
382
+ limit: z.object({
383
+ context: z.number(),
384
+ output: z.number(),
385
+ }),
386
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
387
+ options: z.record(z.string(), z.any()),
388
+ headers: z.record(z.string(), z.string()),
389
+ })
390
+ .meta({
391
+ ref: "Model",
392
+ })
393
+ export type Model = z.infer<typeof Model>
394
+
395
+ export const Info = z
396
+ .object({
397
+ id: z.string(),
398
+ name: z.string(),
399
+ source: z.enum(["env", "config", "custom", "api"]),
400
+ env: z.string().array(),
401
+ key: z.string().optional(),
402
+ options: z.record(z.string(), z.any()),
403
+ models: z.record(z.string(), Model),
404
+ })
405
+ .meta({
406
+ ref: "Provider",
407
+ })
408
+ export type Info = z.infer<typeof Info>
409
+
410
+ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
411
+ return {
412
+ id: model.id,
413
+ providerID: provider.id,
414
+ name: model.name,
415
+ api: {
416
+ id: model.id,
417
+ url: provider.api!,
418
+ npm: model.provider?.npm ?? provider.npm ?? provider.id,
419
+ },
420
+ status: model.status ?? "active",
421
+ headers: model.headers ?? {},
422
+ options: model.options ?? {},
423
+ cost: {
424
+ input: model.cost?.input ?? 0,
425
+ output: model.cost?.output ?? 0,
426
+ cache: {
427
+ read: model.cost?.cache_read ?? 0,
428
+ write: model.cost?.cache_write ?? 0,
429
+ },
430
+ experimentalOver200K: model.cost?.context_over_200k
431
+ ? {
432
+ cache: {
433
+ read: model.cost.context_over_200k.cache_read ?? 0,
434
+ write: model.cost.context_over_200k.cache_write ?? 0,
435
+ },
436
+ input: model.cost.context_over_200k.input,
437
+ output: model.cost.context_over_200k.output,
438
+ }
439
+ : undefined,
440
+ },
441
+ limit: {
442
+ context: model.limit.context,
443
+ output: model.limit.output,
444
+ },
445
+ capabilities: {
446
+ temperature: model.temperature,
447
+ reasoning: model.reasoning,
448
+ attachment: model.attachment,
449
+ toolcall: model.tool_call,
450
+ input: {
451
+ text: model.modalities?.input?.includes("text") ?? false,
452
+ audio: model.modalities?.input?.includes("audio") ?? false,
453
+ image: model.modalities?.input?.includes("image") ?? false,
454
+ video: model.modalities?.input?.includes("video") ?? false,
455
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
456
+ },
457
+ output: {
458
+ text: model.modalities?.output?.includes("text") ?? false,
459
+ audio: model.modalities?.output?.includes("audio") ?? false,
460
+ image: model.modalities?.output?.includes("image") ?? false,
461
+ video: model.modalities?.output?.includes("video") ?? false,
462
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
463
+ },
464
+ },
465
+ }
466
+ }
467
+
468
+ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
469
+ return {
470
+ id: provider.id,
471
+ source: "custom",
472
+ name: provider.name,
473
+ env: provider.env ?? [],
474
+ options: {},
475
+ models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
476
+ }
477
+ }
478
+
479
+ const state = Instance.state(async () => {
480
+ using _ = log.time("state")
481
+ const config = await Config.get()
482
+ const modelsDev = await ModelsDev.get()
483
+ const database = mapValues(modelsDev, fromModelsDevProvider)
484
+
485
+ const disabled = new Set(config.disabled_providers ?? [])
486
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
487
+
488
+ function isProviderAllowed(providerID: string): boolean {
489
+ if (enabled && !enabled.has(providerID)) return false
490
+ if (disabled.has(providerID)) return false
491
+ return true
492
+ }
493
+
494
+ const providers: { [providerID: string]: Info } = {}
495
+ const languages = new Map<string, LanguageModelV2>()
496
+ const modelLoaders: {
497
+ [providerID: string]: CustomModelLoader
498
+ } = {}
499
+ const sdk = new Map<number, SDK>()
500
+
501
+ log.info("init")
502
+
503
+ const configProviders = Object.entries(config.provider ?? {})
504
+
505
+ // Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
506
+ if (database["github-copilot"]) {
507
+ const githubCopilot = database["github-copilot"]
508
+ database["github-copilot-enterprise"] = {
509
+ ...githubCopilot,
510
+ id: "github-copilot-enterprise",
511
+ name: "GitHub Copilot Enterprise",
512
+ models: mapValues(githubCopilot.models, (model) => ({
513
+ ...model,
514
+ providerID: "github-copilot-enterprise",
515
+ })),
516
+ }
517
+ }
518
+
519
+ function mergeProvider(providerID: string, provider: Partial<Info>) {
520
+ const existing = providers[providerID]
521
+ if (existing) {
522
+ // @ts-expect-error
523
+ providers[providerID] = mergeDeep(existing, provider)
524
+ return
525
+ }
526
+ const match = database[providerID]
527
+ if (!match) return
528
+ // @ts-expect-error
529
+ providers[providerID] = mergeDeep(match, provider)
530
+ }
531
+
532
+ // extend database from config
533
+ for (const [providerID, provider] of configProviders) {
534
+ const existing = database[providerID]
535
+ const parsed: Info = {
536
+ id: providerID,
537
+ name: provider.name ?? existing?.name ?? providerID,
538
+ env: provider.env ?? existing?.env ?? [],
539
+ options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
540
+ source: "config",
541
+ models: existing?.models ?? {},
542
+ }
543
+
544
+ for (const [modelID, model] of Object.entries(provider.models ?? {})) {
545
+ const existingModel = parsed.models[model.id ?? modelID]
546
+ const name = iife(() => {
547
+ if (model.name) return model.name
548
+ if (model.id && model.id !== modelID) return modelID
549
+ return existingModel?.name ?? modelID
550
+ })
551
+ const parsedModel: Model = {
552
+ id: modelID,
553
+ api: {
554
+ id: model.id ?? existingModel?.api.id ?? modelID,
555
+ npm:
556
+ model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? providerID,
557
+ url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
558
+ },
559
+ status: model.status ?? existingModel?.status ?? "active",
560
+ name,
561
+ providerID,
562
+ capabilities: {
563
+ temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
564
+ reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
565
+ attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
566
+ toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
567
+ input: {
568
+ text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
569
+ audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
570
+ image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
571
+ video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
572
+ pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
573
+ },
574
+ output: {
575
+ text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
576
+ audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
577
+ image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
578
+ video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
579
+ pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
580
+ },
581
+ },
582
+ cost: {
583
+ input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
584
+ output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
585
+ cache: {
586
+ read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
587
+ write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
588
+ },
589
+ },
590
+ options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
591
+ limit: {
592
+ context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
593
+ output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
594
+ },
595
+ headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
596
+ }
597
+ parsed.models[modelID] = parsedModel
598
+ }
599
+ database[providerID] = parsed
600
+ }
601
+
602
+ // load env
603
+ const env = Env.all()
604
+ for (const [providerID, provider] of Object.entries(database)) {
605
+ if (disabled.has(providerID)) continue
606
+ const apiKey = provider.env.map((item) => env[item]).find(Boolean)
607
+ if (!apiKey) continue
608
+ mergeProvider(providerID, {
609
+ source: "env",
610
+ key: provider.env.length === 1 ? apiKey : undefined,
611
+ })
612
+ }
613
+
614
+ // load apikeys
615
+ for (const [providerID, provider] of Object.entries(await Auth.all())) {
616
+ if (disabled.has(providerID)) continue
617
+ if (provider.type === "api") {
618
+ mergeProvider(providerID, {
619
+ source: "api",
620
+ key: provider.key,
621
+ })
622
+ }
623
+ }
624
+
625
+ for (const plugin of await Plugin.list()) {
626
+ if (!plugin.auth) continue
627
+ const providerID = plugin.auth.provider
628
+ if (disabled.has(providerID)) continue
629
+
630
+ // For github-copilot plugin, check if auth exists for either github-copilot or github-copilot-enterprise
631
+ let hasAuth = false
632
+ const auth = await Auth.get(providerID)
633
+ if (auth) hasAuth = true
634
+
635
+ // Special handling for github-copilot: also check for enterprise auth
636
+ if (providerID === "github-copilot" && !hasAuth) {
637
+ const enterpriseAuth = await Auth.get("github-copilot-enterprise")
638
+ if (enterpriseAuth) hasAuth = true
639
+ }
640
+
641
+ if (!hasAuth) continue
642
+ if (!plugin.auth.loader) continue
643
+
644
+ // Load for the main provider if auth exists
645
+ if (auth) {
646
+ const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
647
+ mergeProvider(plugin.auth.provider, {
648
+ source: "custom",
649
+ options: options,
650
+ })
651
+ }
652
+
653
+ // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
654
+ if (providerID === "github-copilot") {
655
+ const enterpriseProviderID = "github-copilot-enterprise"
656
+ if (!disabled.has(enterpriseProviderID)) {
657
+ const enterpriseAuth = await Auth.get(enterpriseProviderID)
658
+ if (enterpriseAuth) {
659
+ const enterpriseOptions = await plugin.auth.loader(
660
+ () => Auth.get(enterpriseProviderID) as any,
661
+ database[enterpriseProviderID],
662
+ )
663
+ mergeProvider(enterpriseProviderID, {
664
+ source: "custom",
665
+ options: enterpriseOptions,
666
+ })
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
673
+ if (disabled.has(providerID)) continue
674
+ const result = await fn(database[providerID])
675
+ if (result && (result.autoload || providers[providerID])) {
676
+ if (result.getModel) modelLoaders[providerID] = result.getModel
677
+ mergeProvider(providerID, {
678
+ source: "custom",
679
+ options: result.options,
680
+ })
681
+ }
682
+ }
683
+
684
+ // load config
685
+ for (const [providerID, provider] of configProviders) {
686
+ const partial: Partial<Info> = { source: "config" }
687
+ if (provider.env) partial.env = provider.env
688
+ if (provider.name) partial.name = provider.name
689
+ if (provider.options) partial.options = provider.options
690
+ mergeProvider(providerID, partial)
691
+ }
692
+
693
+ for (const [providerID, provider] of Object.entries(providers)) {
694
+ if (!isProviderAllowed(providerID)) {
695
+ delete providers[providerID]
696
+ continue
697
+ }
698
+
699
+ if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
700
+ provider.models = mapValues(provider.models, (model) => ({
701
+ ...model,
702
+ api: {
703
+ ...model.api,
704
+ npm: "@ai-sdk/github-copilot",
705
+ },
706
+ }))
707
+ }
708
+
709
+ const configProvider = config.provider?.[providerID]
710
+
711
+ for (const [modelID, model] of Object.entries(provider.models)) {
712
+ model.api.id = model.api.id ?? model.id ?? modelID
713
+ if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
714
+ delete provider.models[modelID]
715
+ if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
716
+ if (
717
+ (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
718
+ (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
719
+ )
720
+ delete provider.models[modelID]
721
+ }
722
+
723
+ if (Object.keys(provider.models).length === 0) {
724
+ delete providers[providerID]
725
+ continue
726
+ }
727
+
728
+ log.info("found", { providerID })
729
+ }
730
+
731
+ return {
732
+ models: languages,
733
+ providers,
734
+ sdk,
735
+ modelLoaders,
736
+ }
737
+ })
738
+
739
+ export async function list() {
740
+ return state().then((state) => state.providers)
741
+ }
742
+
743
+ async function getSDK(model: Model) {
744
+ try {
745
+ using _ = log.time("getSDK", {
746
+ providerID: model.providerID,
747
+ })
748
+ const s = await state()
749
+ const provider = s.providers[model.providerID]
750
+ const options = { ...provider.options }
751
+
752
+ if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
753
+ options["includeUsage"] = true
754
+ }
755
+
756
+ if (!options["baseURL"]) options["baseURL"] = model.api.url
757
+ if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
758
+ if (model.headers)
759
+ options["headers"] = {
760
+ ...options["headers"],
761
+ ...model.headers,
762
+ }
763
+
764
+ const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
765
+ const existing = s.sdk.get(key)
766
+ if (existing) return existing
767
+
768
+ const customFetch = options["fetch"]
769
+
770
+ options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
771
+ // Preserve custom fetch if it exists, wrap it with timeout logic
772
+ const fetchFn = customFetch ?? fetch
773
+ const opts = init ?? {}
774
+
775
+ if (options["timeout"] !== undefined && options["timeout"] !== null) {
776
+ const signals: AbortSignal[] = []
777
+ if (opts.signal) signals.push(opts.signal)
778
+ if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
779
+
780
+ const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
781
+
782
+ opts.signal = combined
783
+ }
784
+
785
+ const response = await fetchFn(input, {
786
+ ...opts,
787
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
788
+ timeout: false,
789
+ })
790
+
791
+ // Capture rate limit headers from response
792
+ try {
793
+ const rateLimitInfo = RateLimit.parseHeaders(response.headers, model.providerID)
794
+ if (rateLimitInfo) {
795
+ RateLimit.setLatest(model.providerID, rateLimitInfo)
796
+ }
797
+ } catch {
798
+ // Ignore errors parsing rate limit headers
799
+ }
800
+
801
+ return response
802
+ }
803
+
804
+ // Special case: google-vertex-anthropic uses a subpath import
805
+ const bundledKey =
806
+ model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
807
+ const bundledFn = BUNDLED_PROVIDERS[bundledKey]
808
+ if (bundledFn) {
809
+ log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
810
+ const loaded = bundledFn({
811
+ name: model.providerID,
812
+ ...options,
813
+ })
814
+ s.sdk.set(key, loaded)
815
+ return loaded as SDK
816
+ }
817
+
818
+ let installedPath: string
819
+ if (!model.api.npm.startsWith("file://")) {
820
+ installedPath = await BunProc.install(model.api.npm, "latest")
821
+ } else {
822
+ log.info("loading local provider", { pkg: model.api.npm })
823
+ installedPath = model.api.npm
824
+ }
825
+
826
+ const mod = await import(installedPath)
827
+
828
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
829
+ const loaded = fn({
830
+ name: model.providerID,
831
+ ...options,
832
+ })
833
+ s.sdk.set(key, loaded)
834
+ return loaded as SDK
835
+ } catch (e) {
836
+ throw new InitError({ providerID: model.providerID }, { cause: e })
837
+ }
838
+ }
839
+
840
+ export async function getProvider(providerID: string) {
841
+ return state().then((s) => s.providers[providerID])
842
+ }
843
+
844
+ export async function getModel(providerID: string, modelID: string) {
845
+ const s = await state()
846
+ const provider = s.providers[providerID]
847
+ if (!provider) {
848
+ const availableProviders = Object.keys(s.providers)
849
+ const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 })
850
+ const suggestions = matches.map((m) => m.target)
851
+ throw new ModelNotFoundError({ providerID, modelID, suggestions })
852
+ }
853
+
854
+ const info = provider.models[modelID]
855
+ if (!info) {
856
+ const availableModels = Object.keys(provider.models)
857
+ const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
858
+ const suggestions = matches.map((m) => m.target)
859
+ throw new ModelNotFoundError({ providerID, modelID, suggestions })
860
+ }
861
+ return info
862
+ }
863
+
864
+ export async function getLanguage(model: Model) {
865
+ const s = await state()
866
+ const key = `${model.providerID}/${model.id}`
867
+ if (s.models.has(key)) return s.models.get(key)!
868
+
869
+ const provider = s.providers[model.providerID]
870
+ const sdk = await getSDK(model)
871
+
872
+ try {
873
+ const language = s.modelLoaders[model.providerID]
874
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
875
+ : sdk.languageModel(model.api.id)
876
+ s.models.set(key, language)
877
+ return language
878
+ } catch (e) {
879
+ if (e instanceof NoSuchModelError)
880
+ throw new ModelNotFoundError(
881
+ {
882
+ modelID: model.id,
883
+ providerID: model.providerID,
884
+ },
885
+ { cause: e },
886
+ )
887
+ throw e
888
+ }
889
+ }
890
+
891
+ export async function closest(providerID: string, query: string[]) {
892
+ const s = await state()
893
+ const provider = s.providers[providerID]
894
+ if (!provider) return undefined
895
+ for (const item of query) {
896
+ for (const modelID of Object.keys(provider.models)) {
897
+ if (modelID.includes(item))
898
+ return {
899
+ providerID,
900
+ modelID,
901
+ }
902
+ }
903
+ }
904
+ }
905
+
906
+ export async function getSmallModel(providerID: string) {
907
+ const cfg = await Config.get()
908
+
909
+ if (cfg.small_model) {
910
+ const parsed = parseModel(cfg.small_model)
911
+ return getModel(parsed.providerID, parsed.modelID)
912
+ }
913
+
914
+ const provider = await state().then((state) => state.providers[providerID])
915
+ if (provider) {
916
+ let priority = [
917
+ "claude-haiku-4-5",
918
+ "claude-haiku-4.5",
919
+ "3-5-haiku",
920
+ "3.5-haiku",
921
+ "gemini-2.5-flash",
922
+ "gpt-5-nano",
923
+ ]
924
+ // claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen
925
+ if (providerID === "github-copilot") {
926
+ priority = priority.filter((m) => m !== "claude-haiku-4.5")
927
+ }
928
+ if (providerID.startsWith("opencode")) {
929
+ priority = ["gpt-5-nano"]
930
+ }
931
+ for (const item of priority) {
932
+ for (const model of Object.keys(provider.models)) {
933
+ if (model.includes(item)) return getModel(providerID, model)
934
+ }
935
+ }
936
+ }
937
+
938
+ // Check if opencode provider is available before using it
939
+ const opencodeProvider = await state().then((state) => state.providers["opencode"])
940
+ if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
941
+ return getModel("opencode", "gpt-5-nano")
942
+ }
943
+
944
+ return undefined
945
+ }
946
+
947
+ const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
948
+ export function sort(models: Model[]) {
949
+ return sortBy(
950
+ models,
951
+ [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
952
+ [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
953
+ [(model) => model.id, "desc"],
954
+ )
955
+ }
956
+
957
+ export async function defaultModel() {
958
+ const cfg = await Config.get()
959
+ if (cfg.model) return parseModel(cfg.model)
960
+
961
+ const provider = await list()
962
+ .then((val) => Object.values(val))
963
+ .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
964
+ if (!provider) throw new Error("no providers found")
965
+ const [model] = sort(Object.values(provider.models))
966
+ if (!model) throw new Error("no models found")
967
+ return {
968
+ providerID: provider.id,
969
+ modelID: model.id,
970
+ }
971
+ }
972
+
973
+ export function parseModel(model: string) {
974
+ const [providerID, ...rest] = model.split("/")
975
+ return {
976
+ providerID: providerID,
977
+ modelID: rest.join("/"),
978
+ }
979
+ }
980
+
981
+ export const ModelNotFoundError = NamedError.create(
982
+ "ProviderModelNotFoundError",
983
+ z.object({
984
+ providerID: z.string(),
985
+ modelID: z.string(),
986
+ suggestions: z.array(z.string()).optional(),
987
+ }),
988
+ )
989
+
990
+ export const InitError = NamedError.create(
991
+ "ProviderInitError",
992
+ z.object({
993
+ providerID: z.string(),
994
+ }),
995
+ )
996
+ }