bonecode 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (575) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -50
  3. package/bone/output/agent/.dockerignore +7 -7
  4. package/bone/output/agent/.env.example +36 -36
  5. package/bone/output/agent/.github/workflows/ci.yaml +58 -58
  6. package/bone/output/agent/AgentDomain.bone.map +349 -349
  7. package/bone/output/agent/AgentDomain.postman_collection.json +957 -957
  8. package/bone/output/agent/Dockerfile +22 -22
  9. package/bone/output/agent/README.md +47 -47
  10. package/bone/output/agent/admin/index.html +739 -739
  11. package/bone/output/agent/docker-compose.yaml +22 -22
  12. package/bone/output/agent/k8s/deployment.yaml +75 -75
  13. package/bone/output/agent/migrations/agent.sql +36 -36
  14. package/bone/output/agent/migrations/agent_instance.sql +36 -36
  15. package/bone/output/agent/migrations/audit_log.sql +18 -18
  16. package/bone/output/agent/migrations/build_step.sql +34 -34
  17. package/bone/output/agent/migrations/event_outbox.sql +31 -31
  18. package/bone/output/agent/migrations/plan.sql +30 -30
  19. package/bone/output/agent/migrations/task.sql +30 -30
  20. package/bone/output/agent/migrations/tool_call.sql +33 -33
  21. package/bone/output/agent/openapi.yaml +1116 -1116
  22. package/bone/output/agent/package.json +35 -35
  23. package/bone/output/agent/schema.graphql +233 -233
  24. package/bone/output/agent/sdk/client.ts +231 -231
  25. package/bone/output/agent/src/algorithms.ts +2 -2
  26. package/bone/output/agent/src/audit.ts +44 -44
  27. package/bone/output/agent/src/auth.ts +57 -57
  28. package/bone/output/agent/src/cron.ts +12 -12
  29. package/bone/output/agent/src/db.ts +31 -31
  30. package/bone/output/agent/src/debug.ts +66 -66
  31. package/bone/output/agent/src/events.ts +243 -243
  32. package/bone/output/agent/src/extensions.ts +54 -54
  33. package/bone/output/agent/src/failure_rules.ts +322 -322
  34. package/bone/output/agent/src/flows.ts +168 -168
  35. package/bone/output/agent/src/health.ts +43 -43
  36. package/bone/output/agent/src/index.ts +99 -99
  37. package/bone/output/agent/src/logger.ts +69 -66
  38. package/bone/output/agent/src/metrics.ts +75 -75
  39. package/bone/output/agent/src/migrate.ts +351 -351
  40. package/bone/output/agent/src/migration_diff.ts +108 -108
  41. package/bone/output/agent/src/notify.ts +125 -125
  42. package/bone/output/agent/src/routes/plan.ts +91 -91
  43. package/bone/output/agent/src/routes/task.ts +105 -105
  44. package/bone/output/agent/src/routes/tool_call.ts +166 -166
  45. package/bone/output/agent/src/schemas.ts +384 -384
  46. package/bone/output/agent/src/state_machines/agent_instance.ts +24 -24
  47. package/bone/output/agent/src/state_machines/build_step.ts +22 -22
  48. package/bone/output/agent/src/state_machines/plan.ts +22 -22
  49. package/bone/output/agent/src/state_machines/task.ts +22 -22
  50. package/bone/output/agent/src/state_machines/tool_call.ts +22 -22
  51. package/bone/output/agent/src/tests.ts +361 -361
  52. package/bone/output/agent/src/websocket.ts +200 -200
  53. package/bone/output/agent/tsconfig.json +24 -24
  54. package/bone/output/rag/.dockerignore +7 -7
  55. package/bone/output/rag/.env.example +36 -36
  56. package/bone/output/rag/.github/workflows/ci.yaml +58 -58
  57. package/bone/output/rag/Dockerfile +22 -22
  58. package/bone/output/rag/RAGDomain.bone.map +286 -286
  59. package/bone/output/rag/RAGDomain.postman_collection.json +922 -922
  60. package/bone/output/rag/README.md +47 -47
  61. package/bone/output/rag/admin/index.html +817 -817
  62. package/bone/output/rag/docker-compose.yaml +22 -22
  63. package/bone/output/rag/k8s/deployment.yaml +75 -75
  64. package/bone/output/rag/migrations/audit_log.sql +18 -18
  65. package/bone/output/rag/migrations/code_chunk.sql +34 -34
  66. package/bone/output/rag/migrations/code_file.sql +33 -33
  67. package/bone/output/rag/migrations/event_outbox.sql +31 -31
  68. package/bone/output/rag/migrations/indexing_job.sql +33 -33
  69. package/bone/output/rag/migrations/knowledge_base.sql +35 -35
  70. package/bone/output/rag/migrations/memory_entry.sql +34 -34
  71. package/bone/output/rag/openapi.yaml +1097 -1097
  72. package/bone/output/rag/package.json +35 -35
  73. package/bone/output/rag/schema.graphql +245 -245
  74. package/bone/output/rag/sdk/client.ts +234 -234
  75. package/bone/output/rag/src/algorithms.ts +2 -2
  76. package/bone/output/rag/src/audit.ts +37 -37
  77. package/bone/output/rag/src/auth.ts +57 -57
  78. package/bone/output/rag/src/cron.ts +12 -12
  79. package/bone/output/rag/src/db.ts +31 -31
  80. package/bone/output/rag/src/debug.ts +66 -66
  81. package/bone/output/rag/src/events.ts +243 -243
  82. package/bone/output/rag/src/extensions.ts +350 -350
  83. package/bone/output/rag/src/failure_rules.ts +314 -314
  84. package/bone/output/rag/src/flows.ts +239 -239
  85. package/bone/output/rag/src/health.ts +43 -43
  86. package/bone/output/rag/src/index.ts +94 -94
  87. package/bone/output/rag/src/logger.ts +69 -66
  88. package/bone/output/rag/src/metrics.ts +75 -75
  89. package/bone/output/rag/src/migrate.ts +363 -363
  90. package/bone/output/rag/src/migration_diff.ts +108 -108
  91. package/bone/output/rag/src/notify.ts +99 -99
  92. package/bone/output/rag/src/routes/code_chunk.ts +75 -75
  93. package/bone/output/rag/src/routes/code_file.ts +101 -101
  94. package/bone/output/rag/src/routes/indexing_job.ts +87 -87
  95. package/bone/output/rag/src/routes/knowledge_base.ts +230 -230
  96. package/bone/output/rag/src/routes/memory_entry.ts +87 -87
  97. package/bone/output/rag/src/schemas.ts +394 -394
  98. package/bone/output/rag/src/state_machines/code_file.ts +23 -23
  99. package/bone/output/rag/src/state_machines/indexing_job.ts +22 -22
  100. package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -23
  101. package/bone/output/rag/src/state_machines/memory_entry.ts +20 -20
  102. package/bone/output/rag/src/tests.ts +339 -339
  103. package/bone/output/rag/tsconfig.json +24 -24
  104. package/bone/output/session/.dockerignore +7 -7
  105. package/bone/output/session/.env.example +36 -36
  106. package/bone/output/session/.github/workflows/ci.yaml +58 -58
  107. package/bone/output/session/Dockerfile +22 -22
  108. package/bone/output/session/README.md +47 -47
  109. package/bone/output/session/SessionDomain.bone.map +349 -349
  110. package/bone/output/session/SessionDomain.postman_collection.json +957 -957
  111. package/bone/output/session/admin/index.html +666 -666
  112. package/bone/output/session/docker-compose.yaml +22 -22
  113. package/bone/output/session/k8s/deployment.yaml +75 -75
  114. package/bone/output/session/migrations/audit_log.sql +18 -18
  115. package/bone/output/session/migrations/event_outbox.sql +31 -31
  116. package/bone/output/session/migrations/message.sql +31 -31
  117. package/bone/output/session/migrations/part.sql +28 -28
  118. package/bone/output/session/migrations/permission.sql +28 -28
  119. package/bone/output/session/migrations/project.sql +28 -28
  120. package/bone/output/session/migrations/session.sql +38 -38
  121. package/bone/output/session/openapi.yaml +1101 -1101
  122. package/bone/output/session/package.json +35 -35
  123. package/bone/output/session/schema.graphql +222 -222
  124. package/bone/output/session/sdk/client.ts +225 -225
  125. package/bone/output/session/src/algorithms.ts +2 -2
  126. package/bone/output/session/src/audit.ts +44 -44
  127. package/bone/output/session/src/auth.ts +57 -57
  128. package/bone/output/session/src/cron.ts +12 -12
  129. package/bone/output/session/src/db.ts +31 -31
  130. package/bone/output/session/src/debug.ts +66 -66
  131. package/bone/output/session/src/events.ts +270 -270
  132. package/bone/output/session/src/extensions.ts +215 -215
  133. package/bone/output/session/src/failure_rules.ts +283 -283
  134. package/bone/output/session/src/flows.ts +168 -168
  135. package/bone/output/session/src/health.ts +43 -43
  136. package/bone/output/session/src/index.ts +99 -99
  137. package/bone/output/session/src/logger.ts +67 -66
  138. package/bone/output/session/src/metrics.ts +75 -75
  139. package/bone/output/session/src/migrate.ts +331 -331
  140. package/bone/output/session/src/migration_diff.ts +108 -108
  141. package/bone/output/session/src/notify.ts +112 -112
  142. package/bone/output/session/src/routes/message.ts +93 -93
  143. package/bone/output/session/src/routes/part.ts +79 -79
  144. package/bone/output/session/src/routes/permission.ts +79 -79
  145. package/bone/output/session/src/routes/project.ts +79 -79
  146. package/bone/output/session/src/routes/session.ts +294 -294
  147. package/bone/output/session/src/schemas.ts +357 -357
  148. package/bone/output/session/src/state_machines/session.ts +23 -23
  149. package/bone/output/session/src/tests.ts +325 -325
  150. package/bone/output/session/src/websocket.ts +223 -200
  151. package/bone/output/session/tsconfig.json +24 -24
  152. package/bone/output/workspace/.dockerignore +7 -7
  153. package/bone/output/workspace/.env.example +36 -36
  154. package/bone/output/workspace/.github/workflows/ci.yaml +58 -58
  155. package/bone/output/workspace/Dockerfile +22 -22
  156. package/bone/output/workspace/README.md +45 -45
  157. package/bone/output/workspace/WorkspaceDomain.bone.map +188 -188
  158. package/bone/output/workspace/WorkspaceDomain.postman_collection.json +620 -620
  159. package/bone/output/workspace/admin/index.html +484 -484
  160. package/bone/output/workspace/docker-compose.yaml +22 -22
  161. package/bone/output/workspace/k8s/deployment.yaml +75 -75
  162. package/bone/output/workspace/migrations/audit_log.sql +18 -18
  163. package/bone/output/workspace/migrations/codebase.sql +34 -34
  164. package/bone/output/workspace/migrations/event_outbox.sql +31 -31
  165. package/bone/output/workspace/migrations/snapshot.sql +32 -32
  166. package/bone/output/workspace/migrations/workspace.sql +33 -33
  167. package/bone/output/workspace/openapi.yaml +721 -721
  168. package/bone/output/workspace/package.json +35 -35
  169. package/bone/output/workspace/schema.graphql +153 -153
  170. package/bone/output/workspace/sdk/client.ts +155 -155
  171. package/bone/output/workspace/src/algorithms.ts +2 -2
  172. package/bone/output/workspace/src/audit.ts +37 -37
  173. package/bone/output/workspace/src/auth.ts +57 -57
  174. package/bone/output/workspace/src/cron.ts +12 -12
  175. package/bone/output/workspace/src/db.ts +31 -31
  176. package/bone/output/workspace/src/debug.ts +66 -66
  177. package/bone/output/workspace/src/events.ts +243 -243
  178. package/bone/output/workspace/src/extensions.ts +44 -44
  179. package/bone/output/workspace/src/failure_rules.ts +152 -152
  180. package/bone/output/workspace/src/health.ts +43 -43
  181. package/bone/output/workspace/src/index.ts +88 -88
  182. package/bone/output/workspace/src/logger.ts +69 -66
  183. package/bone/output/workspace/src/metrics.ts +75 -75
  184. package/bone/output/workspace/src/migrate.ts +219 -219
  185. package/bone/output/workspace/src/migration_diff.ts +108 -108
  186. package/bone/output/workspace/src/notify.ts +73 -73
  187. package/bone/output/workspace/src/routes/codebase.ts +87 -87
  188. package/bone/output/workspace/src/routes/snapshot.ts +127 -127
  189. package/bone/output/workspace/src/routes/workspace.ts +190 -190
  190. package/bone/output/workspace/src/schemas.ts +231 -231
  191. package/bone/output/workspace/src/state_machines/codebase.ts +21 -21
  192. package/bone/output/workspace/src/state_machines/snapshot.ts +20 -20
  193. package/bone/output/workspace/src/state_machines/workspace.ts +21 -21
  194. package/bone/output/workspace/src/tests.ts +248 -248
  195. package/bone/output/workspace/tsconfig.json +24 -24
  196. package/compat/opencode_adapter.ts +94 -17
  197. package/package.json +15 -2
  198. package/src/cli.ts +66 -107
  199. package/src/db_adapter.ts +354 -0
  200. package/src/engine/account/account.sql.ts +39 -39
  201. package/src/engine/account/account.ts +456 -456
  202. package/src/engine/account/repo.ts +166 -166
  203. package/src/engine/account/schema.ts +99 -99
  204. package/src/engine/account/url.ts +8 -8
  205. package/src/engine/acp/README.md +174 -174
  206. package/src/engine/acp/agent.ts +1968 -1968
  207. package/src/engine/acp/runtime.ts +22 -22
  208. package/src/engine/acp/session.ts +122 -122
  209. package/src/engine/acp/types.ts +24 -24
  210. package/src/engine/agent/agent.ts +463 -463
  211. package/src/engine/agent/generate.txt +75 -75
  212. package/src/engine/agent/prompt/compaction.txt +9 -9
  213. package/src/engine/agent/prompt/explore.txt +18 -18
  214. package/src/engine/agent/prompt/scout.txt +36 -36
  215. package/src/engine/agent/prompt/summary.txt +11 -11
  216. package/src/engine/agent/prompt/title.txt +44 -44
  217. package/src/engine/agent/subagent-permissions.ts +34 -34
  218. package/src/engine/auth/index.ts +96 -96
  219. package/src/engine/background/background/job.ts +200 -200
  220. package/src/engine/background/job.ts +200 -200
  221. package/src/engine/bus/bus-event.ts +45 -45
  222. package/src/engine/bus/global.ts +22 -22
  223. package/src/engine/bus/index.ts +203 -203
  224. package/src/engine/command/command/index.ts +181 -181
  225. package/src/engine/command/command/template/initialize.txt +66 -66
  226. package/src/engine/command/command/template/review.txt +101 -101
  227. package/src/engine/command/index.ts +181 -181
  228. package/src/engine/command/template/initialize.txt +66 -66
  229. package/src/engine/command/template/review.txt +101 -101
  230. package/src/engine/config/agent.ts +172 -172
  231. package/src/engine/config/attachment.ts +25 -25
  232. package/src/engine/config/command.ts +62 -62
  233. package/src/engine/config/config.ts +833 -833
  234. package/src/engine/config/console-state.ts +14 -14
  235. package/src/engine/config/entry-name.ts +16 -16
  236. package/src/engine/config/error.ts +23 -23
  237. package/src/engine/config/formatter.ts +13 -13
  238. package/src/engine/config/layout.ts +6 -6
  239. package/src/engine/config/lsp.ts +43 -43
  240. package/src/engine/config/managed.ts +71 -71
  241. package/src/engine/config/markdown.ts +96 -96
  242. package/src/engine/config/mcp.ts +56 -56
  243. package/src/engine/config/model-id.ts +5 -5
  244. package/src/engine/config/parse.ts +79 -79
  245. package/src/engine/config/paths.ts +45 -45
  246. package/src/engine/config/permission.ts +58 -58
  247. package/src/engine/config/plugin.ts +84 -84
  248. package/src/engine/config/provider.ts +111 -111
  249. package/src/engine/config/reference.ts +23 -23
  250. package/src/engine/config/server.ts +19 -19
  251. package/src/engine/config/skills.ts +14 -14
  252. package/src/engine/config/variable.ts +90 -90
  253. package/src/engine/control-plane/adapters/index.ts +41 -41
  254. package/src/engine/control-plane/adapters/worktree.ts +96 -96
  255. package/src/engine/control-plane/dev/README.md +19 -19
  256. package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -73
  257. package/src/engine/control-plane/schema.ts +14 -14
  258. package/src/engine/control-plane/types.ts +59 -59
  259. package/src/engine/control-plane/util.ts +39 -39
  260. package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -51
  261. package/src/engine/control-plane/workspace-context.ts +26 -26
  262. package/src/engine/control-plane/workspace.sql.ts +20 -20
  263. package/src/engine/control-plane/workspace.ts +1072 -1072
  264. package/src/engine/data-migration.ts +161 -161
  265. package/src/engine/effect/app-runtime.ts +143 -143
  266. package/src/engine/effect/bootstrap-runtime.ts +29 -29
  267. package/src/engine/effect/bridge.ts +84 -84
  268. package/src/engine/effect/config-service.ts +67 -67
  269. package/src/engine/effect/instance-ref.ts +11 -11
  270. package/src/engine/effect/instance-registry.ts +12 -12
  271. package/src/engine/effect/instance-state.ts +72 -72
  272. package/src/engine/effect/promise.ts +17 -17
  273. package/src/engine/effect/run-service.ts +47 -47
  274. package/src/engine/effect/runner.ts +217 -217
  275. package/src/engine/effect/runtime-flags.ts +74 -74
  276. package/src/engine/effect/service-use.ts +38 -38
  277. package/src/engine/env/index.ts +37 -37
  278. package/src/engine/event-v2-bridge.ts +89 -89
  279. package/src/engine/file/file/ignore.ts +81 -81
  280. package/src/engine/file/file/index.ts +651 -651
  281. package/src/engine/file/file/protected.ts +59 -59
  282. package/src/engine/file/file/ripgrep.ts +481 -481
  283. package/src/engine/file/file/watcher.ts +167 -167
  284. package/src/engine/file/ignore.ts +81 -81
  285. package/src/engine/file/index.ts +651 -651
  286. package/src/engine/file/protected.ts +59 -59
  287. package/src/engine/file/ripgrep.ts +481 -481
  288. package/src/engine/file/watcher.ts +167 -167
  289. package/src/engine/format/format/formatter.ts +404 -404
  290. package/src/engine/format/format/index.ts +209 -209
  291. package/src/engine/format/formatter.ts +404 -404
  292. package/src/engine/format/index.ts +209 -209
  293. package/src/engine/git/git/index.ts +347 -347
  294. package/src/engine/git/index.ts +347 -347
  295. package/src/engine/id/id.ts +80 -80
  296. package/src/engine/ide/index.ts +70 -70
  297. package/src/engine/image/image/image.ts +176 -176
  298. package/src/engine/image/image.ts +176 -176
  299. package/src/engine/index.ts +251 -251
  300. package/src/engine/installation/index.ts +327 -327
  301. package/src/engine/lsp/client.ts +707 -707
  302. package/src/engine/lsp/diagnostic.ts +29 -29
  303. package/src/engine/lsp/language.ts +121 -121
  304. package/src/engine/lsp/launch.ts +21 -21
  305. package/src/engine/lsp/lsp/client.ts +707 -707
  306. package/src/engine/lsp/lsp/diagnostic.ts +29 -29
  307. package/src/engine/lsp/lsp/language.ts +121 -121
  308. package/src/engine/lsp/lsp/launch.ts +21 -21
  309. package/src/engine/lsp/lsp/lsp.ts +507 -507
  310. package/src/engine/lsp/lsp/server.ts +2064 -2064
  311. package/src/engine/lsp/lsp.ts +507 -507
  312. package/src/engine/lsp/server.ts +2064 -2064
  313. package/src/engine/mcp/auth.ts +146 -146
  314. package/src/engine/mcp/index.ts +958 -958
  315. package/src/engine/mcp/mcp/auth.ts +146 -146
  316. package/src/engine/mcp/mcp/index.ts +958 -958
  317. package/src/engine/mcp/mcp/oauth-callback.ts +232 -232
  318. package/src/engine/mcp/mcp/oauth-provider.ts +214 -214
  319. package/src/engine/mcp/oauth-callback.ts +232 -232
  320. package/src/engine/mcp/oauth-provider.ts +214 -214
  321. package/src/engine/node.ts +6 -6
  322. package/src/engine/patch/index.ts +689 -689
  323. package/src/engine/patch/patch/index.ts +689 -689
  324. package/src/engine/permission/arity.ts +163 -163
  325. package/src/engine/permission/evaluate.ts +15 -15
  326. package/src/engine/permission/index.ts +306 -306
  327. package/src/engine/permission/permission/arity.ts +163 -163
  328. package/src/engine/permission/permission/evaluate.ts +15 -15
  329. package/src/engine/permission/permission/index.ts +306 -306
  330. package/src/engine/permission/permission/schema.ts +13 -13
  331. package/src/engine/permission/schema.ts +13 -13
  332. package/src/engine/plugin/azure.ts +26 -26
  333. package/src/engine/plugin/cloudflare.ts +76 -76
  334. package/src/engine/plugin/codex.ts +622 -622
  335. package/src/engine/plugin/digitalocean.ts +411 -411
  336. package/src/engine/plugin/github-copilot/copilot.ts +394 -394
  337. package/src/engine/plugin/github-copilot/models.ts +196 -196
  338. package/src/engine/plugin/index.ts +295 -295
  339. package/src/engine/plugin/install.ts +439 -439
  340. package/src/engine/plugin/loader.ts +216 -216
  341. package/src/engine/plugin/meta.ts +188 -188
  342. package/src/engine/plugin/shared.ts +323 -323
  343. package/src/engine/project/bootstrap-service.ts +9 -9
  344. package/src/engine/project/bootstrap.ts +75 -75
  345. package/src/engine/project/instance-context.ts +24 -24
  346. package/src/engine/project/instance-layer.ts +11 -11
  347. package/src/engine/project/instance-runtime.ts +16 -16
  348. package/src/engine/project/instance-store.ts +193 -193
  349. package/src/engine/project/project.sql.ts +17 -17
  350. package/src/engine/project/project.ts +537 -537
  351. package/src/engine/project/schema.ts +13 -13
  352. package/src/engine/project/vcs.ts +405 -405
  353. package/src/engine/provider/auth.ts +225 -225
  354. package/src/engine/provider/error.ts +204 -204
  355. package/src/engine/provider/model-status.ts +8 -8
  356. package/src/engine/provider/provider.ts +1843 -1843
  357. package/src/engine/provider/schema.ts +30 -30
  358. package/src/engine/provider/transform.ts +1376 -1376
  359. package/src/engine/pty/index.ts +365 -365
  360. package/src/engine/pty/input.ts +24 -24
  361. package/src/engine/pty/pty/index.ts +365 -365
  362. package/src/engine/pty/pty/input.ts +24 -24
  363. package/src/engine/pty/pty/pty.bun.ts +26 -26
  364. package/src/engine/pty/pty/pty.node.ts +27 -27
  365. package/src/engine/pty/pty/pty.ts +25 -25
  366. package/src/engine/pty/pty/schema.ts +14 -14
  367. package/src/engine/pty/pty/ticket.ts +68 -68
  368. package/src/engine/pty/pty.bun.ts +26 -26
  369. package/src/engine/pty/pty.node.ts +27 -27
  370. package/src/engine/pty/pty.ts +25 -25
  371. package/src/engine/pty/schema.ts +14 -14
  372. package/src/engine/pty/ticket.ts +68 -68
  373. package/src/engine/question/index.ts +213 -213
  374. package/src/engine/question/question/index.ts +213 -213
  375. package/src/engine/question/question/schema.ts +10 -10
  376. package/src/engine/question/schema.ts +10 -10
  377. package/src/engine/reference/reference/reference.ts +241 -241
  378. package/src/engine/reference/reference/repository-cache.ts +147 -147
  379. package/src/engine/reference/reference.ts +241 -241
  380. package/src/engine/reference/repository-cache.ts +147 -147
  381. package/src/engine/session/compaction.ts +651 -651
  382. package/src/engine/session/instruction.ts +238 -238
  383. package/src/engine/session/llm.ts +459 -459
  384. package/src/engine/session/message-error.ts +14 -14
  385. package/src/engine/session/message-v2.ts +1202 -1202
  386. package/src/engine/session/message.ts +146 -146
  387. package/src/engine/session/overflow.ts +32 -32
  388. package/src/engine/session/processor.ts +823 -823
  389. package/src/engine/session/prompt/anthropic.txt +105 -105
  390. package/src/engine/session/prompt/beast.txt +147 -147
  391. package/src/engine/session/prompt/build-switch.txt +5 -5
  392. package/src/engine/session/prompt/codex.txt +79 -79
  393. package/src/engine/session/prompt/copilot-gpt-5.txt +143 -143
  394. package/src/engine/session/prompt/default.txt +105 -105
  395. package/src/engine/session/prompt/gemini.txt +155 -155
  396. package/src/engine/session/prompt/gpt.txt +107 -107
  397. package/src/engine/session/prompt/kimi.txt +95 -95
  398. package/src/engine/session/prompt/max-steps.txt +15 -15
  399. package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -67
  400. package/src/engine/session/prompt/plan.txt +26 -26
  401. package/src/engine/session/prompt/trinity.txt +97 -97
  402. package/src/engine/session/prompt.ts +66 -9
  403. package/src/engine/session/retry.ts +200 -200
  404. package/src/engine/session/revert.ts +162 -162
  405. package/src/engine/session/run-state.ts +153 -153
  406. package/src/engine/session/schema.ts +26 -26
  407. package/src/engine/session/session.sql.ts +137 -137
  408. package/src/engine/session/session.ts +1011 -1011
  409. package/src/engine/session/status.ts +94 -94
  410. package/src/engine/session/summary.ts +164 -164
  411. package/src/engine/session/system.ts +84 -84
  412. package/src/engine/session/todo.ts +81 -81
  413. package/src/engine/share/session.ts +61 -61
  414. package/src/engine/share/share-next.ts +376 -376
  415. package/src/engine/share/share.sql.ts +13 -13
  416. package/src/engine/shell/shell/shell.ts +215 -215
  417. package/src/engine/shell/shell.ts +215 -215
  418. package/src/engine/skill/discovery.ts +116 -116
  419. package/src/engine/skill/index.ts +336 -336
  420. package/src/engine/skill/prompt/customize-opencode.md +377 -377
  421. package/src/engine/skill/skill/discovery.ts +116 -116
  422. package/src/engine/skill/skill/index.ts +336 -336
  423. package/src/engine/skill/skill/prompt/customize-opencode.md +377 -377
  424. package/src/engine/snapshot/index.ts +762 -762
  425. package/src/engine/snapshot/snapshot/index.ts +762 -762
  426. package/src/engine/sync/README.md +179 -179
  427. package/src/engine/sync/event.sql.ts +17 -17
  428. package/src/engine/sync/index.ts +410 -410
  429. package/src/engine/sync/schema.ts +11 -11
  430. package/src/engine/temporary.ts +33 -33
  431. package/src/engine/tool/apply_patch.ts +313 -313
  432. package/src/engine/tool/apply_patch.txt +33 -33
  433. package/src/engine/tool/edit.ts +711 -711
  434. package/src/engine/tool/edit.txt +10 -10
  435. package/src/engine/tool/external-directory.ts +49 -49
  436. package/src/engine/tool/glob.ts +103 -103
  437. package/src/engine/tool/glob.txt +6 -6
  438. package/src/engine/tool/grep.ts +156 -156
  439. package/src/engine/tool/grep.txt +8 -8
  440. package/src/engine/tool/invalid.ts +21 -21
  441. package/src/engine/tool/json-schema.ts +164 -164
  442. package/src/engine/tool/lsp.ts +113 -113
  443. package/src/engine/tool/lsp.txt +24 -24
  444. package/src/engine/tool/mcp-websearch.ts +96 -96
  445. package/src/engine/tool/plan-enter.txt +14 -14
  446. package/src/engine/tool/plan-exit.txt +13 -13
  447. package/src/engine/tool/plan.ts +78 -78
  448. package/src/engine/tool/question.ts +44 -44
  449. package/src/engine/tool/question.txt +10 -10
  450. package/src/engine/tool/read.ts +337 -337
  451. package/src/engine/tool/read.txt +14 -14
  452. package/src/engine/tool/registry.ts +472 -472
  453. package/src/engine/tool/repo_clone.ts +80 -80
  454. package/src/engine/tool/repo_clone.txt +5 -5
  455. package/src/engine/tool/repo_overview.ts +279 -279
  456. package/src/engine/tool/repo_overview.txt +4 -4
  457. package/src/engine/tool/schema.ts +14 -14
  458. package/src/engine/tool/shell/id.ts +19 -19
  459. package/src/engine/tool/shell/prompt.ts +295 -295
  460. package/src/engine/tool/shell/shell.txt +77 -77
  461. package/src/engine/tool/shell.ts +647 -647
  462. package/src/engine/tool/skill.ts +75 -75
  463. package/src/engine/tool/skill.txt +5 -5
  464. package/src/engine/tool/task.ts +337 -337
  465. package/src/engine/tool/task.txt +58 -58
  466. package/src/engine/tool/task_status.ts +179 -179
  467. package/src/engine/tool/task_status.txt +13 -13
  468. package/src/engine/tool/todo.ts +57 -57
  469. package/src/engine/tool/todowrite.txt +167 -167
  470. package/src/engine/tool/tool/apply_patch.ts +313 -313
  471. package/src/engine/tool/tool/apply_patch.txt +33 -33
  472. package/src/engine/tool/tool/edit.ts +711 -711
  473. package/src/engine/tool/tool/edit.txt +10 -10
  474. package/src/engine/tool/tool/external-directory.ts +49 -49
  475. package/src/engine/tool/tool/glob.ts +103 -103
  476. package/src/engine/tool/tool/glob.txt +6 -6
  477. package/src/engine/tool/tool/grep.ts +156 -156
  478. package/src/engine/tool/tool/grep.txt +8 -8
  479. package/src/engine/tool/tool/invalid.ts +21 -21
  480. package/src/engine/tool/tool/json-schema.ts +164 -164
  481. package/src/engine/tool/tool/lsp.ts +113 -113
  482. package/src/engine/tool/tool/lsp.txt +24 -24
  483. package/src/engine/tool/tool/mcp-websearch.ts +96 -96
  484. package/src/engine/tool/tool/plan-enter.txt +14 -14
  485. package/src/engine/tool/tool/plan-exit.txt +13 -13
  486. package/src/engine/tool/tool/plan.ts +78 -78
  487. package/src/engine/tool/tool/question.ts +44 -44
  488. package/src/engine/tool/tool/question.txt +10 -10
  489. package/src/engine/tool/tool/read.ts +337 -337
  490. package/src/engine/tool/tool/read.txt +14 -14
  491. package/src/engine/tool/tool/registry.ts +472 -472
  492. package/src/engine/tool/tool/repo_clone.ts +80 -80
  493. package/src/engine/tool/tool/repo_clone.txt +5 -5
  494. package/src/engine/tool/tool/repo_overview.ts +279 -279
  495. package/src/engine/tool/tool/repo_overview.txt +4 -4
  496. package/src/engine/tool/tool/schema.ts +14 -14
  497. package/src/engine/tool/tool/shell/id.ts +19 -19
  498. package/src/engine/tool/tool/shell/prompt.ts +295 -295
  499. package/src/engine/tool/tool/shell/shell.txt +77 -77
  500. package/src/engine/tool/tool/shell.ts +647 -647
  501. package/src/engine/tool/tool/skill.ts +75 -75
  502. package/src/engine/tool/tool/skill.txt +5 -5
  503. package/src/engine/tool/tool/task.ts +337 -337
  504. package/src/engine/tool/tool/task.txt +58 -58
  505. package/src/engine/tool/tool/task_status.ts +179 -179
  506. package/src/engine/tool/tool/task_status.txt +13 -13
  507. package/src/engine/tool/tool/todo.ts +57 -57
  508. package/src/engine/tool/tool/todowrite.txt +167 -167
  509. package/src/engine/tool/tool/tool.ts +164 -164
  510. package/src/engine/tool/tool/truncate.ts +160 -160
  511. package/src/engine/tool/tool/truncation-dir.ts +4 -4
  512. package/src/engine/tool/tool/webfetch.ts +192 -192
  513. package/src/engine/tool/tool/webfetch.txt +13 -13
  514. package/src/engine/tool/tool/websearch.ts +143 -143
  515. package/src/engine/tool/tool/websearch.txt +14 -14
  516. package/src/engine/tool/tool/write.ts +104 -104
  517. package/src/engine/tool/tool/write.txt +8 -8
  518. package/src/engine/tool/tool.ts +164 -164
  519. package/src/engine/tool/truncate.ts +160 -160
  520. package/src/engine/tool/truncation-dir.ts +4 -4
  521. package/src/engine/tool/webfetch.ts +192 -192
  522. package/src/engine/tool/webfetch.txt +13 -13
  523. package/src/engine/tool/websearch.ts +143 -143
  524. package/src/engine/tool/websearch.txt +14 -14
  525. package/src/engine/tool/write.ts +104 -104
  526. package/src/engine/tool/write.txt +8 -8
  527. package/src/engine/util/archive.ts +17 -17
  528. package/src/engine/util/bom.ts +31 -31
  529. package/src/engine/util/data-url.ts +9 -9
  530. package/src/engine/util/defer.ts +10 -10
  531. package/src/engine/util/effect-http-client.ts +11 -11
  532. package/src/engine/util/error.ts +88 -88
  533. package/src/engine/util/filesystem.ts +252 -252
  534. package/src/engine/util/format.ts +20 -20
  535. package/src/engine/util/iife.ts +3 -3
  536. package/src/engine/util/lazy.ts +20 -20
  537. package/src/engine/util/local-context.ts +25 -25
  538. package/src/engine/util/locale.ts +86 -86
  539. package/src/engine/util/media.ts +26 -26
  540. package/src/engine/util/process.ts +176 -176
  541. package/src/engine/util/queue.ts +32 -32
  542. package/src/engine/util/record.ts +3 -3
  543. package/src/engine/util/repository.ts +158 -158
  544. package/src/engine/util/rpc.ts +66 -66
  545. package/src/engine/util/signal.ts +12 -12
  546. package/src/engine/util/timeout.ts +13 -13
  547. package/src/engine/util/token.ts +7 -7
  548. package/src/engine/util/util/archive.ts +17 -17
  549. package/src/engine/util/util/bom.ts +31 -31
  550. package/src/engine/util/util/data-url.ts +9 -9
  551. package/src/engine/util/util/defer.ts +10 -10
  552. package/src/engine/util/util/effect-http-client.ts +11 -11
  553. package/src/engine/util/util/error.ts +88 -88
  554. package/src/engine/util/util/filesystem.ts +252 -252
  555. package/src/engine/util/util/format.ts +20 -20
  556. package/src/engine/util/util/iife.ts +3 -3
  557. package/src/engine/util/util/lazy.ts +20 -20
  558. package/src/engine/util/util/local-context.ts +25 -25
  559. package/src/engine/util/util/locale.ts +86 -86
  560. package/src/engine/util/util/media.ts +26 -26
  561. package/src/engine/util/util/process.ts +176 -176
  562. package/src/engine/util/util/queue.ts +32 -32
  563. package/src/engine/util/util/record.ts +3 -3
  564. package/src/engine/util/util/repository.ts +158 -158
  565. package/src/engine/util/util/rpc.ts +66 -66
  566. package/src/engine/util/util/signal.ts +12 -12
  567. package/src/engine/util/util/timeout.ts +13 -13
  568. package/src/engine/util/util/token.ts +7 -7
  569. package/src/engine/util/util/which.ts +14 -14
  570. package/src/engine/util/util/wildcard.ts +59 -59
  571. package/src/engine/util/which.ts +14 -14
  572. package/src/engine/util/wildcard.ts +59 -59
  573. package/src/engine/worktree/index.ts +621 -621
  574. package/src/server.ts +121 -158
  575. package/src/tui.ts +485 -502
@@ -1,1843 +1,1843 @@
1
- import os from "os"
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 * as Log from "@opencode-ai/core/util/log"
7
- import { Npm } from "@opencode-ai/core/npm"
8
- import { Hash } from "@opencode-ai/core/util/hash"
9
- import { Plugin } from "../plugin"
10
- import { type LanguageModelV3 } from "@ai-sdk/provider"
11
- import * as ModelsDev from "@opencode-ai/core/models"
12
- import { Auth } from "../auth"
13
- import { Env } from "../env"
14
- import { InstallationVersion } from "@opencode-ai/core/installation/version"
15
- import { iife } from "@/util/iife"
16
- import { Global } from "@opencode-ai/core/global"
17
- import path from "path"
18
- import { pathToFileURL } from "url"
19
- import { Effect, Layer, Context, Schema, Types } from "effect"
20
- import { EffectBridge } from "@/effect/bridge"
21
- import { InstanceState } from "@/effect/instance-state"
22
- import { EffectPromise } from "@/effect/promise"
23
- import { AppFileSystem } from "@opencode-ai/core/filesystem"
24
- import { isRecord } from "@/util/record"
25
- import { optionalOmitUndefined } from "@opencode-ai/core/schema"
26
- import * as ProviderTransform from "./transform"
27
- import { ModelID, ProviderID } from "./schema"
28
- import { ModelStatus } from "./model-status"
29
- import { RuntimeFlags } from "@/effect/runtime-flags"
30
-
31
- const log = Log.create({ service: "provider" })
32
-
33
- function shouldUseCopilotResponsesApi(modelID: string): boolean {
34
- const match = /^gpt-(\d+)/.exec(modelID)
35
- if (!match) return false
36
- return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
37
- }
38
-
39
- function wrapSSE(res: Response, ms: number, ctl: AbortController) {
40
- if (typeof ms !== "number" || ms <= 0) return res
41
- if (!res.body) return res
42
- if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
43
-
44
- const reader = res.body.getReader()
45
- const body = new ReadableStream<Uint8Array>({
46
- async pull(ctrl) {
47
- const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
48
- const id = setTimeout(() => {
49
- const err = new Error("SSE read timed out")
50
- ctl.abort(err)
51
- void reader.cancel(err)
52
- reject(err)
53
- }, ms)
54
-
55
- reader.read().then(
56
- (part) => {
57
- clearTimeout(id)
58
- resolve(part)
59
- },
60
- (err) => {
61
- clearTimeout(id)
62
- reject(err)
63
- },
64
- )
65
- })
66
-
67
- if (part.done) {
68
- ctrl.close()
69
- return
70
- }
71
-
72
- ctrl.enqueue(part.value)
73
- },
74
- async cancel(reason) {
75
- ctl.abort(reason)
76
- await reader.cancel(reason)
77
- },
78
- })
79
-
80
- return new Response(body, {
81
- headers: new Headers(res.headers),
82
- status: res.status,
83
- statusText: res.statusText,
84
- })
85
- }
86
-
87
- type BundledSDK = {
88
- languageModel(modelId: string): LanguageModelV3
89
- }
90
-
91
- const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
92
- "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
93
- "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
94
- "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
95
- "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
96
- "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
97
- "@ai-sdk/google-vertex/anthropic": () =>
98
- import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
99
- "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
100
- "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
101
- "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
102
- "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
103
- "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
104
- "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
105
- "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
106
- "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
107
- "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
108
- "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
109
- "@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
110
- "@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
111
- "@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
112
- "@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
113
- "gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
114
- "@ai-sdk/github-copilot": () =>
115
- import("@opencode-ai/core/github-copilot/copilot-provider").then((m) => m.createOpenaiCompatible),
116
- "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
117
- }
118
-
119
- type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
120
- type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
121
- type CustomDiscoverModels = () => Promise<Record<string, Model>>
122
- type CustomLoader = (provider: Info) => Effect.Effect<{
123
- autoload: boolean
124
- getModel?: CustomModelLoader
125
- vars?: CustomVarsLoader
126
- options?: Record<string, any>
127
- discoverModels?: CustomDiscoverModels
128
- }>
129
-
130
- type CustomDep = {
131
- auth: (id: string) => Effect.Effect<Auth.Info | undefined>
132
- config: () => Effect.Effect<Config.Info>
133
- env: () => Effect.Effect<Record<string, string | undefined>>
134
- get: (key: string) => Effect.Effect<string | undefined>
135
- }
136
-
137
- function useLanguageModel(sdk: any) {
138
- return sdk.responses === undefined && sdk.chat === undefined
139
- }
140
-
141
- function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) {
142
- if (useChat && sdk.chat) return sdk.chat(modelID)
143
- if (sdk.responses) return sdk.responses(modelID)
144
- if (sdk.messages) return sdk.messages(modelID)
145
- if (sdk.chat) return sdk.chat(modelID)
146
- return sdk.languageModel(modelID)
147
- }
148
-
149
- function custom(dep: CustomDep): Record<string, CustomLoader> {
150
- return {
151
- anthropic: () =>
152
- Effect.succeed({
153
- autoload: false,
154
- options: {
155
- headers: {
156
- "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
157
- },
158
- },
159
- }),
160
- opencode: Effect.fnUntraced(function* (input: Info) {
161
- const env = yield* dep.env()
162
- const hasKey = iife(() => {
163
- if (input.env.some((item) => env[item])) return true
164
- return false
165
- })
166
- const ok =
167
- hasKey ||
168
- Boolean(yield* dep.auth(input.id)) ||
169
- Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey)
170
-
171
- if (!ok) {
172
- for (const [key, value] of Object.entries(input.models)) {
173
- if (value.cost.input === 0) continue
174
- delete input.models[key]
175
- }
176
- }
177
-
178
- return {
179
- autoload: Object.keys(input.models).length > 0,
180
- options: ok ? {} : { apiKey: "public" },
181
- }
182
- }),
183
- openai: () =>
184
- Effect.succeed({
185
- autoload: false,
186
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
187
- return sdk.responses(modelID)
188
- },
189
- options: {},
190
- }),
191
- xai: () =>
192
- Effect.succeed({
193
- autoload: false,
194
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
195
- return sdk.responses(modelID)
196
- },
197
- options: {},
198
- }),
199
- "github-copilot": () =>
200
- Effect.succeed({
201
- autoload: false,
202
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
203
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
204
- return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
205
- },
206
- options: {},
207
- }),
208
- azure: Effect.fnUntraced(function* (provider: Info) {
209
- const env = yield* dep.env()
210
- const auth = yield* dep.auth(provider.id)
211
- const resource = iife(() => {
212
- return [
213
- provider.options?.resourceName,
214
- auth?.type === "api" ? auth.metadata?.resourceName : undefined,
215
- env["AZURE_RESOURCE_NAME"],
216
- ].find((name) => typeof name === "string" && name.trim() !== "")
217
- })
218
-
219
- if (!resource && !provider.options?.baseURL) {
220
- return {
221
- autoload: false,
222
- async getModel() {
223
- throw new Error(
224
- "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
225
- )
226
- },
227
- }
228
- }
229
-
230
- return {
231
- autoload: false,
232
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
233
- return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
234
- },
235
- options: {
236
- resourceName: resource,
237
- },
238
- vars(_options): Record<string, string> {
239
- if (resource) {
240
- return {
241
- AZURE_RESOURCE_NAME: resource,
242
- }
243
- }
244
- return {}
245
- },
246
- }
247
- }),
248
- "azure-cognitive-services": Effect.fnUntraced(function* () {
249
- const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
250
- return {
251
- autoload: false,
252
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
253
- return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
254
- },
255
- options: {
256
- baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
257
- },
258
- }
259
- }),
260
- "amazon-bedrock": Effect.fnUntraced(function* () {
261
- const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
262
- const auth = yield* dep.auth("amazon-bedrock")
263
- const env = yield* dep.env()
264
-
265
- // Region precedence: 1) config file, 2) env var, 3) default
266
- const configRegion = providerConfig?.options?.region
267
- const envRegion = env["AWS_REGION"]
268
- const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
269
-
270
- // Profile: config file takes precedence over env var
271
- const configProfile = providerConfig?.options?.profile
272
- const envProfile = env["AWS_PROFILE"]
273
- const profile = configProfile ?? envProfile
274
-
275
- const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"]
276
-
277
- // TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
278
- // until the scope of the Env API is clarified (test only or runtime?)
279
- const awsBearerToken = iife(() => {
280
- const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK
281
- if (envToken) return envToken
282
- if (auth?.type === "api") {
283
- process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key
284
- return auth.key
285
- }
286
- return undefined
287
- })
288
-
289
- const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
290
-
291
- const containerCreds = Boolean(
292
- process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
293
- )
294
-
295
- if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds)
296
- return { autoload: false }
297
-
298
- const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
299
-
300
- const providerOptions: Record<string, any> = {
301
- region: defaultRegion,
302
- }
303
-
304
- // Only use credential chain if no bearer token exists
305
- // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)
306
- if (!awsBearerToken) {
307
- // Build credential provider options (only pass profile if specified)
308
- const credentialProviderOptions = profile ? { profile } : {}
309
-
310
- providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
311
- }
312
-
313
- // Add custom endpoint if specified (endpoint takes precedence over baseURL)
314
- const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
315
- if (endpoint) {
316
- providerOptions.baseURL = endpoint
317
- }
318
-
319
- return {
320
- autoload: true,
321
- options: providerOptions,
322
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
323
- // Skip region prefixing if model already has a cross-region inference profile prefix
324
- // Models from models.dev may already include prefixes like us., eu., global., etc.
325
- const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
326
- if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) {
327
- return sdk.languageModel(modelID)
328
- }
329
-
330
- // Region resolution precedence (highest to lowest):
331
- // 1. options.region from opencode.json provider config
332
- // 2. defaultRegion from AWS_REGION environment variable
333
- // 3. Default "us-east-1" (baked into defaultRegion)
334
- const region = options?.region ?? defaultRegion
335
-
336
- let regionPrefix = region.split("-")[0]
337
-
338
- switch (regionPrefix) {
339
- case "us": {
340
- const modelRequiresPrefix = [
341
- "nova-micro",
342
- "nova-lite",
343
- "nova-pro",
344
- "nova-premier",
345
- "nova-2",
346
- "claude",
347
- "deepseek",
348
- ].some((m) => modelID.includes(m))
349
- const isGovCloud = region.startsWith("us-gov")
350
- if (modelRequiresPrefix && !isGovCloud) {
351
- modelID = `${regionPrefix}.${modelID}`
352
- }
353
- break
354
- }
355
- case "eu": {
356
- const regionRequiresPrefix = [
357
- "eu-west-1",
358
- "eu-west-2",
359
- "eu-west-3",
360
- "eu-north-1",
361
- "eu-central-1",
362
- "eu-south-1",
363
- "eu-south-2",
364
- ].some((r) => region.includes(r))
365
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
366
- modelID.includes(m),
367
- )
368
- if (regionRequiresPrefix && modelRequiresPrefix) {
369
- modelID = `${regionPrefix}.${modelID}`
370
- }
371
- break
372
- }
373
- case "ap": {
374
- const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
375
- const isTokyoRegion = region === "ap-northeast-1"
376
- if (
377
- isAustraliaRegion &&
378
- ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
379
- ) {
380
- regionPrefix = "au"
381
- modelID = `${regionPrefix}.${modelID}`
382
- } else if (isTokyoRegion) {
383
- // Tokyo region uses jp. prefix for cross-region inference
384
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
385
- modelID.includes(m),
386
- )
387
- if (modelRequiresPrefix) {
388
- regionPrefix = "jp"
389
- modelID = `${regionPrefix}.${modelID}`
390
- }
391
- } else {
392
- // Other APAC regions use apac. prefix
393
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
394
- modelID.includes(m),
395
- )
396
- if (modelRequiresPrefix) {
397
- regionPrefix = "apac"
398
- modelID = `${regionPrefix}.${modelID}`
399
- }
400
- }
401
- break
402
- }
403
- }
404
-
405
- return sdk.languageModel(modelID)
406
- },
407
- }
408
- }),
409
- llmgateway: () =>
410
- Effect.succeed({
411
- autoload: false,
412
- options: {
413
- headers: {
414
- "HTTP-Referer": "https://opencode.ai/",
415
- "X-Title": "opencode",
416
- "X-Source": "opencode",
417
- },
418
- },
419
- }),
420
- openrouter: () =>
421
- Effect.succeed({
422
- autoload: false,
423
- options: {
424
- headers: {
425
- "HTTP-Referer": "https://opencode.ai/",
426
- "X-Title": "opencode",
427
- },
428
- },
429
- }),
430
- nvidia: (provider) =>
431
- Effect.succeed({
432
- autoload: provider.source === "config",
433
- options: {
434
- headers: {
435
- "HTTP-Referer": "https://opencode.ai/",
436
- "X-Title": "opencode",
437
- "X-BILLING-INVOKE-ORIGIN": "OpenCode",
438
- },
439
- },
440
- }),
441
- vercel: () =>
442
- Effect.succeed({
443
- autoload: false,
444
- options: {
445
- headers: {
446
- "http-referer": "https://opencode.ai/",
447
- "x-title": "opencode",
448
- },
449
- },
450
- }),
451
- "google-vertex": Effect.fnUntraced(function* (provider: Info) {
452
- const env = yield* dep.env()
453
- // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex; keep the wider
454
- // Google Cloud project env names as fallbacks for existing ADC setups.
455
- const project =
456
- provider.options?.project ??
457
- env["GOOGLE_VERTEX_PROJECT"] ??
458
- env["GOOGLE_CLOUD_PROJECT"] ??
459
- env["GCP_PROJECT"] ??
460
- env["GCLOUD_PROJECT"]
461
-
462
- const location = String(
463
- provider.options?.location ??
464
- env["GOOGLE_VERTEX_LOCATION"] ??
465
- env["GOOGLE_CLOUD_LOCATION"] ??
466
- env["VERTEX_LOCATION"] ??
467
- "us-central1",
468
- )
469
-
470
- const autoload = Boolean(project)
471
- if (!autoload) return { autoload: false }
472
- return {
473
- autoload: true,
474
- vars(_options: Record<string, any>) {
475
- const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
476
- return {
477
- ...(project && { GOOGLE_VERTEX_PROJECT: project }),
478
- GOOGLE_VERTEX_LOCATION: location,
479
- GOOGLE_VERTEX_ENDPOINT: endpoint,
480
- }
481
- },
482
- options: {
483
- project,
484
- location,
485
- fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
486
- const { GoogleAuth } = await import("google-auth-library")
487
- const auth = new GoogleAuth()
488
- const client = await auth.getApplicationDefault()
489
- const token = await client.credential.getAccessToken()
490
-
491
- const headers = new Headers(init?.headers)
492
- headers.set("Authorization", `Bearer ${token.token}`)
493
-
494
- return fetch(input, { ...init, headers })
495
- },
496
- },
497
- async getModel(sdk: any, modelID: string) {
498
- const id = String(modelID).trim()
499
- return sdk.languageModel(id)
500
- },
501
- }
502
- }),
503
- "google-vertex-anthropic": Effect.fnUntraced(function* () {
504
- const env = yield* dep.env()
505
- const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
506
- const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
507
- const autoload = Boolean(project)
508
- if (!autoload) return { autoload: false }
509
- return {
510
- autoload: true,
511
- options: {
512
- project,
513
- location,
514
- },
515
- async getModel(sdk: any, modelID) {
516
- const id = String(modelID).trim()
517
- return sdk.languageModel(id)
518
- },
519
- }
520
- }),
521
- "sap-ai-core": Effect.fnUntraced(function* () {
522
- const auth = yield* dep.auth("sap-ai-core")
523
- // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env),
524
- // until the scope of the Env API is clarified (test only or runtime?)
525
- const envServiceKey = iife(() => {
526
- const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY
527
- if (envAICoreServiceKey) return envAICoreServiceKey
528
- if (auth?.type === "api") {
529
- process.env.AICORE_SERVICE_KEY = auth.key
530
- return auth.key
531
- }
532
- return undefined
533
- })
534
- const deploymentId = process.env.AICORE_DEPLOYMENT_ID
535
- const resourceGroup = process.env.AICORE_RESOURCE_GROUP
536
-
537
- return {
538
- autoload: !!envServiceKey,
539
- options: envServiceKey ? { deploymentId, resourceGroup } : {},
540
- async getModel(sdk: any, modelID: string) {
541
- return sdk(modelID)
542
- },
543
- }
544
- }),
545
- zenmux: () =>
546
- Effect.succeed({
547
- autoload: false,
548
- options: {
549
- headers: {
550
- "HTTP-Referer": "https://opencode.ai/",
551
- "X-Title": "opencode",
552
- },
553
- },
554
- }),
555
- gitlab: Effect.fnUntraced(function* (input: Info) {
556
- const {
557
- VERSION: GITLAB_PROVIDER_VERSION,
558
- isWorkflowModel,
559
- discoverWorkflowModels,
560
- } = yield* Effect.promise(() => import("gitlab-ai-provider"))
561
-
562
- const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
563
-
564
- const auth = yield* dep.auth(input.id)
565
- const apiKey = yield* Effect.sync(() => {
566
- if (auth?.type === "oauth") return auth.access
567
- if (auth?.type === "api") return auth.key
568
- return undefined
569
- })
570
- const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
571
-
572
- const providerConfig = (yield* dep.config()).provider?.["gitlab"]
573
- const directory = yield* InstanceState.directory
574
-
575
- const aiGatewayHeaders = {
576
- "User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
577
- "anthropic-beta": "context-1m-2025-08-07",
578
- ...providerConfig?.options?.aiGatewayHeaders,
579
- }
580
-
581
- const featureFlags = {
582
- duo_agent_platform_agentic_chat: true,
583
- duo_agent_platform: true,
584
- ...providerConfig?.options?.featureFlags,
585
- }
586
-
587
- return {
588
- autoload: !!token,
589
- options: {
590
- instanceUrl,
591
- apiKey: token,
592
- aiGatewayHeaders,
593
- featureFlags,
594
- },
595
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
596
- if (modelID.startsWith("duo-workflow-")) {
597
- const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined
598
- // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
599
- const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
600
- const workflowDefinition =
601
- typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined
602
- const model = sdk.workflowChat(sdkModelID, {
603
- featureFlags,
604
- workflowDefinition,
605
- })
606
- if (workflowRef) {
607
- model.selectedModelRef = workflowRef
608
- }
609
- return model
610
- }
611
- return sdk.agenticChat(modelID, {
612
- aiGatewayHeaders,
613
- featureFlags,
614
- })
615
- },
616
- async discoverModels(): Promise<Record<string, Model>> {
617
- if (!apiKey) {
618
- log.info("gitlab model discovery skipped: no apiKey")
619
- return {}
620
- }
621
-
622
- try {
623
- const token = apiKey
624
- const getHeaders = (): Record<string, string> =>
625
- auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
626
-
627
- log.info("gitlab model discovery starting", { instanceUrl })
628
- const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory })
629
-
630
- if (!result.models.length) {
631
- log.info("gitlab model discovery skipped: no models found", {
632
- project: result.project
633
- ? {
634
- id: result.project.id,
635
- path: result.project.pathWithNamespace,
636
- }
637
- : null,
638
- })
639
- return {}
640
- }
641
-
642
- const models: Record<string, Model> = {}
643
- for (const m of result.models) {
644
- if (!input.models[m.id]) {
645
- models[m.id] = {
646
- id: ModelID.make(m.id),
647
- providerID: ProviderID.make("gitlab"),
648
- name: `Agent Platform (${m.name})`,
649
- family: "",
650
- api: {
651
- id: m.id,
652
- url: instanceUrl,
653
- npm: "gitlab-ai-provider",
654
- },
655
- status: "active",
656
- headers: {},
657
- options: { workflowRef: m.ref },
658
- cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
659
- limit: { context: m.context, output: m.output },
660
- capabilities: {
661
- temperature: false,
662
- reasoning: true,
663
- attachment: true,
664
- toolcall: true,
665
- input: {
666
- text: true,
667
- audio: false,
668
- image: true,
669
- video: false,
670
- pdf: true,
671
- },
672
- output: {
673
- text: true,
674
- audio: false,
675
- image: false,
676
- video: false,
677
- pdf: false,
678
- },
679
- interleaved: false,
680
- },
681
- release_date: "",
682
- variants: {},
683
- }
684
- }
685
- }
686
-
687
- log.info("gitlab model discovery complete", {
688
- count: Object.keys(models).length,
689
- models: Object.keys(models),
690
- })
691
- return models
692
- } catch (e) {
693
- log.warn("gitlab model discovery failed", { error: e })
694
- return {}
695
- }
696
- },
697
- }
698
- }),
699
- "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
700
- // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
701
- // skip the account ID check because the URL is already fully specified.
702
- if (input.options?.baseURL) return { autoload: false }
703
-
704
- const auth = yield* dep.auth(input.id)
705
- const env = yield* dep.env()
706
- const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
707
- if (!accountId)
708
- return {
709
- autoload: false,
710
- async getModel() {
711
- throw new Error(
712
- "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>",
713
- )
714
- },
715
- }
716
-
717
- const apiKey = yield* Effect.gen(function* () {
718
- const envToken = env["CLOUDFLARE_API_KEY"]
719
- if (envToken) return envToken
720
- if (auth?.type === "api") return auth.key
721
- return undefined
722
- })
723
-
724
- return {
725
- autoload: !!apiKey,
726
- options: {
727
- apiKey,
728
- headers: {
729
- "User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
730
- },
731
- },
732
- async getModel(sdk: any, modelID: string) {
733
- return sdk.languageModel(modelID)
734
- },
735
- vars(_options) {
736
- return {
737
- CLOUDFLARE_ACCOUNT_ID: accountId,
738
- }
739
- },
740
- }
741
- }),
742
- "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
743
- // When baseURL is already configured (e.g. corporate config), skip the ID checks.
744
- if (input.options?.baseURL) return { autoload: false }
745
-
746
- const auth = yield* dep.auth(input.id)
747
- const env = yield* dep.env()
748
- const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
749
- // The Cloudflare auth prompt stores this value as gatewayId metadata.
750
- const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
751
-
752
- if (!accountId || !gateway) {
753
- const missing = [
754
- !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined,
755
- !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined,
756
- ].filter((x): x is string => Boolean(x))
757
- return {
758
- autoload: false,
759
- async getModel() {
760
- throw new Error(
761
- `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
762
- )
763
- },
764
- }
765
- }
766
-
767
- // Get API token from env or auth - required for authenticated gateways
768
- const apiToken = yield* Effect.gen(function* () {
769
- const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
770
- if (envToken) return envToken
771
- if (auth?.type === "api") return auth.key
772
- return undefined
773
- })
774
-
775
- if (!apiToken) {
776
- throw new Error(
777
- "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " +
778
- "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.",
779
- )
780
- }
781
-
782
- // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
783
- const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
784
- const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
785
-
786
- const metadata = iife(() => {
787
- if (input.options?.metadata) return input.options.metadata
788
- try {
789
- return JSON.parse(input.options?.headers?.["cf-aig-metadata"])
790
- } catch {
791
- return undefined
792
- }
793
- })
794
- const opts = {
795
- metadata,
796
- cacheTtl: input.options?.cacheTtl,
797
- cacheKey: input.options?.cacheKey,
798
- skipCache: input.options?.skipCache,
799
- collectLog: input.options?.collectLog,
800
- headers: {
801
- "User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
802
- },
803
- }
804
-
805
- const aigateway = createAiGateway({
806
- accountId,
807
- gateway,
808
- apiKey: apiToken,
809
- ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}),
810
- })
811
- const unified = createUnified()
812
-
813
- return {
814
- autoload: true,
815
- async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
816
- // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
817
- return aigateway(unified(modelID))
818
- },
819
- options: {},
820
- }
821
- }),
822
- cerebras: () =>
823
- Effect.succeed({
824
- autoload: false,
825
- options: {
826
- headers: {
827
- "X-Cerebras-3rd-Party-Integration": "opencode",
828
- },
829
- },
830
- }),
831
- kilo: () =>
832
- Effect.succeed({
833
- autoload: false,
834
- options: {
835
- headers: {
836
- "HTTP-Referer": "https://opencode.ai/",
837
- "X-Title": "opencode",
838
- },
839
- },
840
- }),
841
- }
842
- }
843
-
844
- const ProviderApiInfo = Schema.Struct({
845
- id: Schema.String,
846
- url: Schema.String,
847
- npm: Schema.String,
848
- })
849
-
850
- const ProviderModalities = Schema.Struct({
851
- text: Schema.Boolean,
852
- audio: Schema.Boolean,
853
- image: Schema.Boolean,
854
- video: Schema.Boolean,
855
- pdf: Schema.Boolean,
856
- })
857
-
858
- const ProviderInterleaved = Schema.Union([
859
- Schema.Boolean,
860
- Schema.Struct({
861
- field: Schema.Literals(["reasoning_content", "reasoning_details"]),
862
- }),
863
- ])
864
-
865
- const ProviderCapabilities = Schema.Struct({
866
- temperature: Schema.Boolean,
867
- reasoning: Schema.Boolean,
868
- attachment: Schema.Boolean,
869
- toolcall: Schema.Boolean,
870
- input: ProviderModalities,
871
- output: ProviderModalities,
872
- interleaved: ProviderInterleaved,
873
- })
874
-
875
- const ProviderCacheCost = Schema.Struct({
876
- read: Schema.Finite,
877
- write: Schema.Finite,
878
- })
879
-
880
- const ProviderCostTier = Schema.Struct({
881
- input: Schema.Finite,
882
- output: Schema.Finite,
883
- cache: ProviderCacheCost,
884
- tier: Schema.Struct({
885
- type: Schema.Literal("context"),
886
- size: Schema.Finite,
887
- }),
888
- })
889
-
890
- const ProviderCost = Schema.Struct({
891
- input: Schema.Finite,
892
- output: Schema.Finite,
893
- cache: ProviderCacheCost,
894
- tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
895
- experimentalOver200K: optionalOmitUndefined(
896
- Schema.Struct({
897
- input: Schema.Finite,
898
- output: Schema.Finite,
899
- cache: ProviderCacheCost,
900
- }),
901
- ),
902
- })
903
-
904
- const ProviderLimit = Schema.Struct({
905
- context: Schema.Finite,
906
- input: optionalOmitUndefined(Schema.Finite),
907
- output: Schema.Finite,
908
- })
909
-
910
- export const Model = Schema.Struct({
911
- id: ModelID,
912
- providerID: ProviderID,
913
- api: ProviderApiInfo,
914
- name: Schema.String,
915
- family: optionalOmitUndefined(Schema.String),
916
- capabilities: ProviderCapabilities,
917
- cost: ProviderCost,
918
- limit: ProviderLimit,
919
- status: ModelStatus,
920
- options: Schema.Record(Schema.String, Schema.Any),
921
- headers: Schema.Record(Schema.String, Schema.String),
922
- release_date: Schema.String,
923
- variants: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
924
- }).annotate({ identifier: "Model" })
925
- export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
926
-
927
- export const Info = Schema.Struct({
928
- id: ProviderID,
929
- name: Schema.String,
930
- source: Schema.Literals(["env", "config", "custom", "api"]),
931
- env: Schema.Array(Schema.String),
932
- key: optionalOmitUndefined(Schema.String),
933
- options: Schema.Record(Schema.String, Schema.Any),
934
- models: Schema.Record(Schema.String, Model),
935
- }).annotate({ identifier: "Provider" })
936
- export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
937
-
938
- const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
939
-
940
- export const ListResult = Schema.Struct({
941
- all: Schema.Array(Info),
942
- default: DefaultModelIDs,
943
- connected: Schema.Array(Schema.String),
944
- })
945
- export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
946
-
947
- export const ConfigProvidersResult = Schema.Struct({
948
- providers: Schema.Array(Info),
949
- default: DefaultModelIDs,
950
- })
951
- export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
952
-
953
- export function toPublicInfo(provider: Info): Info {
954
- return JSON.parse(
955
- JSON.stringify(provider, (_, value) => {
956
- if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined
957
- if (typeof value === "bigint") return value.toString()
958
- return value
959
- }),
960
- )
961
- }
962
-
963
- export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
964
- return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
965
- }
966
-
967
- export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
968
- providerID: ProviderID,
969
- modelID: ModelID,
970
- suggestions: Schema.optional(Schema.Array(Schema.String)),
971
- cause: Schema.optional(Schema.Defect),
972
- }) {
973
- static isInstance(input: unknown): input is ModelNotFoundError {
974
- return input instanceof ModelNotFoundError
975
- }
976
- }
977
-
978
- export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderInitError", {
979
- providerID: ProviderID,
980
- cause: Schema.optional(Schema.Defect),
981
- }) {
982
- static isInstance(input: unknown): input is InitError {
983
- return input instanceof InitError
984
- }
985
- }
986
-
987
- export type Error = ModelNotFoundError | InitError
988
-
989
- export interface Interface {
990
- readonly list: () => Effect.Effect<Record<ProviderID, Info>>
991
- readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
992
- readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
993
- readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
994
- readonly closest: (
995
- providerID: ProviderID,
996
- query: string[],
997
- ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
998
- readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
999
- readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
1000
- }
1001
-
1002
- interface State {
1003
- models: Map<string, LanguageModelV3>
1004
- providers: Record<ProviderID, Info>
1005
- catalog: Record<ProviderID, Info>
1006
- sdk: Map<string, BundledSDK>
1007
- modelLoaders: Record<string, CustomModelLoader>
1008
- varsLoaders: Record<string, CustomVarsLoader>
1009
- }
1010
-
1011
- export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
1012
-
1013
- function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
1014
- const result: Model["cost"] = {
1015
- input: c?.input ?? 0,
1016
- output: c?.output ?? 0,
1017
- cache: {
1018
- read: c?.cache_read ?? 0,
1019
- write: c?.cache_write ?? 0,
1020
- },
1021
- }
1022
- if (c?.tiers) {
1023
- result.tiers = c.tiers.map((item) => ({
1024
- input: item.input,
1025
- output: item.output,
1026
- cache: {
1027
- read: item.cache_read ?? 0,
1028
- write: item.cache_write ?? 0,
1029
- },
1030
- tier: item.tier,
1031
- }))
1032
- }
1033
- if (c?.context_over_200k) {
1034
- result.experimentalOver200K = {
1035
- cache: {
1036
- read: c.context_over_200k.cache_read ?? 0,
1037
- write: c.context_over_200k.cache_write ?? 0,
1038
- },
1039
- input: c.context_over_200k.input,
1040
- output: c.context_over_200k.output,
1041
- }
1042
- }
1043
- return result
1044
- }
1045
-
1046
- function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
1047
- const base: Model = {
1048
- id: ModelID.make(model.id),
1049
- providerID: ProviderID.make(provider.id),
1050
- name: model.name,
1051
- family: model.family,
1052
- api: {
1053
- id: model.id,
1054
- url: model.provider?.api ?? provider.api ?? "",
1055
- npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
1056
- },
1057
- status: model.status ?? "active",
1058
- headers: {},
1059
- options: {},
1060
- cost: cost(model.cost),
1061
- limit: {
1062
- context: model.limit.context,
1063
- input: model.limit.input,
1064
- output: model.limit.output,
1065
- },
1066
- capabilities: {
1067
- temperature: model.temperature ?? false,
1068
- reasoning: model.reasoning ?? false,
1069
- attachment: model.attachment ?? false,
1070
- toolcall: model.tool_call ?? true,
1071
- input: {
1072
- text: model.modalities?.input?.includes("text") ?? false,
1073
- audio: model.modalities?.input?.includes("audio") ?? false,
1074
- image: model.modalities?.input?.includes("image") ?? false,
1075
- video: model.modalities?.input?.includes("video") ?? false,
1076
- pdf: model.modalities?.input?.includes("pdf") ?? false,
1077
- },
1078
- output: {
1079
- text: model.modalities?.output?.includes("text") ?? false,
1080
- audio: model.modalities?.output?.includes("audio") ?? false,
1081
- image: model.modalities?.output?.includes("image") ?? false,
1082
- video: model.modalities?.output?.includes("video") ?? false,
1083
- pdf: model.modalities?.output?.includes("pdf") ?? false,
1084
- },
1085
- interleaved: model.interleaved ?? false,
1086
- },
1087
- release_date: model.release_date ?? "",
1088
- variants: {},
1089
- }
1090
-
1091
- return {
1092
- ...base,
1093
- variants: mapValues(ProviderTransform.variants(base), (v) => v),
1094
- }
1095
- }
1096
-
1097
- export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
1098
- const models: Record<string, Model> = {}
1099
- for (const [key, model] of Object.entries(provider.models)) {
1100
- models[key] = fromModelsDevModel(provider, model)
1101
- for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
1102
- const id = `${model.id}-${mode}`
1103
- const base = fromModelsDevModel(provider, model)
1104
- models[id] = {
1105
- ...base,
1106
- id: ModelID.make(id),
1107
- name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
1108
- cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
1109
- options: opts.provider?.body
1110
- ? Object.fromEntries(
1111
- Object.entries(opts.provider.body).map(([k, v]) => [
1112
- k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
1113
- v,
1114
- ]),
1115
- )
1116
- : base.options,
1117
- headers: opts.provider?.headers ?? base.headers,
1118
- }
1119
- }
1120
- }
1121
- return {
1122
- id: ProviderID.make(provider.id),
1123
- source: "custom",
1124
- name: provider.name,
1125
- env: [...(provider.env ?? [])],
1126
- options: {},
1127
- models,
1128
- }
1129
- }
1130
-
1131
- function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels: boolean) {
1132
- if (!provider) return []
1133
- return Object.keys(provider.models).filter((id) => {
1134
- const model = provider.models[id]
1135
- if (model.status === "deprecated") return false
1136
- if (model.status === "alpha" && !enableExperimentalModels) return false
1137
- return true
1138
- })
1139
- }
1140
-
1141
- function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
1142
- const available = suggestionModelIDs(provider, enableExperimentalModels)
1143
- const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
1144
- if (fuzzy.length) return fuzzy
1145
- const query = modelID
1146
- .toLowerCase()
1147
- .split(/[^a-z0-9]+/)
1148
- .filter((part) => part.length > 1)
1149
- return sortBy(
1150
- available
1151
- .map((id) => ({
1152
- id,
1153
- score: query.filter((part) => id.toLowerCase().includes(part)).length,
1154
- }))
1155
- .filter((item) => item.score > 0),
1156
- [(item) => item.score, "desc"],
1157
- [(item) => item.id, "asc"],
1158
- )
1159
- .slice(0, 3)
1160
- .map((item) => item.id)
1161
- }
1162
-
1163
- export const layer = Layer.effect(
1164
- Service,
1165
- Effect.gen(function* () {
1166
- const fs = yield* AppFileSystem.Service
1167
- const config = yield* Config.Service
1168
- const auth = yield* Auth.Service
1169
- const env = yield* Env.Service
1170
- const plugin = yield* Plugin.Service
1171
- const modelsDevSvc = yield* ModelsDev.Service
1172
- const runtimeFlags = yield* RuntimeFlags.Service
1173
-
1174
- const state = yield* InstanceState.make<State>(() =>
1175
- Effect.gen(function* () {
1176
- using _ = log.time("state")
1177
- const bridge = yield* EffectBridge.make()
1178
- const cfg = yield* config.get()
1179
- const modelsDev = yield* modelsDevSvc.get()
1180
- const catalog = mapValues(modelsDev, fromModelsDevProvider)
1181
- const database = mapValues(catalog, toPublicInfo)
1182
-
1183
- const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
1184
- const languages = new Map<string, LanguageModelV3>()
1185
- const modelLoaders: {
1186
- [providerID: string]: CustomModelLoader
1187
- } = {}
1188
- const varsLoaders: {
1189
- [providerID: string]: CustomVarsLoader
1190
- } = {}
1191
- const sdk = new Map<string, BundledSDK>()
1192
- const discoveryLoaders: {
1193
- [providerID: string]: CustomDiscoverModels
1194
- } = {}
1195
- const dep = {
1196
- auth: (id: string) => auth.get(id).pipe(Effect.orDie),
1197
- config: () => config.get(),
1198
- env: () => env.all(),
1199
- get: (key: string) => env.get(key),
1200
- }
1201
-
1202
- log.info("init")
1203
-
1204
- function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
1205
- const existing = providers[providerID]
1206
- if (existing) {
1207
- // @ts-expect-error
1208
- providers[providerID] = mergeDeep(existing, provider)
1209
- return
1210
- }
1211
- const match = database[providerID]
1212
- if (!match) return
1213
- // @ts-expect-error
1214
- providers[providerID] = mergeDeep(match, provider)
1215
- }
1216
-
1217
- // load plugins first so config() hook runs before reading cfg.provider
1218
- const plugins = yield* plugin.list()
1219
-
1220
- // now read config providers - includes any modifications from plugin config() hook
1221
- const configProviders = Object.entries(cfg.provider ?? {})
1222
- const disabled = new Set(cfg.disabled_providers ?? [])
1223
- const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
1224
-
1225
- function isProviderAllowed(providerID: ProviderID): boolean {
1226
- if (enabled && !enabled.has(providerID)) return false
1227
- if (disabled.has(providerID)) return false
1228
- return true
1229
- }
1230
-
1231
- for (const hook of plugins) {
1232
- const p = hook.provider
1233
- const models = p?.models
1234
- if (!p || !models) continue
1235
-
1236
- const providerID = ProviderID.make(p.id)
1237
- if (disabled.has(providerID)) continue
1238
-
1239
- const provider = database[providerID]
1240
- if (!provider) continue
1241
- const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
1242
-
1243
- provider.models = yield* Effect.promise(async () => {
1244
- const next = await models(toPublicInfo(provider), { auth: pluginAuth })
1245
- return Object.fromEntries(
1246
- Object.entries(next).map(([id, model]) => [
1247
- id,
1248
- {
1249
- ...model,
1250
- id: ModelID.make(id),
1251
- providerID,
1252
- },
1253
- ]),
1254
- )
1255
- })
1256
- }
1257
-
1258
- // extend database from config
1259
- for (const [providerID, provider] of configProviders) {
1260
- const existing = database[providerID]
1261
- const parsed: Info = {
1262
- id: ProviderID.make(providerID),
1263
- name: provider.name ?? existing?.name ?? providerID,
1264
- env: provider.env ?? existing?.env ?? [],
1265
- options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
1266
- source: "config",
1267
- models: existing?.models ?? {},
1268
- }
1269
-
1270
- for (const [modelID, model] of Object.entries(provider.models ?? {})) {
1271
- const existingModel = parsed.models[model.id ?? modelID]
1272
- const apiID = model.id ?? existingModel?.api.id ?? modelID
1273
- const apiNpm =
1274
- model.provider?.npm ??
1275
- provider.npm ??
1276
- existingModel?.api.npm ??
1277
- modelsDev[providerID]?.npm ??
1278
- "@ai-sdk/openai-compatible"
1279
- const name = iife(() => {
1280
- if (model.name) return model.name
1281
- if (model.id && model.id !== modelID) return modelID
1282
- return existingModel?.name ?? modelID
1283
- })
1284
- const parsedModel: Model = {
1285
- id: ModelID.make(modelID),
1286
- api: {
1287
- id: apiID,
1288
- npm: apiNpm,
1289
- url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "",
1290
- },
1291
- status: model.status ?? existingModel?.status ?? "active",
1292
- name,
1293
- providerID: ProviderID.make(providerID),
1294
- capabilities: {
1295
- temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
1296
- reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
1297
- attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
1298
- toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
1299
- input: {
1300
- text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
1301
- audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
1302
- image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
1303
- video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
1304
- pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
1305
- },
1306
- output: {
1307
- text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
1308
- audio:
1309
- model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
1310
- image:
1311
- model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
1312
- video:
1313
- model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
1314
- pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
1315
- },
1316
- interleaved:
1317
- model.interleaved ??
1318
- existingModel?.capabilities.interleaved ??
1319
- (!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek")
1320
- ? { field: "reasoning_content" }
1321
- : false),
1322
- },
1323
- cost: {
1324
- input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
1325
- output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
1326
- cache: {
1327
- read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
1328
- write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
1329
- },
1330
- },
1331
- options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
1332
- limit: {
1333
- context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
1334
- input: model.limit?.input ?? existingModel?.limit?.input,
1335
- output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
1336
- },
1337
- headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
1338
- family: model.family ?? existingModel?.family ?? "",
1339
- release_date: model.release_date ?? existingModel?.release_date ?? "",
1340
- variants: {},
1341
- }
1342
- const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
1343
- parsedModel.variants = mapValues(
1344
- pickBy(merged, (v) => !v.disabled),
1345
- (v) => omit(v, ["disabled"]),
1346
- )
1347
- parsed.models[modelID] = parsedModel
1348
- }
1349
- database[providerID] = parsed
1350
- }
1351
-
1352
- // load env
1353
- const envs = yield* env.all()
1354
- for (const [id, provider] of Object.entries(database)) {
1355
- const providerID = ProviderID.make(id)
1356
- if (disabled.has(providerID)) continue
1357
- const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
1358
- if (!apiKey) continue
1359
- mergeProvider(providerID, {
1360
- source: "env",
1361
- key: provider.env.length === 1 ? apiKey : undefined,
1362
- })
1363
- }
1364
-
1365
- // load apikeys
1366
- const auths = yield* auth.all().pipe(Effect.orDie)
1367
- for (const [id, provider] of Object.entries(auths)) {
1368
- const providerID = ProviderID.make(id)
1369
- if (disabled.has(providerID)) continue
1370
- if (provider.type === "api") {
1371
- mergeProvider(providerID, {
1372
- source: "api",
1373
- key: provider.key,
1374
- })
1375
- }
1376
- }
1377
-
1378
- // plugin auth loader - database now has entries for config providers
1379
- for (const plugin of plugins) {
1380
- if (!plugin.auth) continue
1381
- const providerID = ProviderID.make(plugin.auth.provider)
1382
- if (disabled.has(providerID)) continue
1383
-
1384
- const stored = yield* auth.get(providerID).pipe(Effect.orDie)
1385
- if (!stored) continue
1386
- if (!plugin.auth.loader) continue
1387
-
1388
- const options = yield* Effect.promise(() =>
1389
- plugin.auth!.loader!(
1390
- () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
1391
- toPublicInfo(database[plugin.auth!.provider]),
1392
- ),
1393
- )
1394
- const opts = options ?? {}
1395
- const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
1396
- mergeProvider(providerID, patch)
1397
- }
1398
-
1399
- for (const [id, fn] of Object.entries(custom(dep))) {
1400
- const providerID = ProviderID.make(id)
1401
- if (disabled.has(providerID)) continue
1402
- const data = database[providerID]
1403
- if (!data) {
1404
- log.error("Provider does not exist in model list " + providerID)
1405
- continue
1406
- }
1407
- const result = yield* fn(data)
1408
- if (result && (result.autoload || providers[providerID])) {
1409
- if (result.getModel) modelLoaders[providerID] = result.getModel
1410
- if (result.vars) varsLoaders[providerID] = result.vars
1411
- if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
1412
- const opts = result.options ?? {}
1413
- const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
1414
- mergeProvider(providerID, patch)
1415
- }
1416
- }
1417
-
1418
- // load config - re-apply with updated data
1419
- for (const [id, provider] of configProviders) {
1420
- const providerID = ProviderID.make(id)
1421
- const partial: Partial<Info> = { source: "config" }
1422
- if (provider.env) partial.env = provider.env
1423
- if (provider.name) partial.name = provider.name
1424
- if (provider.options) partial.options = provider.options
1425
- mergeProvider(providerID, partial)
1426
- }
1427
-
1428
- const gitlab = ProviderID.make("gitlab")
1429
- if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
1430
- yield* Effect.promise(async () => {
1431
- try {
1432
- const discovered = await discoveryLoaders[gitlab]()
1433
- for (const [modelID, model] of Object.entries(discovered)) {
1434
- if (!providers[gitlab].models[modelID]) {
1435
- providers[gitlab].models[modelID] = model
1436
- }
1437
- }
1438
- } catch (e) {
1439
- log.warn("state discovery error", { id: "gitlab", error: e })
1440
- }
1441
- })
1442
- }
1443
-
1444
- for (const [id, provider] of Object.entries(providers)) {
1445
- const providerID = ProviderID.make(id)
1446
- if (!isProviderAllowed(providerID)) {
1447
- delete providers[providerID]
1448
- continue
1449
- }
1450
-
1451
- const configProvider = cfg.provider?.[providerID]
1452
-
1453
- for (const [modelID, model] of Object.entries(provider.models)) {
1454
- model.api.id = model.api.id ?? model.id ?? modelID
1455
- if (
1456
- // These chat aliases are invalid for the special handling in the
1457
- // built-in providers below, but custom providers may support them.
1458
- (modelID === "gpt-5-chat-latest" &&
1459
- (providerID === ProviderID.openai ||
1460
- providerID === ProviderID.githubCopilot ||
1461
- providerID === ProviderID.openrouter)) ||
1462
- (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
1463
- )
1464
- delete provider.models[modelID]
1465
- if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID]
1466
- if (model.status === "deprecated") delete provider.models[modelID]
1467
- if (
1468
- (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
1469
- (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
1470
- )
1471
- delete provider.models[modelID]
1472
-
1473
- if (!model.variants || Object.keys(model.variants).length === 0) {
1474
- model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
1475
- }
1476
-
1477
- const configVariants = configProvider?.models?.[modelID]?.variants
1478
- if (configVariants && model.variants) {
1479
- const merged = mergeDeep(model.variants, configVariants)
1480
- model.variants = mapValues(
1481
- pickBy(merged, (v) => !v.disabled),
1482
- (v) => omit(v, ["disabled"]),
1483
- )
1484
- }
1485
- }
1486
-
1487
- if (Object.keys(provider.models).length === 0) {
1488
- delete providers[providerID]
1489
- continue
1490
- }
1491
-
1492
- log.info("found", { providerID })
1493
- }
1494
-
1495
- return {
1496
- models: languages,
1497
- providers,
1498
- catalog,
1499
- sdk,
1500
- modelLoaders,
1501
- varsLoaders,
1502
- }
1503
- }),
1504
- )
1505
-
1506
- const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
1507
-
1508
- async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
1509
- try {
1510
- using _ = log.time("getSDK", {
1511
- providerID: model.providerID,
1512
- })
1513
- const provider = s.providers[model.providerID]
1514
- const options = { ...provider.options }
1515
-
1516
- if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
1517
- delete options.fetch
1518
- }
1519
-
1520
- if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
1521
- options["includeUsage"] = true
1522
- }
1523
-
1524
- const baseURL = iife(() => {
1525
- let url =
1526
- typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
1527
- if (!url) return
1528
-
1529
- const loader = s.varsLoaders[model.providerID]
1530
- if (loader) {
1531
- const vars = loader(options)
1532
- for (const [key, value] of Object.entries(vars)) {
1533
- const field = "${" + key + "}"
1534
- url = url.replaceAll(field, value)
1535
- }
1536
- }
1537
-
1538
- url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
1539
- const val = envs[String(key)]
1540
- return val ?? item
1541
- })
1542
- return url
1543
- })
1544
-
1545
- if (baseURL !== undefined) options["baseURL"] = baseURL
1546
- if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
1547
- if (model.headers)
1548
- options["headers"] = {
1549
- ...options["headers"],
1550
- ...model.headers,
1551
- }
1552
-
1553
- const key = Hash.fast(
1554
- JSON.stringify({
1555
- providerID: model.providerID,
1556
- npm: model.api.npm,
1557
- options,
1558
- }),
1559
- )
1560
- const existing = s.sdk.get(key)
1561
- if (existing) return existing
1562
-
1563
- const customFetch = options["fetch"]
1564
- const chunkTimeout = options["chunkTimeout"]
1565
- delete options["chunkTimeout"]
1566
-
1567
- options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
1568
- const fetchFn = customFetch ?? fetch
1569
- const opts = init ?? {}
1570
- const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
1571
- const signals: AbortSignal[] = []
1572
-
1573
- if (opts.signal) signals.push(opts.signal)
1574
- if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
1575
- if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
1576
- signals.push(AbortSignal.timeout(options["timeout"]))
1577
-
1578
- const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
1579
- if (combined) opts.signal = combined
1580
-
1581
- // Strip openai itemId metadata following what codex does
1582
- if (
1583
- (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") &&
1584
- opts.body &&
1585
- opts.method === "POST"
1586
- ) {
1587
- const body = JSON.parse(opts.body as string)
1588
- const keepIds = body.store === true
1589
- if (!keepIds && Array.isArray(body.input)) {
1590
- for (const item of body.input) {
1591
- if ("id" in item) {
1592
- delete item.id
1593
- }
1594
- }
1595
- opts.body = JSON.stringify(body)
1596
- }
1597
- }
1598
-
1599
- const res = await fetchFn(input, {
1600
- ...opts,
1601
- // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
1602
- timeout: false,
1603
- })
1604
-
1605
- if (!chunkAbortCtl) return res
1606
- return wrapSSE(res, chunkTimeout, chunkAbortCtl)
1607
- }
1608
-
1609
- const bundledLoader = BUNDLED_PROVIDERS[model.api.npm]
1610
- if (bundledLoader) {
1611
- log.info("using bundled provider", {
1612
- providerID: model.providerID,
1613
- pkg: model.api.npm,
1614
- })
1615
- const factory = await bundledLoader()
1616
- const loaded = factory({
1617
- name: model.providerID,
1618
- ...options,
1619
- })
1620
- s.sdk.set(key, loaded)
1621
- return loaded as SDK
1622
- }
1623
-
1624
- let installedPath: string
1625
- if (!model.api.npm.startsWith("file://")) {
1626
- const item = await Npm.add(model.api.npm)
1627
- if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
1628
- installedPath = item.entrypoint
1629
- } else {
1630
- log.info("loading local provider", { pkg: model.api.npm })
1631
- installedPath = model.api.npm
1632
- }
1633
-
1634
- // `installedPath` is a local entry path or an existing `file://` URL. Normalize
1635
- // only path inputs so Node on Windows accepts the dynamic import.
1636
- const importSpec = installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
1637
- const mod = await import(importSpec)
1638
-
1639
- const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
1640
- const loaded = fn({
1641
- name: model.providerID,
1642
- ...options,
1643
- })
1644
- s.sdk.set(key, loaded)
1645
- return loaded as SDK
1646
- } catch (e) {
1647
- throw new InitError({ providerID: model.providerID, cause: e })
1648
- }
1649
- }
1650
-
1651
- const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
1652
- InstanceState.use(state, (s) => s.providers[providerID]),
1653
- )
1654
-
1655
- const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
1656
- const s = yield* InstanceState.get(state)
1657
- const provider = s.providers[providerID]
1658
- if (!provider) {
1659
- const catalogProvider = s.catalog[providerID]
1660
- const suggestions = catalogProvider
1661
- ? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels)
1662
- : fuzzysort
1663
- .go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
1664
- .map((m) => m.target)
1665
- return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
1666
- }
1667
-
1668
- const info = provider.models[modelID]
1669
- if (!info) {
1670
- const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels)
1671
- const suggestions = current.length
1672
- ? current
1673
- : modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels)
1674
- return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
1675
- }
1676
- return info
1677
- })
1678
-
1679
- const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
1680
- const s = yield* InstanceState.get(state)
1681
- const envs = yield* env.all()
1682
- const key = `${model.providerID}/${model.id}`
1683
- if (s.models.has(key)) return s.models.get(key)!
1684
-
1685
- const provider = s.providers[model.providerID]
1686
- return yield* EffectPromise.refineRejection(
1687
- async () => {
1688
- const sdk = await resolveSDK(model, s, envs)
1689
- const language = s.modelLoaders[model.providerID]
1690
- ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
1691
- ...provider.options,
1692
- ...model.options,
1693
- })
1694
- : sdk.languageModel(model.api.id)
1695
- s.models.set(key, language)
1696
- return language
1697
- },
1698
- (cause) =>
1699
- cause instanceof NoSuchModelError
1700
- ? new ModelNotFoundError({ modelID: model.id, providerID: model.providerID, cause })
1701
- : undefined,
1702
- )
1703
- })
1704
-
1705
- const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
1706
- const s = yield* InstanceState.get(state)
1707
- const provider = s.providers[providerID]
1708
- if (!provider) return undefined
1709
- for (const item of query) {
1710
- for (const modelID of Object.keys(provider.models)) {
1711
- if (modelID.includes(item)) return { providerID, modelID }
1712
- }
1713
- }
1714
- return undefined
1715
- })
1716
-
1717
- const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
1718
- const cfg = yield* config.get()
1719
-
1720
- if (cfg.small_model) {
1721
- const parsed = parseModel(cfg.small_model)
1722
- return yield* getModel(parsed.providerID, parsed.modelID).pipe(
1723
- Effect.catchTag("ProviderModelNotFoundError", () => Effect.succeed(undefined)),
1724
- )
1725
- }
1726
-
1727
- const s = yield* InstanceState.get(state)
1728
- const provider = s.providers[providerID]
1729
- if (!provider) return undefined
1730
-
1731
- let priority = [
1732
- "claude-haiku-4-5",
1733
- "claude-haiku-4.5",
1734
- "3-5-haiku",
1735
- "3.5-haiku",
1736
- "gemini-3-flash",
1737
- "gemini-2.5-flash",
1738
- "gpt-5-nano",
1739
- ]
1740
- if (providerID.startsWith("opencode")) {
1741
- priority = ["gpt-5-nano"]
1742
- }
1743
- if (providerID.startsWith("github-copilot")) {
1744
- priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
1745
- }
1746
- for (const item of priority) {
1747
- if (providerID === ProviderID.amazonBedrock) {
1748
- const crossRegionPrefixes = ["global.", "us.", "eu."]
1749
- const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
1750
-
1751
- const globalMatch = candidates.find((m) => m.startsWith("global."))
1752
- if (globalMatch) return provider.models[globalMatch]
1753
-
1754
- const region = provider.options?.region
1755
- if (region) {
1756
- const regionPrefix = region.split("-")[0]
1757
- if (regionPrefix === "us" || regionPrefix === "eu") {
1758
- const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
1759
- if (regionalMatch) return provider.models[regionalMatch]
1760
- }
1761
- }
1762
-
1763
- const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
1764
- if (unprefixed) return provider.models[unprefixed]
1765
- } else {
1766
- for (const model of Object.keys(provider.models)) {
1767
- if (model.includes(item)) return provider.models[model]
1768
- }
1769
- }
1770
- }
1771
-
1772
- return undefined
1773
- })
1774
-
1775
- const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
1776
- const cfg = yield* config.get()
1777
- if (cfg.model) return parseModel(cfg.model)
1778
-
1779
- const s = yield* InstanceState.get(state)
1780
- const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
1781
- Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
1782
- if (!isRecord(x) || !Array.isArray(x.recent)) return []
1783
- return x.recent.flatMap((item) => {
1784
- if (!isRecord(item)) return []
1785
- if (typeof item.providerID !== "string") return []
1786
- if (typeof item.modelID !== "string") return []
1787
- return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
1788
- })
1789
- }),
1790
- Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
1791
- )
1792
- for (const entry of recent) {
1793
- const provider = s.providers[entry.providerID]
1794
- if (!provider) continue
1795
- if (!provider.models[entry.modelID]) continue
1796
- return { providerID: entry.providerID, modelID: entry.modelID }
1797
- }
1798
-
1799
- const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id))
1800
- if (!provider) throw new Error("no providers found")
1801
- const [model] = sort(Object.values(provider.models))
1802
- if (!model) throw new Error("no models found")
1803
- return {
1804
- providerID: provider.id,
1805
- modelID: model.id,
1806
- }
1807
- })
1808
-
1809
- return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
1810
- }),
1811
- )
1812
-
1813
- export const defaultLayer = Layer.suspend(() =>
1814
- layer.pipe(
1815
- Layer.provide(AppFileSystem.defaultLayer),
1816
- Layer.provide(Env.defaultLayer),
1817
- Layer.provide(Config.defaultLayer),
1818
- Layer.provide(Auth.defaultLayer),
1819
- Layer.provide(Plugin.defaultLayer),
1820
- Layer.provide(ModelsDev.defaultLayer),
1821
- Layer.provide(RuntimeFlags.defaultLayer),
1822
- ),
1823
- )
1824
-
1825
- const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
1826
- export function sort<T extends { id: string }>(models: T[]) {
1827
- return sortBy(
1828
- models,
1829
- [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
1830
- [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
1831
- [(model) => model.id, "desc"],
1832
- )
1833
- }
1834
-
1835
- export function parseModel(model: string) {
1836
- const [providerID, ...rest] = model.split("/")
1837
- return {
1838
- providerID: ProviderID.make(providerID),
1839
- modelID: ModelID.make(rest.join("/")),
1840
- }
1841
- }
1842
-
1843
- export * as Provider from "./provider"
1
+ import os from "os"
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 * as Log from "@opencode-ai/core/util/log"
7
+ import { Npm } from "@opencode-ai/core/npm"
8
+ import { Hash } from "@opencode-ai/core/util/hash"
9
+ import { Plugin } from "../plugin"
10
+ import { type LanguageModelV3 } from "@ai-sdk/provider"
11
+ import * as ModelsDev from "@opencode-ai/core/models"
12
+ import { Auth } from "../auth"
13
+ import { Env } from "../env"
14
+ import { InstallationVersion } from "@opencode-ai/core/installation/version"
15
+ import { iife } from "@/util/iife"
16
+ import { Global } from "@opencode-ai/core/global"
17
+ import path from "path"
18
+ import { pathToFileURL } from "url"
19
+ import { Effect, Layer, Context, Schema, Types } from "effect"
20
+ import { EffectBridge } from "@/effect/bridge"
21
+ import { InstanceState } from "@/effect/instance-state"
22
+ import { EffectPromise } from "@/effect/promise"
23
+ import { AppFileSystem } from "@opencode-ai/core/filesystem"
24
+ import { isRecord } from "@/util/record"
25
+ import { optionalOmitUndefined } from "@opencode-ai/core/schema"
26
+ import * as ProviderTransform from "./transform"
27
+ import { ModelID, ProviderID } from "./schema"
28
+ import { ModelStatus } from "./model-status"
29
+ import { RuntimeFlags } from "@/effect/runtime-flags"
30
+
31
+ const log = Log.create({ service: "provider" })
32
+
33
+ function shouldUseCopilotResponsesApi(modelID: string): boolean {
34
+ const match = /^gpt-(\d+)/.exec(modelID)
35
+ if (!match) return false
36
+ return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
37
+ }
38
+
39
+ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
40
+ if (typeof ms !== "number" || ms <= 0) return res
41
+ if (!res.body) return res
42
+ if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
43
+
44
+ const reader = res.body.getReader()
45
+ const body = new ReadableStream<Uint8Array>({
46
+ async pull(ctrl) {
47
+ const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
48
+ const id = setTimeout(() => {
49
+ const err = new Error("SSE read timed out")
50
+ ctl.abort(err)
51
+ void reader.cancel(err)
52
+ reject(err)
53
+ }, ms)
54
+
55
+ reader.read().then(
56
+ (part) => {
57
+ clearTimeout(id)
58
+ resolve(part)
59
+ },
60
+ (err) => {
61
+ clearTimeout(id)
62
+ reject(err)
63
+ },
64
+ )
65
+ })
66
+
67
+ if (part.done) {
68
+ ctrl.close()
69
+ return
70
+ }
71
+
72
+ ctrl.enqueue(part.value)
73
+ },
74
+ async cancel(reason) {
75
+ ctl.abort(reason)
76
+ await reader.cancel(reason)
77
+ },
78
+ })
79
+
80
+ return new Response(body, {
81
+ headers: new Headers(res.headers),
82
+ status: res.status,
83
+ statusText: res.statusText,
84
+ })
85
+ }
86
+
87
+ type BundledSDK = {
88
+ languageModel(modelId: string): LanguageModelV3
89
+ }
90
+
91
+ const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
92
+ "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
93
+ "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
94
+ "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
95
+ "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
96
+ "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
97
+ "@ai-sdk/google-vertex/anthropic": () =>
98
+ import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
99
+ "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
100
+ "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
101
+ "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
102
+ "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
103
+ "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
104
+ "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
105
+ "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
106
+ "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
107
+ "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
108
+ "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
109
+ "@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
110
+ "@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
111
+ "@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
112
+ "@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
113
+ "gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
114
+ "@ai-sdk/github-copilot": () =>
115
+ import("@opencode-ai/core/github-copilot/copilot-provider").then((m) => m.createOpenaiCompatible),
116
+ "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
117
+ }
118
+
119
+ type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
120
+ type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
121
+ type CustomDiscoverModels = () => Promise<Record<string, Model>>
122
+ type CustomLoader = (provider: Info) => Effect.Effect<{
123
+ autoload: boolean
124
+ getModel?: CustomModelLoader
125
+ vars?: CustomVarsLoader
126
+ options?: Record<string, any>
127
+ discoverModels?: CustomDiscoverModels
128
+ }>
129
+
130
+ type CustomDep = {
131
+ auth: (id: string) => Effect.Effect<Auth.Info | undefined>
132
+ config: () => Effect.Effect<Config.Info>
133
+ env: () => Effect.Effect<Record<string, string | undefined>>
134
+ get: (key: string) => Effect.Effect<string | undefined>
135
+ }
136
+
137
+ function useLanguageModel(sdk: any) {
138
+ return sdk.responses === undefined && sdk.chat === undefined
139
+ }
140
+
141
+ function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) {
142
+ if (useChat && sdk.chat) return sdk.chat(modelID)
143
+ if (sdk.responses) return sdk.responses(modelID)
144
+ if (sdk.messages) return sdk.messages(modelID)
145
+ if (sdk.chat) return sdk.chat(modelID)
146
+ return sdk.languageModel(modelID)
147
+ }
148
+
149
+ function custom(dep: CustomDep): Record<string, CustomLoader> {
150
+ return {
151
+ anthropic: () =>
152
+ Effect.succeed({
153
+ autoload: false,
154
+ options: {
155
+ headers: {
156
+ "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
157
+ },
158
+ },
159
+ }),
160
+ opencode: Effect.fnUntraced(function* (input: Info) {
161
+ const env = yield* dep.env()
162
+ const hasKey = iife(() => {
163
+ if (input.env.some((item) => env[item])) return true
164
+ return false
165
+ })
166
+ const ok =
167
+ hasKey ||
168
+ Boolean(yield* dep.auth(input.id)) ||
169
+ Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey)
170
+
171
+ if (!ok) {
172
+ for (const [key, value] of Object.entries(input.models)) {
173
+ if (value.cost.input === 0) continue
174
+ delete input.models[key]
175
+ }
176
+ }
177
+
178
+ return {
179
+ autoload: Object.keys(input.models).length > 0,
180
+ options: ok ? {} : { apiKey: "public" },
181
+ }
182
+ }),
183
+ openai: () =>
184
+ Effect.succeed({
185
+ autoload: false,
186
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
187
+ return sdk.responses(modelID)
188
+ },
189
+ options: {},
190
+ }),
191
+ xai: () =>
192
+ Effect.succeed({
193
+ autoload: false,
194
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
195
+ return sdk.responses(modelID)
196
+ },
197
+ options: {},
198
+ }),
199
+ "github-copilot": () =>
200
+ Effect.succeed({
201
+ autoload: false,
202
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
203
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
204
+ return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
205
+ },
206
+ options: {},
207
+ }),
208
+ azure: Effect.fnUntraced(function* (provider: Info) {
209
+ const env = yield* dep.env()
210
+ const auth = yield* dep.auth(provider.id)
211
+ const resource = iife(() => {
212
+ return [
213
+ provider.options?.resourceName,
214
+ auth?.type === "api" ? auth.metadata?.resourceName : undefined,
215
+ env["AZURE_RESOURCE_NAME"],
216
+ ].find((name) => typeof name === "string" && name.trim() !== "")
217
+ })
218
+
219
+ if (!resource && !provider.options?.baseURL) {
220
+ return {
221
+ autoload: false,
222
+ async getModel() {
223
+ throw new Error(
224
+ "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
225
+ )
226
+ },
227
+ }
228
+ }
229
+
230
+ return {
231
+ autoload: false,
232
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
233
+ return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
234
+ },
235
+ options: {
236
+ resourceName: resource,
237
+ },
238
+ vars(_options): Record<string, string> {
239
+ if (resource) {
240
+ return {
241
+ AZURE_RESOURCE_NAME: resource,
242
+ }
243
+ }
244
+ return {}
245
+ },
246
+ }
247
+ }),
248
+ "azure-cognitive-services": Effect.fnUntraced(function* () {
249
+ const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
250
+ return {
251
+ autoload: false,
252
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
253
+ return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
254
+ },
255
+ options: {
256
+ baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
257
+ },
258
+ }
259
+ }),
260
+ "amazon-bedrock": Effect.fnUntraced(function* () {
261
+ const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
262
+ const auth = yield* dep.auth("amazon-bedrock")
263
+ const env = yield* dep.env()
264
+
265
+ // Region precedence: 1) config file, 2) env var, 3) default
266
+ const configRegion = providerConfig?.options?.region
267
+ const envRegion = env["AWS_REGION"]
268
+ const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
269
+
270
+ // Profile: config file takes precedence over env var
271
+ const configProfile = providerConfig?.options?.profile
272
+ const envProfile = env["AWS_PROFILE"]
273
+ const profile = configProfile ?? envProfile
274
+
275
+ const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"]
276
+
277
+ // TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
278
+ // until the scope of the Env API is clarified (test only or runtime?)
279
+ const awsBearerToken = iife(() => {
280
+ const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK
281
+ if (envToken) return envToken
282
+ if (auth?.type === "api") {
283
+ process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key
284
+ return auth.key
285
+ }
286
+ return undefined
287
+ })
288
+
289
+ const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
290
+
291
+ const containerCreds = Boolean(
292
+ process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
293
+ )
294
+
295
+ if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds)
296
+ return { autoload: false }
297
+
298
+ const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
299
+
300
+ const providerOptions: Record<string, any> = {
301
+ region: defaultRegion,
302
+ }
303
+
304
+ // Only use credential chain if no bearer token exists
305
+ // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)
306
+ if (!awsBearerToken) {
307
+ // Build credential provider options (only pass profile if specified)
308
+ const credentialProviderOptions = profile ? { profile } : {}
309
+
310
+ providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
311
+ }
312
+
313
+ // Add custom endpoint if specified (endpoint takes precedence over baseURL)
314
+ const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
315
+ if (endpoint) {
316
+ providerOptions.baseURL = endpoint
317
+ }
318
+
319
+ return {
320
+ autoload: true,
321
+ options: providerOptions,
322
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
323
+ // Skip region prefixing if model already has a cross-region inference profile prefix
324
+ // Models from models.dev may already include prefixes like us., eu., global., etc.
325
+ const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
326
+ if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) {
327
+ return sdk.languageModel(modelID)
328
+ }
329
+
330
+ // Region resolution precedence (highest to lowest):
331
+ // 1. options.region from opencode.json provider config
332
+ // 2. defaultRegion from AWS_REGION environment variable
333
+ // 3. Default "us-east-1" (baked into defaultRegion)
334
+ const region = options?.region ?? defaultRegion
335
+
336
+ let regionPrefix = region.split("-")[0]
337
+
338
+ switch (regionPrefix) {
339
+ case "us": {
340
+ const modelRequiresPrefix = [
341
+ "nova-micro",
342
+ "nova-lite",
343
+ "nova-pro",
344
+ "nova-premier",
345
+ "nova-2",
346
+ "claude",
347
+ "deepseek",
348
+ ].some((m) => modelID.includes(m))
349
+ const isGovCloud = region.startsWith("us-gov")
350
+ if (modelRequiresPrefix && !isGovCloud) {
351
+ modelID = `${regionPrefix}.${modelID}`
352
+ }
353
+ break
354
+ }
355
+ case "eu": {
356
+ const regionRequiresPrefix = [
357
+ "eu-west-1",
358
+ "eu-west-2",
359
+ "eu-west-3",
360
+ "eu-north-1",
361
+ "eu-central-1",
362
+ "eu-south-1",
363
+ "eu-south-2",
364
+ ].some((r) => region.includes(r))
365
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
366
+ modelID.includes(m),
367
+ )
368
+ if (regionRequiresPrefix && modelRequiresPrefix) {
369
+ modelID = `${regionPrefix}.${modelID}`
370
+ }
371
+ break
372
+ }
373
+ case "ap": {
374
+ const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
375
+ const isTokyoRegion = region === "ap-northeast-1"
376
+ if (
377
+ isAustraliaRegion &&
378
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
379
+ ) {
380
+ regionPrefix = "au"
381
+ modelID = `${regionPrefix}.${modelID}`
382
+ } else if (isTokyoRegion) {
383
+ // Tokyo region uses jp. prefix for cross-region inference
384
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
385
+ modelID.includes(m),
386
+ )
387
+ if (modelRequiresPrefix) {
388
+ regionPrefix = "jp"
389
+ modelID = `${regionPrefix}.${modelID}`
390
+ }
391
+ } else {
392
+ // Other APAC regions use apac. prefix
393
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
394
+ modelID.includes(m),
395
+ )
396
+ if (modelRequiresPrefix) {
397
+ regionPrefix = "apac"
398
+ modelID = `${regionPrefix}.${modelID}`
399
+ }
400
+ }
401
+ break
402
+ }
403
+ }
404
+
405
+ return sdk.languageModel(modelID)
406
+ },
407
+ }
408
+ }),
409
+ llmgateway: () =>
410
+ Effect.succeed({
411
+ autoload: false,
412
+ options: {
413
+ headers: {
414
+ "HTTP-Referer": "https://opencode.ai/",
415
+ "X-Title": "opencode",
416
+ "X-Source": "opencode",
417
+ },
418
+ },
419
+ }),
420
+ openrouter: () =>
421
+ Effect.succeed({
422
+ autoload: false,
423
+ options: {
424
+ headers: {
425
+ "HTTP-Referer": "https://opencode.ai/",
426
+ "X-Title": "opencode",
427
+ },
428
+ },
429
+ }),
430
+ nvidia: (provider) =>
431
+ Effect.succeed({
432
+ autoload: provider.source === "config",
433
+ options: {
434
+ headers: {
435
+ "HTTP-Referer": "https://opencode.ai/",
436
+ "X-Title": "opencode",
437
+ "X-BILLING-INVOKE-ORIGIN": "OpenCode",
438
+ },
439
+ },
440
+ }),
441
+ vercel: () =>
442
+ Effect.succeed({
443
+ autoload: false,
444
+ options: {
445
+ headers: {
446
+ "http-referer": "https://opencode.ai/",
447
+ "x-title": "opencode",
448
+ },
449
+ },
450
+ }),
451
+ "google-vertex": Effect.fnUntraced(function* (provider: Info) {
452
+ const env = yield* dep.env()
453
+ // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex; keep the wider
454
+ // Google Cloud project env names as fallbacks for existing ADC setups.
455
+ const project =
456
+ provider.options?.project ??
457
+ env["GOOGLE_VERTEX_PROJECT"] ??
458
+ env["GOOGLE_CLOUD_PROJECT"] ??
459
+ env["GCP_PROJECT"] ??
460
+ env["GCLOUD_PROJECT"]
461
+
462
+ const location = String(
463
+ provider.options?.location ??
464
+ env["GOOGLE_VERTEX_LOCATION"] ??
465
+ env["GOOGLE_CLOUD_LOCATION"] ??
466
+ env["VERTEX_LOCATION"] ??
467
+ "us-central1",
468
+ )
469
+
470
+ const autoload = Boolean(project)
471
+ if (!autoload) return { autoload: false }
472
+ return {
473
+ autoload: true,
474
+ vars(_options: Record<string, any>) {
475
+ const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
476
+ return {
477
+ ...(project && { GOOGLE_VERTEX_PROJECT: project }),
478
+ GOOGLE_VERTEX_LOCATION: location,
479
+ GOOGLE_VERTEX_ENDPOINT: endpoint,
480
+ }
481
+ },
482
+ options: {
483
+ project,
484
+ location,
485
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
486
+ const { GoogleAuth } = await import("google-auth-library")
487
+ const auth = new GoogleAuth()
488
+ const client = await auth.getApplicationDefault()
489
+ const token = await client.credential.getAccessToken()
490
+
491
+ const headers = new Headers(init?.headers)
492
+ headers.set("Authorization", `Bearer ${token.token}`)
493
+
494
+ return fetch(input, { ...init, headers })
495
+ },
496
+ },
497
+ async getModel(sdk: any, modelID: string) {
498
+ const id = String(modelID).trim()
499
+ return sdk.languageModel(id)
500
+ },
501
+ }
502
+ }),
503
+ "google-vertex-anthropic": Effect.fnUntraced(function* () {
504
+ const env = yield* dep.env()
505
+ const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
506
+ const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
507
+ const autoload = Boolean(project)
508
+ if (!autoload) return { autoload: false }
509
+ return {
510
+ autoload: true,
511
+ options: {
512
+ project,
513
+ location,
514
+ },
515
+ async getModel(sdk: any, modelID) {
516
+ const id = String(modelID).trim()
517
+ return sdk.languageModel(id)
518
+ },
519
+ }
520
+ }),
521
+ "sap-ai-core": Effect.fnUntraced(function* () {
522
+ const auth = yield* dep.auth("sap-ai-core")
523
+ // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env),
524
+ // until the scope of the Env API is clarified (test only or runtime?)
525
+ const envServiceKey = iife(() => {
526
+ const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY
527
+ if (envAICoreServiceKey) return envAICoreServiceKey
528
+ if (auth?.type === "api") {
529
+ process.env.AICORE_SERVICE_KEY = auth.key
530
+ return auth.key
531
+ }
532
+ return undefined
533
+ })
534
+ const deploymentId = process.env.AICORE_DEPLOYMENT_ID
535
+ const resourceGroup = process.env.AICORE_RESOURCE_GROUP
536
+
537
+ return {
538
+ autoload: !!envServiceKey,
539
+ options: envServiceKey ? { deploymentId, resourceGroup } : {},
540
+ async getModel(sdk: any, modelID: string) {
541
+ return sdk(modelID)
542
+ },
543
+ }
544
+ }),
545
+ zenmux: () =>
546
+ Effect.succeed({
547
+ autoload: false,
548
+ options: {
549
+ headers: {
550
+ "HTTP-Referer": "https://opencode.ai/",
551
+ "X-Title": "opencode",
552
+ },
553
+ },
554
+ }),
555
+ gitlab: Effect.fnUntraced(function* (input: Info) {
556
+ const {
557
+ VERSION: GITLAB_PROVIDER_VERSION,
558
+ isWorkflowModel,
559
+ discoverWorkflowModels,
560
+ } = yield* Effect.promise(() => import("gitlab-ai-provider"))
561
+
562
+ const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
563
+
564
+ const auth = yield* dep.auth(input.id)
565
+ const apiKey = yield* Effect.sync(() => {
566
+ if (auth?.type === "oauth") return auth.access
567
+ if (auth?.type === "api") return auth.key
568
+ return undefined
569
+ })
570
+ const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
571
+
572
+ const providerConfig = (yield* dep.config()).provider?.["gitlab"]
573
+ const directory = yield* InstanceState.directory
574
+
575
+ const aiGatewayHeaders = {
576
+ "User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
577
+ "anthropic-beta": "context-1m-2025-08-07",
578
+ ...providerConfig?.options?.aiGatewayHeaders,
579
+ }
580
+
581
+ const featureFlags = {
582
+ duo_agent_platform_agentic_chat: true,
583
+ duo_agent_platform: true,
584
+ ...providerConfig?.options?.featureFlags,
585
+ }
586
+
587
+ return {
588
+ autoload: !!token,
589
+ options: {
590
+ instanceUrl,
591
+ apiKey: token,
592
+ aiGatewayHeaders,
593
+ featureFlags,
594
+ },
595
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
596
+ if (modelID.startsWith("duo-workflow-")) {
597
+ const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined
598
+ // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
599
+ const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
600
+ const workflowDefinition =
601
+ typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined
602
+ const model = sdk.workflowChat(sdkModelID, {
603
+ featureFlags,
604
+ workflowDefinition,
605
+ })
606
+ if (workflowRef) {
607
+ model.selectedModelRef = workflowRef
608
+ }
609
+ return model
610
+ }
611
+ return sdk.agenticChat(modelID, {
612
+ aiGatewayHeaders,
613
+ featureFlags,
614
+ })
615
+ },
616
+ async discoverModels(): Promise<Record<string, Model>> {
617
+ if (!apiKey) {
618
+ log.info("gitlab model discovery skipped: no apiKey")
619
+ return {}
620
+ }
621
+
622
+ try {
623
+ const token = apiKey
624
+ const getHeaders = (): Record<string, string> =>
625
+ auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
626
+
627
+ log.info("gitlab model discovery starting", { instanceUrl })
628
+ const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory })
629
+
630
+ if (!result.models.length) {
631
+ log.info("gitlab model discovery skipped: no models found", {
632
+ project: result.project
633
+ ? {
634
+ id: result.project.id,
635
+ path: result.project.pathWithNamespace,
636
+ }
637
+ : null,
638
+ })
639
+ return {}
640
+ }
641
+
642
+ const models: Record<string, Model> = {}
643
+ for (const m of result.models) {
644
+ if (!input.models[m.id]) {
645
+ models[m.id] = {
646
+ id: ModelID.make(m.id),
647
+ providerID: ProviderID.make("gitlab"),
648
+ name: `Agent Platform (${m.name})`,
649
+ family: "",
650
+ api: {
651
+ id: m.id,
652
+ url: instanceUrl,
653
+ npm: "gitlab-ai-provider",
654
+ },
655
+ status: "active",
656
+ headers: {},
657
+ options: { workflowRef: m.ref },
658
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
659
+ limit: { context: m.context, output: m.output },
660
+ capabilities: {
661
+ temperature: false,
662
+ reasoning: true,
663
+ attachment: true,
664
+ toolcall: true,
665
+ input: {
666
+ text: true,
667
+ audio: false,
668
+ image: true,
669
+ video: false,
670
+ pdf: true,
671
+ },
672
+ output: {
673
+ text: true,
674
+ audio: false,
675
+ image: false,
676
+ video: false,
677
+ pdf: false,
678
+ },
679
+ interleaved: false,
680
+ },
681
+ release_date: "",
682
+ variants: {},
683
+ }
684
+ }
685
+ }
686
+
687
+ log.info("gitlab model discovery complete", {
688
+ count: Object.keys(models).length,
689
+ models: Object.keys(models),
690
+ })
691
+ return models
692
+ } catch (e) {
693
+ log.warn("gitlab model discovery failed", { error: e })
694
+ return {}
695
+ }
696
+ },
697
+ }
698
+ }),
699
+ "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
700
+ // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
701
+ // skip the account ID check because the URL is already fully specified.
702
+ if (input.options?.baseURL) return { autoload: false }
703
+
704
+ const auth = yield* dep.auth(input.id)
705
+ const env = yield* dep.env()
706
+ const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
707
+ if (!accountId)
708
+ return {
709
+ autoload: false,
710
+ async getModel() {
711
+ throw new Error(
712
+ "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>",
713
+ )
714
+ },
715
+ }
716
+
717
+ const apiKey = yield* Effect.gen(function* () {
718
+ const envToken = env["CLOUDFLARE_API_KEY"]
719
+ if (envToken) return envToken
720
+ if (auth?.type === "api") return auth.key
721
+ return undefined
722
+ })
723
+
724
+ return {
725
+ autoload: !!apiKey,
726
+ options: {
727
+ apiKey,
728
+ headers: {
729
+ "User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
730
+ },
731
+ },
732
+ async getModel(sdk: any, modelID: string) {
733
+ return sdk.languageModel(modelID)
734
+ },
735
+ vars(_options) {
736
+ return {
737
+ CLOUDFLARE_ACCOUNT_ID: accountId,
738
+ }
739
+ },
740
+ }
741
+ }),
742
+ "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
743
+ // When baseURL is already configured (e.g. corporate config), skip the ID checks.
744
+ if (input.options?.baseURL) return { autoload: false }
745
+
746
+ const auth = yield* dep.auth(input.id)
747
+ const env = yield* dep.env()
748
+ const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
749
+ // The Cloudflare auth prompt stores this value as gatewayId metadata.
750
+ const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
751
+
752
+ if (!accountId || !gateway) {
753
+ const missing = [
754
+ !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined,
755
+ !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined,
756
+ ].filter((x): x is string => Boolean(x))
757
+ return {
758
+ autoload: false,
759
+ async getModel() {
760
+ throw new Error(
761
+ `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
762
+ )
763
+ },
764
+ }
765
+ }
766
+
767
+ // Get API token from env or auth - required for authenticated gateways
768
+ const apiToken = yield* Effect.gen(function* () {
769
+ const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
770
+ if (envToken) return envToken
771
+ if (auth?.type === "api") return auth.key
772
+ return undefined
773
+ })
774
+
775
+ if (!apiToken) {
776
+ throw new Error(
777
+ "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " +
778
+ "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.",
779
+ )
780
+ }
781
+
782
+ // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
783
+ const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
784
+ const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
785
+
786
+ const metadata = iife(() => {
787
+ if (input.options?.metadata) return input.options.metadata
788
+ try {
789
+ return JSON.parse(input.options?.headers?.["cf-aig-metadata"])
790
+ } catch {
791
+ return undefined
792
+ }
793
+ })
794
+ const opts = {
795
+ metadata,
796
+ cacheTtl: input.options?.cacheTtl,
797
+ cacheKey: input.options?.cacheKey,
798
+ skipCache: input.options?.skipCache,
799
+ collectLog: input.options?.collectLog,
800
+ headers: {
801
+ "User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
802
+ },
803
+ }
804
+
805
+ const aigateway = createAiGateway({
806
+ accountId,
807
+ gateway,
808
+ apiKey: apiToken,
809
+ ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}),
810
+ })
811
+ const unified = createUnified()
812
+
813
+ return {
814
+ autoload: true,
815
+ async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
816
+ // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
817
+ return aigateway(unified(modelID))
818
+ },
819
+ options: {},
820
+ }
821
+ }),
822
+ cerebras: () =>
823
+ Effect.succeed({
824
+ autoload: false,
825
+ options: {
826
+ headers: {
827
+ "X-Cerebras-3rd-Party-Integration": "opencode",
828
+ },
829
+ },
830
+ }),
831
+ kilo: () =>
832
+ Effect.succeed({
833
+ autoload: false,
834
+ options: {
835
+ headers: {
836
+ "HTTP-Referer": "https://opencode.ai/",
837
+ "X-Title": "opencode",
838
+ },
839
+ },
840
+ }),
841
+ }
842
+ }
843
+
844
+ const ProviderApiInfo = Schema.Struct({
845
+ id: Schema.String,
846
+ url: Schema.String,
847
+ npm: Schema.String,
848
+ })
849
+
850
+ const ProviderModalities = Schema.Struct({
851
+ text: Schema.Boolean,
852
+ audio: Schema.Boolean,
853
+ image: Schema.Boolean,
854
+ video: Schema.Boolean,
855
+ pdf: Schema.Boolean,
856
+ })
857
+
858
+ const ProviderInterleaved = Schema.Union([
859
+ Schema.Boolean,
860
+ Schema.Struct({
861
+ field: Schema.Literals(["reasoning_content", "reasoning_details"]),
862
+ }),
863
+ ])
864
+
865
+ const ProviderCapabilities = Schema.Struct({
866
+ temperature: Schema.Boolean,
867
+ reasoning: Schema.Boolean,
868
+ attachment: Schema.Boolean,
869
+ toolcall: Schema.Boolean,
870
+ input: ProviderModalities,
871
+ output: ProviderModalities,
872
+ interleaved: ProviderInterleaved,
873
+ })
874
+
875
+ const ProviderCacheCost = Schema.Struct({
876
+ read: Schema.Finite,
877
+ write: Schema.Finite,
878
+ })
879
+
880
+ const ProviderCostTier = Schema.Struct({
881
+ input: Schema.Finite,
882
+ output: Schema.Finite,
883
+ cache: ProviderCacheCost,
884
+ tier: Schema.Struct({
885
+ type: Schema.Literal("context"),
886
+ size: Schema.Finite,
887
+ }),
888
+ })
889
+
890
+ const ProviderCost = Schema.Struct({
891
+ input: Schema.Finite,
892
+ output: Schema.Finite,
893
+ cache: ProviderCacheCost,
894
+ tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
895
+ experimentalOver200K: optionalOmitUndefined(
896
+ Schema.Struct({
897
+ input: Schema.Finite,
898
+ output: Schema.Finite,
899
+ cache: ProviderCacheCost,
900
+ }),
901
+ ),
902
+ })
903
+
904
+ const ProviderLimit = Schema.Struct({
905
+ context: Schema.Finite,
906
+ input: optionalOmitUndefined(Schema.Finite),
907
+ output: Schema.Finite,
908
+ })
909
+
910
+ export const Model = Schema.Struct({
911
+ id: ModelID,
912
+ providerID: ProviderID,
913
+ api: ProviderApiInfo,
914
+ name: Schema.String,
915
+ family: optionalOmitUndefined(Schema.String),
916
+ capabilities: ProviderCapabilities,
917
+ cost: ProviderCost,
918
+ limit: ProviderLimit,
919
+ status: ModelStatus,
920
+ options: Schema.Record(Schema.String, Schema.Any),
921
+ headers: Schema.Record(Schema.String, Schema.String),
922
+ release_date: Schema.String,
923
+ variants: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
924
+ }).annotate({ identifier: "Model" })
925
+ export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
926
+
927
+ export const Info = Schema.Struct({
928
+ id: ProviderID,
929
+ name: Schema.String,
930
+ source: Schema.Literals(["env", "config", "custom", "api"]),
931
+ env: Schema.Array(Schema.String),
932
+ key: optionalOmitUndefined(Schema.String),
933
+ options: Schema.Record(Schema.String, Schema.Any),
934
+ models: Schema.Record(Schema.String, Model),
935
+ }).annotate({ identifier: "Provider" })
936
+ export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
937
+
938
+ const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
939
+
940
+ export const ListResult = Schema.Struct({
941
+ all: Schema.Array(Info),
942
+ default: DefaultModelIDs,
943
+ connected: Schema.Array(Schema.String),
944
+ })
945
+ export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
946
+
947
+ export const ConfigProvidersResult = Schema.Struct({
948
+ providers: Schema.Array(Info),
949
+ default: DefaultModelIDs,
950
+ })
951
+ export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
952
+
953
+ export function toPublicInfo(provider: Info): Info {
954
+ return JSON.parse(
955
+ JSON.stringify(provider, (_, value) => {
956
+ if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined
957
+ if (typeof value === "bigint") return value.toString()
958
+ return value
959
+ }),
960
+ )
961
+ }
962
+
963
+ export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
964
+ return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
965
+ }
966
+
967
+ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
968
+ providerID: ProviderID,
969
+ modelID: ModelID,
970
+ suggestions: Schema.optional(Schema.Array(Schema.String)),
971
+ cause: Schema.optional(Schema.Defect),
972
+ }) {
973
+ static isInstance(input: unknown): input is ModelNotFoundError {
974
+ return input instanceof ModelNotFoundError
975
+ }
976
+ }
977
+
978
+ export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderInitError", {
979
+ providerID: ProviderID,
980
+ cause: Schema.optional(Schema.Defect),
981
+ }) {
982
+ static isInstance(input: unknown): input is InitError {
983
+ return input instanceof InitError
984
+ }
985
+ }
986
+
987
+ export type Error = ModelNotFoundError | InitError
988
+
989
+ export interface Interface {
990
+ readonly list: () => Effect.Effect<Record<ProviderID, Info>>
991
+ readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
992
+ readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
993
+ readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
994
+ readonly closest: (
995
+ providerID: ProviderID,
996
+ query: string[],
997
+ ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
998
+ readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
999
+ readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
1000
+ }
1001
+
1002
+ interface State {
1003
+ models: Map<string, LanguageModelV3>
1004
+ providers: Record<ProviderID, Info>
1005
+ catalog: Record<ProviderID, Info>
1006
+ sdk: Map<string, BundledSDK>
1007
+ modelLoaders: Record<string, CustomModelLoader>
1008
+ varsLoaders: Record<string, CustomVarsLoader>
1009
+ }
1010
+
1011
+ export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
1012
+
1013
+ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
1014
+ const result: Model["cost"] = {
1015
+ input: c?.input ?? 0,
1016
+ output: c?.output ?? 0,
1017
+ cache: {
1018
+ read: c?.cache_read ?? 0,
1019
+ write: c?.cache_write ?? 0,
1020
+ },
1021
+ }
1022
+ if (c?.tiers) {
1023
+ result.tiers = c.tiers.map((item) => ({
1024
+ input: item.input,
1025
+ output: item.output,
1026
+ cache: {
1027
+ read: item.cache_read ?? 0,
1028
+ write: item.cache_write ?? 0,
1029
+ },
1030
+ tier: item.tier,
1031
+ }))
1032
+ }
1033
+ if (c?.context_over_200k) {
1034
+ result.experimentalOver200K = {
1035
+ cache: {
1036
+ read: c.context_over_200k.cache_read ?? 0,
1037
+ write: c.context_over_200k.cache_write ?? 0,
1038
+ },
1039
+ input: c.context_over_200k.input,
1040
+ output: c.context_over_200k.output,
1041
+ }
1042
+ }
1043
+ return result
1044
+ }
1045
+
1046
+ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
1047
+ const base: Model = {
1048
+ id: ModelID.make(model.id),
1049
+ providerID: ProviderID.make(provider.id),
1050
+ name: model.name,
1051
+ family: model.family,
1052
+ api: {
1053
+ id: model.id,
1054
+ url: model.provider?.api ?? provider.api ?? "",
1055
+ npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
1056
+ },
1057
+ status: model.status ?? "active",
1058
+ headers: {},
1059
+ options: {},
1060
+ cost: cost(model.cost),
1061
+ limit: {
1062
+ context: model.limit.context,
1063
+ input: model.limit.input,
1064
+ output: model.limit.output,
1065
+ },
1066
+ capabilities: {
1067
+ temperature: model.temperature ?? false,
1068
+ reasoning: model.reasoning ?? false,
1069
+ attachment: model.attachment ?? false,
1070
+ toolcall: model.tool_call ?? true,
1071
+ input: {
1072
+ text: model.modalities?.input?.includes("text") ?? false,
1073
+ audio: model.modalities?.input?.includes("audio") ?? false,
1074
+ image: model.modalities?.input?.includes("image") ?? false,
1075
+ video: model.modalities?.input?.includes("video") ?? false,
1076
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
1077
+ },
1078
+ output: {
1079
+ text: model.modalities?.output?.includes("text") ?? false,
1080
+ audio: model.modalities?.output?.includes("audio") ?? false,
1081
+ image: model.modalities?.output?.includes("image") ?? false,
1082
+ video: model.modalities?.output?.includes("video") ?? false,
1083
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
1084
+ },
1085
+ interleaved: model.interleaved ?? false,
1086
+ },
1087
+ release_date: model.release_date ?? "",
1088
+ variants: {},
1089
+ }
1090
+
1091
+ return {
1092
+ ...base,
1093
+ variants: mapValues(ProviderTransform.variants(base), (v) => v),
1094
+ }
1095
+ }
1096
+
1097
+ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
1098
+ const models: Record<string, Model> = {}
1099
+ for (const [key, model] of Object.entries(provider.models)) {
1100
+ models[key] = fromModelsDevModel(provider, model)
1101
+ for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
1102
+ const id = `${model.id}-${mode}`
1103
+ const base = fromModelsDevModel(provider, model)
1104
+ models[id] = {
1105
+ ...base,
1106
+ id: ModelID.make(id),
1107
+ name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
1108
+ cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
1109
+ options: opts.provider?.body
1110
+ ? Object.fromEntries(
1111
+ Object.entries(opts.provider.body).map(([k, v]) => [
1112
+ k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
1113
+ v,
1114
+ ]),
1115
+ )
1116
+ : base.options,
1117
+ headers: opts.provider?.headers ?? base.headers,
1118
+ }
1119
+ }
1120
+ }
1121
+ return {
1122
+ id: ProviderID.make(provider.id),
1123
+ source: "custom",
1124
+ name: provider.name,
1125
+ env: [...(provider.env ?? [])],
1126
+ options: {},
1127
+ models,
1128
+ }
1129
+ }
1130
+
1131
+ function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels: boolean) {
1132
+ if (!provider) return []
1133
+ return Object.keys(provider.models).filter((id) => {
1134
+ const model = provider.models[id]
1135
+ if (model.status === "deprecated") return false
1136
+ if (model.status === "alpha" && !enableExperimentalModels) return false
1137
+ return true
1138
+ })
1139
+ }
1140
+
1141
+ function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
1142
+ const available = suggestionModelIDs(provider, enableExperimentalModels)
1143
+ const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
1144
+ if (fuzzy.length) return fuzzy
1145
+ const query = modelID
1146
+ .toLowerCase()
1147
+ .split(/[^a-z0-9]+/)
1148
+ .filter((part) => part.length > 1)
1149
+ return sortBy(
1150
+ available
1151
+ .map((id) => ({
1152
+ id,
1153
+ score: query.filter((part) => id.toLowerCase().includes(part)).length,
1154
+ }))
1155
+ .filter((item) => item.score > 0),
1156
+ [(item) => item.score, "desc"],
1157
+ [(item) => item.id, "asc"],
1158
+ )
1159
+ .slice(0, 3)
1160
+ .map((item) => item.id)
1161
+ }
1162
+
1163
+ export const layer = Layer.effect(
1164
+ Service,
1165
+ Effect.gen(function* () {
1166
+ const fs = yield* AppFileSystem.Service
1167
+ const config = yield* Config.Service
1168
+ const auth = yield* Auth.Service
1169
+ const env = yield* Env.Service
1170
+ const plugin = yield* Plugin.Service
1171
+ const modelsDevSvc = yield* ModelsDev.Service
1172
+ const runtimeFlags = yield* RuntimeFlags.Service
1173
+
1174
+ const state = yield* InstanceState.make<State>(() =>
1175
+ Effect.gen(function* () {
1176
+ using _ = log.time("state")
1177
+ const bridge = yield* EffectBridge.make()
1178
+ const cfg = yield* config.get()
1179
+ const modelsDev = yield* modelsDevSvc.get()
1180
+ const catalog = mapValues(modelsDev, fromModelsDevProvider)
1181
+ const database = mapValues(catalog, toPublicInfo)
1182
+
1183
+ const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
1184
+ const languages = new Map<string, LanguageModelV3>()
1185
+ const modelLoaders: {
1186
+ [providerID: string]: CustomModelLoader
1187
+ } = {}
1188
+ const varsLoaders: {
1189
+ [providerID: string]: CustomVarsLoader
1190
+ } = {}
1191
+ const sdk = new Map<string, BundledSDK>()
1192
+ const discoveryLoaders: {
1193
+ [providerID: string]: CustomDiscoverModels
1194
+ } = {}
1195
+ const dep = {
1196
+ auth: (id: string) => auth.get(id).pipe(Effect.orDie),
1197
+ config: () => config.get(),
1198
+ env: () => env.all(),
1199
+ get: (key: string) => env.get(key),
1200
+ }
1201
+
1202
+ log.info("init")
1203
+
1204
+ function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
1205
+ const existing = providers[providerID]
1206
+ if (existing) {
1207
+ // @ts-expect-error
1208
+ providers[providerID] = mergeDeep(existing, provider)
1209
+ return
1210
+ }
1211
+ const match = database[providerID]
1212
+ if (!match) return
1213
+ // @ts-expect-error
1214
+ providers[providerID] = mergeDeep(match, provider)
1215
+ }
1216
+
1217
+ // load plugins first so config() hook runs before reading cfg.provider
1218
+ const plugins = yield* plugin.list()
1219
+
1220
+ // now read config providers - includes any modifications from plugin config() hook
1221
+ const configProviders = Object.entries(cfg.provider ?? {})
1222
+ const disabled = new Set(cfg.disabled_providers ?? [])
1223
+ const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
1224
+
1225
+ function isProviderAllowed(providerID: ProviderID): boolean {
1226
+ if (enabled && !enabled.has(providerID)) return false
1227
+ if (disabled.has(providerID)) return false
1228
+ return true
1229
+ }
1230
+
1231
+ for (const hook of plugins) {
1232
+ const p = hook.provider
1233
+ const models = p?.models
1234
+ if (!p || !models) continue
1235
+
1236
+ const providerID = ProviderID.make(p.id)
1237
+ if (disabled.has(providerID)) continue
1238
+
1239
+ const provider = database[providerID]
1240
+ if (!provider) continue
1241
+ const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
1242
+
1243
+ provider.models = yield* Effect.promise(async () => {
1244
+ const next = await models(toPublicInfo(provider), { auth: pluginAuth })
1245
+ return Object.fromEntries(
1246
+ Object.entries(next).map(([id, model]) => [
1247
+ id,
1248
+ {
1249
+ ...model,
1250
+ id: ModelID.make(id),
1251
+ providerID,
1252
+ },
1253
+ ]),
1254
+ )
1255
+ })
1256
+ }
1257
+
1258
+ // extend database from config
1259
+ for (const [providerID, provider] of configProviders) {
1260
+ const existing = database[providerID]
1261
+ const parsed: Info = {
1262
+ id: ProviderID.make(providerID),
1263
+ name: provider.name ?? existing?.name ?? providerID,
1264
+ env: provider.env ?? existing?.env ?? [],
1265
+ options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
1266
+ source: "config",
1267
+ models: existing?.models ?? {},
1268
+ }
1269
+
1270
+ for (const [modelID, model] of Object.entries(provider.models ?? {})) {
1271
+ const existingModel = parsed.models[model.id ?? modelID]
1272
+ const apiID = model.id ?? existingModel?.api.id ?? modelID
1273
+ const apiNpm =
1274
+ model.provider?.npm ??
1275
+ provider.npm ??
1276
+ existingModel?.api.npm ??
1277
+ modelsDev[providerID]?.npm ??
1278
+ "@ai-sdk/openai-compatible"
1279
+ const name = iife(() => {
1280
+ if (model.name) return model.name
1281
+ if (model.id && model.id !== modelID) return modelID
1282
+ return existingModel?.name ?? modelID
1283
+ })
1284
+ const parsedModel: Model = {
1285
+ id: ModelID.make(modelID),
1286
+ api: {
1287
+ id: apiID,
1288
+ npm: apiNpm,
1289
+ url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "",
1290
+ },
1291
+ status: model.status ?? existingModel?.status ?? "active",
1292
+ name,
1293
+ providerID: ProviderID.make(providerID),
1294
+ capabilities: {
1295
+ temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
1296
+ reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
1297
+ attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
1298
+ toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
1299
+ input: {
1300
+ text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
1301
+ audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
1302
+ image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
1303
+ video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
1304
+ pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
1305
+ },
1306
+ output: {
1307
+ text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
1308
+ audio:
1309
+ model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
1310
+ image:
1311
+ model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
1312
+ video:
1313
+ model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
1314
+ pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
1315
+ },
1316
+ interleaved:
1317
+ model.interleaved ??
1318
+ existingModel?.capabilities.interleaved ??
1319
+ (!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek")
1320
+ ? { field: "reasoning_content" }
1321
+ : false),
1322
+ },
1323
+ cost: {
1324
+ input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
1325
+ output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
1326
+ cache: {
1327
+ read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
1328
+ write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
1329
+ },
1330
+ },
1331
+ options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
1332
+ limit: {
1333
+ context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
1334
+ input: model.limit?.input ?? existingModel?.limit?.input,
1335
+ output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
1336
+ },
1337
+ headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
1338
+ family: model.family ?? existingModel?.family ?? "",
1339
+ release_date: model.release_date ?? existingModel?.release_date ?? "",
1340
+ variants: {},
1341
+ }
1342
+ const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
1343
+ parsedModel.variants = mapValues(
1344
+ pickBy(merged, (v) => !v.disabled),
1345
+ (v) => omit(v, ["disabled"]),
1346
+ )
1347
+ parsed.models[modelID] = parsedModel
1348
+ }
1349
+ database[providerID] = parsed
1350
+ }
1351
+
1352
+ // load env
1353
+ const envs = yield* env.all()
1354
+ for (const [id, provider] of Object.entries(database)) {
1355
+ const providerID = ProviderID.make(id)
1356
+ if (disabled.has(providerID)) continue
1357
+ const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
1358
+ if (!apiKey) continue
1359
+ mergeProvider(providerID, {
1360
+ source: "env",
1361
+ key: provider.env.length === 1 ? apiKey : undefined,
1362
+ })
1363
+ }
1364
+
1365
+ // load apikeys
1366
+ const auths = yield* auth.all().pipe(Effect.orDie)
1367
+ for (const [id, provider] of Object.entries(auths)) {
1368
+ const providerID = ProviderID.make(id)
1369
+ if (disabled.has(providerID)) continue
1370
+ if (provider.type === "api") {
1371
+ mergeProvider(providerID, {
1372
+ source: "api",
1373
+ key: provider.key,
1374
+ })
1375
+ }
1376
+ }
1377
+
1378
+ // plugin auth loader - database now has entries for config providers
1379
+ for (const plugin of plugins) {
1380
+ if (!plugin.auth) continue
1381
+ const providerID = ProviderID.make(plugin.auth.provider)
1382
+ if (disabled.has(providerID)) continue
1383
+
1384
+ const stored = yield* auth.get(providerID).pipe(Effect.orDie)
1385
+ if (!stored) continue
1386
+ if (!plugin.auth.loader) continue
1387
+
1388
+ const options = yield* Effect.promise(() =>
1389
+ plugin.auth!.loader!(
1390
+ () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
1391
+ toPublicInfo(database[plugin.auth!.provider]),
1392
+ ),
1393
+ )
1394
+ const opts = options ?? {}
1395
+ const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
1396
+ mergeProvider(providerID, patch)
1397
+ }
1398
+
1399
+ for (const [id, fn] of Object.entries(custom(dep))) {
1400
+ const providerID = ProviderID.make(id)
1401
+ if (disabled.has(providerID)) continue
1402
+ const data = database[providerID]
1403
+ if (!data) {
1404
+ log.error("Provider does not exist in model list " + providerID)
1405
+ continue
1406
+ }
1407
+ const result = yield* fn(data)
1408
+ if (result && (result.autoload || providers[providerID])) {
1409
+ if (result.getModel) modelLoaders[providerID] = result.getModel
1410
+ if (result.vars) varsLoaders[providerID] = result.vars
1411
+ if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
1412
+ const opts = result.options ?? {}
1413
+ const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
1414
+ mergeProvider(providerID, patch)
1415
+ }
1416
+ }
1417
+
1418
+ // load config - re-apply with updated data
1419
+ for (const [id, provider] of configProviders) {
1420
+ const providerID = ProviderID.make(id)
1421
+ const partial: Partial<Info> = { source: "config" }
1422
+ if (provider.env) partial.env = provider.env
1423
+ if (provider.name) partial.name = provider.name
1424
+ if (provider.options) partial.options = provider.options
1425
+ mergeProvider(providerID, partial)
1426
+ }
1427
+
1428
+ const gitlab = ProviderID.make("gitlab")
1429
+ if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
1430
+ yield* Effect.promise(async () => {
1431
+ try {
1432
+ const discovered = await discoveryLoaders[gitlab]()
1433
+ for (const [modelID, model] of Object.entries(discovered)) {
1434
+ if (!providers[gitlab].models[modelID]) {
1435
+ providers[gitlab].models[modelID] = model
1436
+ }
1437
+ }
1438
+ } catch (e) {
1439
+ log.warn("state discovery error", { id: "gitlab", error: e })
1440
+ }
1441
+ })
1442
+ }
1443
+
1444
+ for (const [id, provider] of Object.entries(providers)) {
1445
+ const providerID = ProviderID.make(id)
1446
+ if (!isProviderAllowed(providerID)) {
1447
+ delete providers[providerID]
1448
+ continue
1449
+ }
1450
+
1451
+ const configProvider = cfg.provider?.[providerID]
1452
+
1453
+ for (const [modelID, model] of Object.entries(provider.models)) {
1454
+ model.api.id = model.api.id ?? model.id ?? modelID
1455
+ if (
1456
+ // These chat aliases are invalid for the special handling in the
1457
+ // built-in providers below, but custom providers may support them.
1458
+ (modelID === "gpt-5-chat-latest" &&
1459
+ (providerID === ProviderID.openai ||
1460
+ providerID === ProviderID.githubCopilot ||
1461
+ providerID === ProviderID.openrouter)) ||
1462
+ (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
1463
+ )
1464
+ delete provider.models[modelID]
1465
+ if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID]
1466
+ if (model.status === "deprecated") delete provider.models[modelID]
1467
+ if (
1468
+ (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
1469
+ (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
1470
+ )
1471
+ delete provider.models[modelID]
1472
+
1473
+ if (!model.variants || Object.keys(model.variants).length === 0) {
1474
+ model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
1475
+ }
1476
+
1477
+ const configVariants = configProvider?.models?.[modelID]?.variants
1478
+ if (configVariants && model.variants) {
1479
+ const merged = mergeDeep(model.variants, configVariants)
1480
+ model.variants = mapValues(
1481
+ pickBy(merged, (v) => !v.disabled),
1482
+ (v) => omit(v, ["disabled"]),
1483
+ )
1484
+ }
1485
+ }
1486
+
1487
+ if (Object.keys(provider.models).length === 0) {
1488
+ delete providers[providerID]
1489
+ continue
1490
+ }
1491
+
1492
+ log.info("found", { providerID })
1493
+ }
1494
+
1495
+ return {
1496
+ models: languages,
1497
+ providers,
1498
+ catalog,
1499
+ sdk,
1500
+ modelLoaders,
1501
+ varsLoaders,
1502
+ }
1503
+ }),
1504
+ )
1505
+
1506
+ const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
1507
+
1508
+ async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
1509
+ try {
1510
+ using _ = log.time("getSDK", {
1511
+ providerID: model.providerID,
1512
+ })
1513
+ const provider = s.providers[model.providerID]
1514
+ const options = { ...provider.options }
1515
+
1516
+ if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
1517
+ delete options.fetch
1518
+ }
1519
+
1520
+ if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
1521
+ options["includeUsage"] = true
1522
+ }
1523
+
1524
+ const baseURL = iife(() => {
1525
+ let url =
1526
+ typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
1527
+ if (!url) return
1528
+
1529
+ const loader = s.varsLoaders[model.providerID]
1530
+ if (loader) {
1531
+ const vars = loader(options)
1532
+ for (const [key, value] of Object.entries(vars)) {
1533
+ const field = "${" + key + "}"
1534
+ url = url.replaceAll(field, value)
1535
+ }
1536
+ }
1537
+
1538
+ url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
1539
+ const val = envs[String(key)]
1540
+ return val ?? item
1541
+ })
1542
+ return url
1543
+ })
1544
+
1545
+ if (baseURL !== undefined) options["baseURL"] = baseURL
1546
+ if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
1547
+ if (model.headers)
1548
+ options["headers"] = {
1549
+ ...options["headers"],
1550
+ ...model.headers,
1551
+ }
1552
+
1553
+ const key = Hash.fast(
1554
+ JSON.stringify({
1555
+ providerID: model.providerID,
1556
+ npm: model.api.npm,
1557
+ options,
1558
+ }),
1559
+ )
1560
+ const existing = s.sdk.get(key)
1561
+ if (existing) return existing
1562
+
1563
+ const customFetch = options["fetch"]
1564
+ const chunkTimeout = options["chunkTimeout"]
1565
+ delete options["chunkTimeout"]
1566
+
1567
+ options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
1568
+ const fetchFn = customFetch ?? fetch
1569
+ const opts = init ?? {}
1570
+ const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
1571
+ const signals: AbortSignal[] = []
1572
+
1573
+ if (opts.signal) signals.push(opts.signal)
1574
+ if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
1575
+ if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
1576
+ signals.push(AbortSignal.timeout(options["timeout"]))
1577
+
1578
+ const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
1579
+ if (combined) opts.signal = combined
1580
+
1581
+ // Strip openai itemId metadata following what codex does
1582
+ if (
1583
+ (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") &&
1584
+ opts.body &&
1585
+ opts.method === "POST"
1586
+ ) {
1587
+ const body = JSON.parse(opts.body as string)
1588
+ const keepIds = body.store === true
1589
+ if (!keepIds && Array.isArray(body.input)) {
1590
+ for (const item of body.input) {
1591
+ if ("id" in item) {
1592
+ delete item.id
1593
+ }
1594
+ }
1595
+ opts.body = JSON.stringify(body)
1596
+ }
1597
+ }
1598
+
1599
+ const res = await fetchFn(input, {
1600
+ ...opts,
1601
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
1602
+ timeout: false,
1603
+ })
1604
+
1605
+ if (!chunkAbortCtl) return res
1606
+ return wrapSSE(res, chunkTimeout, chunkAbortCtl)
1607
+ }
1608
+
1609
+ const bundledLoader = BUNDLED_PROVIDERS[model.api.npm]
1610
+ if (bundledLoader) {
1611
+ log.info("using bundled provider", {
1612
+ providerID: model.providerID,
1613
+ pkg: model.api.npm,
1614
+ })
1615
+ const factory = await bundledLoader()
1616
+ const loaded = factory({
1617
+ name: model.providerID,
1618
+ ...options,
1619
+ })
1620
+ s.sdk.set(key, loaded)
1621
+ return loaded as SDK
1622
+ }
1623
+
1624
+ let installedPath: string
1625
+ if (!model.api.npm.startsWith("file://")) {
1626
+ const item = await Npm.add(model.api.npm)
1627
+ if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
1628
+ installedPath = item.entrypoint
1629
+ } else {
1630
+ log.info("loading local provider", { pkg: model.api.npm })
1631
+ installedPath = model.api.npm
1632
+ }
1633
+
1634
+ // `installedPath` is a local entry path or an existing `file://` URL. Normalize
1635
+ // only path inputs so Node on Windows accepts the dynamic import.
1636
+ const importSpec = installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
1637
+ const mod = await import(importSpec)
1638
+
1639
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
1640
+ const loaded = fn({
1641
+ name: model.providerID,
1642
+ ...options,
1643
+ })
1644
+ s.sdk.set(key, loaded)
1645
+ return loaded as SDK
1646
+ } catch (e) {
1647
+ throw new InitError({ providerID: model.providerID, cause: e })
1648
+ }
1649
+ }
1650
+
1651
+ const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
1652
+ InstanceState.use(state, (s) => s.providers[providerID]),
1653
+ )
1654
+
1655
+ const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
1656
+ const s = yield* InstanceState.get(state)
1657
+ const provider = s.providers[providerID]
1658
+ if (!provider) {
1659
+ const catalogProvider = s.catalog[providerID]
1660
+ const suggestions = catalogProvider
1661
+ ? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels)
1662
+ : fuzzysort
1663
+ .go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
1664
+ .map((m) => m.target)
1665
+ return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
1666
+ }
1667
+
1668
+ const info = provider.models[modelID]
1669
+ if (!info) {
1670
+ const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels)
1671
+ const suggestions = current.length
1672
+ ? current
1673
+ : modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels)
1674
+ return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
1675
+ }
1676
+ return info
1677
+ })
1678
+
1679
+ const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
1680
+ const s = yield* InstanceState.get(state)
1681
+ const envs = yield* env.all()
1682
+ const key = `${model.providerID}/${model.id}`
1683
+ if (s.models.has(key)) return s.models.get(key)!
1684
+
1685
+ const provider = s.providers[model.providerID]
1686
+ return yield* EffectPromise.refineRejection(
1687
+ async () => {
1688
+ const sdk = await resolveSDK(model, s, envs)
1689
+ const language = s.modelLoaders[model.providerID]
1690
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
1691
+ ...provider.options,
1692
+ ...model.options,
1693
+ })
1694
+ : sdk.languageModel(model.api.id)
1695
+ s.models.set(key, language)
1696
+ return language
1697
+ },
1698
+ (cause) =>
1699
+ cause instanceof NoSuchModelError
1700
+ ? new ModelNotFoundError({ modelID: model.id, providerID: model.providerID, cause })
1701
+ : undefined,
1702
+ )
1703
+ })
1704
+
1705
+ const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
1706
+ const s = yield* InstanceState.get(state)
1707
+ const provider = s.providers[providerID]
1708
+ if (!provider) return undefined
1709
+ for (const item of query) {
1710
+ for (const modelID of Object.keys(provider.models)) {
1711
+ if (modelID.includes(item)) return { providerID, modelID }
1712
+ }
1713
+ }
1714
+ return undefined
1715
+ })
1716
+
1717
+ const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
1718
+ const cfg = yield* config.get()
1719
+
1720
+ if (cfg.small_model) {
1721
+ const parsed = parseModel(cfg.small_model)
1722
+ return yield* getModel(parsed.providerID, parsed.modelID).pipe(
1723
+ Effect.catchTag("ProviderModelNotFoundError", () => Effect.succeed(undefined)),
1724
+ )
1725
+ }
1726
+
1727
+ const s = yield* InstanceState.get(state)
1728
+ const provider = s.providers[providerID]
1729
+ if (!provider) return undefined
1730
+
1731
+ let priority = [
1732
+ "claude-haiku-4-5",
1733
+ "claude-haiku-4.5",
1734
+ "3-5-haiku",
1735
+ "3.5-haiku",
1736
+ "gemini-3-flash",
1737
+ "gemini-2.5-flash",
1738
+ "gpt-5-nano",
1739
+ ]
1740
+ if (providerID.startsWith("opencode")) {
1741
+ priority = ["gpt-5-nano"]
1742
+ }
1743
+ if (providerID.startsWith("github-copilot")) {
1744
+ priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
1745
+ }
1746
+ for (const item of priority) {
1747
+ if (providerID === ProviderID.amazonBedrock) {
1748
+ const crossRegionPrefixes = ["global.", "us.", "eu."]
1749
+ const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
1750
+
1751
+ const globalMatch = candidates.find((m) => m.startsWith("global."))
1752
+ if (globalMatch) return provider.models[globalMatch]
1753
+
1754
+ const region = provider.options?.region
1755
+ if (region) {
1756
+ const regionPrefix = region.split("-")[0]
1757
+ if (regionPrefix === "us" || regionPrefix === "eu") {
1758
+ const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
1759
+ if (regionalMatch) return provider.models[regionalMatch]
1760
+ }
1761
+ }
1762
+
1763
+ const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
1764
+ if (unprefixed) return provider.models[unprefixed]
1765
+ } else {
1766
+ for (const model of Object.keys(provider.models)) {
1767
+ if (model.includes(item)) return provider.models[model]
1768
+ }
1769
+ }
1770
+ }
1771
+
1772
+ return undefined
1773
+ })
1774
+
1775
+ const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
1776
+ const cfg = yield* config.get()
1777
+ if (cfg.model) return parseModel(cfg.model)
1778
+
1779
+ const s = yield* InstanceState.get(state)
1780
+ const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
1781
+ Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
1782
+ if (!isRecord(x) || !Array.isArray(x.recent)) return []
1783
+ return x.recent.flatMap((item) => {
1784
+ if (!isRecord(item)) return []
1785
+ if (typeof item.providerID !== "string") return []
1786
+ if (typeof item.modelID !== "string") return []
1787
+ return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
1788
+ })
1789
+ }),
1790
+ Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
1791
+ )
1792
+ for (const entry of recent) {
1793
+ const provider = s.providers[entry.providerID]
1794
+ if (!provider) continue
1795
+ if (!provider.models[entry.modelID]) continue
1796
+ return { providerID: entry.providerID, modelID: entry.modelID }
1797
+ }
1798
+
1799
+ const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id))
1800
+ if (!provider) throw new Error("no providers found")
1801
+ const [model] = sort(Object.values(provider.models))
1802
+ if (!model) throw new Error("no models found")
1803
+ return {
1804
+ providerID: provider.id,
1805
+ modelID: model.id,
1806
+ }
1807
+ })
1808
+
1809
+ return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
1810
+ }),
1811
+ )
1812
+
1813
+ export const defaultLayer = Layer.suspend(() =>
1814
+ layer.pipe(
1815
+ Layer.provide(AppFileSystem.defaultLayer),
1816
+ Layer.provide(Env.defaultLayer),
1817
+ Layer.provide(Config.defaultLayer),
1818
+ Layer.provide(Auth.defaultLayer),
1819
+ Layer.provide(Plugin.defaultLayer),
1820
+ Layer.provide(ModelsDev.defaultLayer),
1821
+ Layer.provide(RuntimeFlags.defaultLayer),
1822
+ ),
1823
+ )
1824
+
1825
+ const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
1826
+ export function sort<T extends { id: string }>(models: T[]) {
1827
+ return sortBy(
1828
+ models,
1829
+ [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
1830
+ [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
1831
+ [(model) => model.id, "desc"],
1832
+ )
1833
+ }
1834
+
1835
+ export function parseModel(model: string) {
1836
+ const [providerID, ...rest] = model.split("/")
1837
+ return {
1838
+ providerID: ProviderID.make(providerID),
1839
+ modelID: ModelID.make(rest.join("/")),
1840
+ }
1841
+ }
1842
+
1843
+ export * as Provider from "./provider"