bonecode 1.0.0 → 1.2.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 (840) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -50
  3. package/bin/bonecode +47 -42
  4. package/bone/output/agent/.dockerignore +7 -7
  5. package/bone/output/agent/.env.example +36 -36
  6. package/bone/output/agent/.github/workflows/ci.yaml +58 -58
  7. package/bone/output/agent/AgentDomain.bone.map +349 -349
  8. package/bone/output/agent/AgentDomain.postman_collection.json +957 -957
  9. package/bone/output/agent/Dockerfile +22 -22
  10. package/bone/output/agent/README.md +47 -47
  11. package/bone/output/agent/admin/index.html +739 -739
  12. package/bone/output/agent/docker-compose.yaml +22 -22
  13. package/bone/output/agent/k8s/deployment.yaml +75 -75
  14. package/bone/output/agent/migrations/agent.sql +36 -36
  15. package/bone/output/agent/migrations/agent_instance.sql +36 -36
  16. package/bone/output/agent/migrations/audit_log.sql +18 -18
  17. package/bone/output/agent/migrations/build_step.sql +34 -34
  18. package/bone/output/agent/migrations/event_outbox.sql +31 -31
  19. package/bone/output/agent/migrations/plan.sql +30 -30
  20. package/bone/output/agent/migrations/task.sql +30 -30
  21. package/bone/output/agent/migrations/tool_call.sql +33 -33
  22. package/bone/output/agent/openapi.yaml +1116 -1116
  23. package/bone/output/agent/package.json +35 -35
  24. package/bone/output/agent/schema.graphql +233 -233
  25. package/bone/output/agent/sdk/client.ts +231 -231
  26. package/bone/output/agent/src/algorithms.ts +2 -2
  27. package/bone/output/agent/src/audit.ts +44 -44
  28. package/bone/output/agent/src/auth.ts +57 -57
  29. package/bone/output/agent/src/cron.ts +12 -12
  30. package/bone/output/agent/src/db.ts +31 -31
  31. package/bone/output/agent/src/debug.ts +66 -66
  32. package/bone/output/agent/src/events.ts +243 -243
  33. package/bone/output/agent/src/extensions.ts +54 -54
  34. package/bone/output/agent/src/failure_rules.ts +322 -322
  35. package/bone/output/agent/src/flows.ts +168 -168
  36. package/bone/output/agent/src/health.ts +43 -43
  37. package/bone/output/agent/src/index.ts +99 -99
  38. package/bone/output/agent/src/logger.ts +69 -66
  39. package/bone/output/agent/src/metrics.ts +75 -75
  40. package/bone/output/agent/src/migrate.ts +351 -351
  41. package/bone/output/agent/src/migration_diff.ts +108 -108
  42. package/bone/output/agent/src/notify.ts +125 -125
  43. package/bone/output/agent/src/routes/plan.ts +91 -91
  44. package/bone/output/agent/src/routes/task.ts +105 -105
  45. package/bone/output/agent/src/routes/tool_call.ts +166 -166
  46. package/bone/output/agent/src/schemas.ts +384 -384
  47. package/bone/output/agent/src/state_machines/agent_instance.ts +24 -24
  48. package/bone/output/agent/src/state_machines/build_step.ts +22 -22
  49. package/bone/output/agent/src/state_machines/plan.ts +22 -22
  50. package/bone/output/agent/src/state_machines/task.ts +22 -22
  51. package/bone/output/agent/src/state_machines/tool_call.ts +22 -22
  52. package/bone/output/agent/src/tests.ts +361 -361
  53. package/bone/output/agent/src/websocket.ts +200 -200
  54. package/bone/output/agent/tsconfig.json +24 -24
  55. package/bone/output/rag/.dockerignore +7 -7
  56. package/bone/output/rag/.env.example +36 -36
  57. package/bone/output/rag/.github/workflows/ci.yaml +58 -58
  58. package/bone/output/rag/Dockerfile +22 -22
  59. package/bone/output/rag/RAGDomain.bone.map +286 -286
  60. package/bone/output/rag/RAGDomain.postman_collection.json +922 -922
  61. package/bone/output/rag/README.md +47 -47
  62. package/bone/output/rag/admin/index.html +817 -817
  63. package/bone/output/rag/docker-compose.yaml +22 -22
  64. package/bone/output/rag/k8s/deployment.yaml +75 -75
  65. package/bone/output/rag/migrations/audit_log.sql +18 -18
  66. package/bone/output/rag/migrations/code_chunk.sql +34 -34
  67. package/bone/output/rag/migrations/code_file.sql +33 -33
  68. package/bone/output/rag/migrations/event_outbox.sql +31 -31
  69. package/bone/output/rag/migrations/indexing_job.sql +33 -33
  70. package/bone/output/rag/migrations/knowledge_base.sql +35 -35
  71. package/bone/output/rag/migrations/memory_entry.sql +34 -34
  72. package/bone/output/rag/openapi.yaml +1097 -1097
  73. package/bone/output/rag/package.json +35 -35
  74. package/bone/output/rag/schema.graphql +245 -245
  75. package/bone/output/rag/sdk/client.ts +234 -234
  76. package/bone/output/rag/src/algorithms.ts +2 -2
  77. package/bone/output/rag/src/audit.ts +37 -37
  78. package/bone/output/rag/src/auth.ts +57 -57
  79. package/bone/output/rag/src/cron.ts +12 -12
  80. package/bone/output/rag/src/db.ts +31 -31
  81. package/bone/output/rag/src/debug.ts +66 -66
  82. package/bone/output/rag/src/events.ts +243 -243
  83. package/bone/output/rag/src/extensions.ts +350 -350
  84. package/bone/output/rag/src/failure_rules.ts +314 -314
  85. package/bone/output/rag/src/flows.ts +239 -239
  86. package/bone/output/rag/src/health.ts +43 -43
  87. package/bone/output/rag/src/index.ts +94 -94
  88. package/bone/output/rag/src/logger.ts +69 -66
  89. package/bone/output/rag/src/metrics.ts +75 -75
  90. package/bone/output/rag/src/migrate.ts +363 -363
  91. package/bone/output/rag/src/migration_diff.ts +108 -108
  92. package/bone/output/rag/src/notify.ts +99 -99
  93. package/bone/output/rag/src/routes/code_chunk.ts +75 -75
  94. package/bone/output/rag/src/routes/code_file.ts +101 -101
  95. package/bone/output/rag/src/routes/indexing_job.ts +87 -87
  96. package/bone/output/rag/src/routes/knowledge_base.ts +230 -230
  97. package/bone/output/rag/src/routes/memory_entry.ts +87 -87
  98. package/bone/output/rag/src/schemas.ts +394 -394
  99. package/bone/output/rag/src/state_machines/code_file.ts +23 -23
  100. package/bone/output/rag/src/state_machines/indexing_job.ts +22 -22
  101. package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -23
  102. package/bone/output/rag/src/state_machines/memory_entry.ts +20 -20
  103. package/bone/output/rag/src/tests.ts +339 -339
  104. package/bone/output/rag/tsconfig.json +24 -24
  105. package/bone/output/session/.dockerignore +7 -7
  106. package/bone/output/session/.env.example +36 -36
  107. package/bone/output/session/.github/workflows/ci.yaml +58 -58
  108. package/bone/output/session/Dockerfile +22 -22
  109. package/bone/output/session/README.md +47 -47
  110. package/bone/output/session/SessionDomain.bone.map +349 -349
  111. package/bone/output/session/SessionDomain.postman_collection.json +957 -957
  112. package/bone/output/session/admin/index.html +666 -666
  113. package/bone/output/session/docker-compose.yaml +22 -22
  114. package/bone/output/session/k8s/deployment.yaml +75 -75
  115. package/bone/output/session/migrations/audit_log.sql +18 -18
  116. package/bone/output/session/migrations/event_outbox.sql +31 -31
  117. package/bone/output/session/migrations/message.sql +31 -31
  118. package/bone/output/session/migrations/part.sql +28 -28
  119. package/bone/output/session/migrations/permission.sql +28 -28
  120. package/bone/output/session/migrations/project.sql +28 -28
  121. package/bone/output/session/migrations/session.sql +38 -38
  122. package/bone/output/session/openapi.yaml +1101 -1101
  123. package/bone/output/session/package.json +35 -35
  124. package/bone/output/session/schema.graphql +222 -222
  125. package/bone/output/session/sdk/client.ts +225 -225
  126. package/bone/output/session/src/algorithms.ts +2 -2
  127. package/bone/output/session/src/audit.ts +44 -44
  128. package/bone/output/session/src/auth.ts +57 -57
  129. package/bone/output/session/src/cron.ts +12 -12
  130. package/bone/output/session/src/db.ts +31 -31
  131. package/bone/output/session/src/debug.ts +66 -66
  132. package/bone/output/session/src/events.ts +270 -270
  133. package/bone/output/session/src/extensions.ts +215 -215
  134. package/bone/output/session/src/failure_rules.ts +283 -283
  135. package/bone/output/session/src/flows.ts +168 -168
  136. package/bone/output/session/src/health.ts +43 -43
  137. package/bone/output/session/src/index.ts +99 -99
  138. package/bone/output/session/src/logger.ts +67 -66
  139. package/bone/output/session/src/metrics.ts +75 -75
  140. package/bone/output/session/src/migrate.ts +331 -331
  141. package/bone/output/session/src/migration_diff.ts +108 -108
  142. package/bone/output/session/src/notify.ts +112 -112
  143. package/bone/output/session/src/routes/message.ts +93 -93
  144. package/bone/output/session/src/routes/part.ts +79 -79
  145. package/bone/output/session/src/routes/permission.ts +79 -79
  146. package/bone/output/session/src/routes/project.ts +79 -79
  147. package/bone/output/session/src/routes/session.ts +294 -294
  148. package/bone/output/session/src/schemas.ts +357 -357
  149. package/bone/output/session/src/state_machines/session.ts +23 -23
  150. package/bone/output/session/src/tests.ts +325 -325
  151. package/bone/output/session/src/websocket.ts +223 -200
  152. package/bone/output/session/tsconfig.json +24 -24
  153. package/bone/output/workspace/.dockerignore +7 -7
  154. package/bone/output/workspace/.env.example +36 -36
  155. package/bone/output/workspace/.github/workflows/ci.yaml +58 -58
  156. package/bone/output/workspace/Dockerfile +22 -22
  157. package/bone/output/workspace/README.md +45 -45
  158. package/bone/output/workspace/WorkspaceDomain.bone.map +188 -188
  159. package/bone/output/workspace/WorkspaceDomain.postman_collection.json +620 -620
  160. package/bone/output/workspace/admin/index.html +484 -484
  161. package/bone/output/workspace/docker-compose.yaml +22 -22
  162. package/bone/output/workspace/k8s/deployment.yaml +75 -75
  163. package/bone/output/workspace/migrations/audit_log.sql +18 -18
  164. package/bone/output/workspace/migrations/codebase.sql +34 -34
  165. package/bone/output/workspace/migrations/event_outbox.sql +31 -31
  166. package/bone/output/workspace/migrations/snapshot.sql +32 -32
  167. package/bone/output/workspace/migrations/workspace.sql +33 -33
  168. package/bone/output/workspace/openapi.yaml +721 -721
  169. package/bone/output/workspace/package.json +35 -35
  170. package/bone/output/workspace/schema.graphql +153 -153
  171. package/bone/output/workspace/sdk/client.ts +155 -155
  172. package/bone/output/workspace/src/algorithms.ts +2 -2
  173. package/bone/output/workspace/src/audit.ts +37 -37
  174. package/bone/output/workspace/src/auth.ts +57 -57
  175. package/bone/output/workspace/src/cron.ts +12 -12
  176. package/bone/output/workspace/src/db.ts +31 -31
  177. package/bone/output/workspace/src/debug.ts +66 -66
  178. package/bone/output/workspace/src/events.ts +243 -243
  179. package/bone/output/workspace/src/extensions.ts +44 -44
  180. package/bone/output/workspace/src/failure_rules.ts +152 -152
  181. package/bone/output/workspace/src/health.ts +43 -43
  182. package/bone/output/workspace/src/index.ts +88 -88
  183. package/bone/output/workspace/src/logger.ts +69 -66
  184. package/bone/output/workspace/src/metrics.ts +75 -75
  185. package/bone/output/workspace/src/migrate.ts +219 -219
  186. package/bone/output/workspace/src/migration_diff.ts +108 -108
  187. package/bone/output/workspace/src/notify.ts +73 -73
  188. package/bone/output/workspace/src/routes/codebase.ts +87 -87
  189. package/bone/output/workspace/src/routes/snapshot.ts +127 -127
  190. package/bone/output/workspace/src/routes/workspace.ts +190 -190
  191. package/bone/output/workspace/src/schemas.ts +231 -231
  192. package/bone/output/workspace/src/state_machines/codebase.ts +21 -21
  193. package/bone/output/workspace/src/state_machines/snapshot.ts +20 -20
  194. package/bone/output/workspace/src/state_machines/workspace.ts +21 -21
  195. package/bone/output/workspace/src/tests.ts +248 -248
  196. package/bone/output/workspace/tsconfig.json +24 -24
  197. package/compat/opencode_adapter.ts +282 -34
  198. package/dist/bone/output/agent/src/algorithms.d.ts +1 -0
  199. package/dist/bone/output/agent/src/algorithms.js +3 -0
  200. package/dist/bone/output/agent/src/algorithms.js.map +1 -0
  201. package/dist/bone/output/agent/src/audit.d.ts +3 -0
  202. package/dist/bone/output/agent/src/audit.js +40 -0
  203. package/dist/bone/output/agent/src/audit.js.map +1 -0
  204. package/dist/bone/output/agent/src/auth.d.ts +8 -0
  205. package/dist/bone/output/agent/src/auth.js +56 -0
  206. package/dist/bone/output/agent/src/auth.js.map +1 -0
  207. package/dist/bone/output/agent/src/db.d.ts +6 -0
  208. package/dist/bone/output/agent/src/db.js +63 -0
  209. package/dist/bone/output/agent/src/db.js.map +1 -0
  210. package/dist/bone/output/agent/src/events.d.ts +25 -0
  211. package/dist/bone/output/agent/src/events.js +184 -0
  212. package/dist/bone/output/agent/src/events.js.map +1 -0
  213. package/dist/bone/output/agent/src/logger.d.ts +28 -0
  214. package/dist/bone/output/agent/src/logger.js +45 -0
  215. package/dist/bone/output/agent/src/logger.js.map +1 -0
  216. package/dist/bone/output/agent/src/metrics.d.ts +5 -0
  217. package/dist/bone/output/agent/src/metrics.js +60 -0
  218. package/dist/bone/output/agent/src/metrics.js.map +1 -0
  219. package/dist/bone/output/agent/src/routes/agent_instance.d.ts +1 -0
  220. package/dist/bone/output/agent/src/routes/agent_instance.js +253 -0
  221. package/dist/bone/output/agent/src/routes/agent_instance.js.map +1 -0
  222. package/dist/bone/output/agent/src/routes/build_step.d.ts +1 -0
  223. package/dist/bone/output/agent/src/routes/build_step.js +133 -0
  224. package/dist/bone/output/agent/src/routes/build_step.js.map +1 -0
  225. package/dist/bone/output/agent/src/routes/plan.d.ts +1 -0
  226. package/dist/bone/output/agent/src/routes/plan.js +119 -0
  227. package/dist/bone/output/agent/src/routes/plan.js.map +1 -0
  228. package/dist/bone/output/agent/src/routes/task.d.ts +1 -0
  229. package/dist/bone/output/agent/src/routes/task.js +133 -0
  230. package/dist/bone/output/agent/src/routes/task.js.map +1 -0
  231. package/dist/bone/output/agent/src/routes/tool_call.d.ts +1 -0
  232. package/dist/bone/output/agent/src/routes/tool_call.js +190 -0
  233. package/dist/bone/output/agent/src/routes/tool_call.js.map +1 -0
  234. package/dist/bone/output/agent/src/state_machines/agent_instance.d.ts +9 -0
  235. package/dist/bone/output/agent/src/state_machines/agent_instance.js +22 -0
  236. package/dist/bone/output/agent/src/state_machines/agent_instance.js.map +1 -0
  237. package/dist/bone/output/agent/src/state_machines/build_step.d.ts +9 -0
  238. package/dist/bone/output/agent/src/state_machines/build_step.js +20 -0
  239. package/dist/bone/output/agent/src/state_machines/build_step.js.map +1 -0
  240. package/dist/bone/output/agent/src/state_machines/plan.d.ts +9 -0
  241. package/dist/bone/output/agent/src/state_machines/plan.js +20 -0
  242. package/dist/bone/output/agent/src/state_machines/plan.js.map +1 -0
  243. package/dist/bone/output/agent/src/state_machines/task.d.ts +9 -0
  244. package/dist/bone/output/agent/src/state_machines/task.js +20 -0
  245. package/dist/bone/output/agent/src/state_machines/task.js.map +1 -0
  246. package/dist/bone/output/agent/src/state_machines/tool_call.d.ts +9 -0
  247. package/dist/bone/output/agent/src/state_machines/tool_call.js +20 -0
  248. package/dist/bone/output/agent/src/state_machines/tool_call.js.map +1 -0
  249. package/dist/bone/output/rag/src/algorithms.d.ts +1 -0
  250. package/dist/bone/output/rag/src/algorithms.js +3 -0
  251. package/dist/bone/output/rag/src/algorithms.js.map +1 -0
  252. package/dist/bone/output/rag/src/auth.d.ts +8 -0
  253. package/dist/bone/output/rag/src/auth.js +56 -0
  254. package/dist/bone/output/rag/src/auth.js.map +1 -0
  255. package/dist/bone/output/rag/src/db.d.ts +6 -0
  256. package/dist/bone/output/rag/src/db.js +63 -0
  257. package/dist/bone/output/rag/src/db.js.map +1 -0
  258. package/dist/bone/output/rag/src/events.d.ts +25 -0
  259. package/dist/bone/output/rag/src/events.js +184 -0
  260. package/dist/bone/output/rag/src/events.js.map +1 -0
  261. package/dist/bone/output/rag/src/extensions.d.ts +83 -0
  262. package/dist/bone/output/rag/src/extensions.js +329 -0
  263. package/dist/bone/output/rag/src/extensions.js.map +1 -0
  264. package/dist/bone/output/rag/src/flows.d.ts +24 -0
  265. package/dist/bone/output/rag/src/flows.js +236 -0
  266. package/dist/bone/output/rag/src/flows.js.map +1 -0
  267. package/dist/bone/output/rag/src/logger.d.ts +28 -0
  268. package/dist/bone/output/rag/src/logger.js +45 -0
  269. package/dist/bone/output/rag/src/logger.js.map +1 -0
  270. package/dist/bone/output/rag/src/metrics.d.ts +5 -0
  271. package/dist/bone/output/rag/src/metrics.js +60 -0
  272. package/dist/bone/output/rag/src/metrics.js.map +1 -0
  273. package/dist/bone/output/rag/src/routes/code_chunk.d.ts +1 -0
  274. package/dist/bone/output/rag/src/routes/code_chunk.js +100 -0
  275. package/dist/bone/output/rag/src/routes/code_chunk.js.map +1 -0
  276. package/dist/bone/output/rag/src/routes/code_file.d.ts +1 -0
  277. package/dist/bone/output/rag/src/routes/code_file.js +127 -0
  278. package/dist/bone/output/rag/src/routes/code_file.js.map +1 -0
  279. package/dist/bone/output/rag/src/routes/indexing_job.d.ts +1 -0
  280. package/dist/bone/output/rag/src/routes/indexing_job.js +113 -0
  281. package/dist/bone/output/rag/src/routes/indexing_job.js.map +1 -0
  282. package/dist/bone/output/rag/src/routes/knowledge_base.d.ts +1 -0
  283. package/dist/bone/output/rag/src/routes/knowledge_base.js +242 -0
  284. package/dist/bone/output/rag/src/routes/knowledge_base.js.map +1 -0
  285. package/dist/bone/output/rag/src/routes/memory_entry.d.ts +1 -0
  286. package/dist/bone/output/rag/src/routes/memory_entry.js +113 -0
  287. package/dist/bone/output/rag/src/routes/memory_entry.js.map +1 -0
  288. package/dist/bone/output/rag/src/state_machines/code_file.d.ts +9 -0
  289. package/dist/bone/output/rag/src/state_machines/code_file.js +21 -0
  290. package/dist/bone/output/rag/src/state_machines/code_file.js.map +1 -0
  291. package/dist/bone/output/rag/src/state_machines/indexing_job.d.ts +9 -0
  292. package/dist/bone/output/rag/src/state_machines/indexing_job.js +20 -0
  293. package/dist/bone/output/rag/src/state_machines/indexing_job.js.map +1 -0
  294. package/dist/bone/output/rag/src/state_machines/knowledge_base.d.ts +9 -0
  295. package/dist/bone/output/rag/src/state_machines/knowledge_base.js +21 -0
  296. package/dist/bone/output/rag/src/state_machines/knowledge_base.js.map +1 -0
  297. package/dist/bone/output/rag/src/state_machines/memory_entry.d.ts +9 -0
  298. package/dist/bone/output/rag/src/state_machines/memory_entry.js +18 -0
  299. package/dist/bone/output/rag/src/state_machines/memory_entry.js.map +1 -0
  300. package/dist/bone/output/session/src/algorithms.d.ts +1 -0
  301. package/dist/bone/output/session/src/algorithms.js +3 -0
  302. package/dist/bone/output/session/src/algorithms.js.map +1 -0
  303. package/dist/bone/output/session/src/audit.d.ts +3 -0
  304. package/dist/bone/output/session/src/audit.js +40 -0
  305. package/dist/bone/output/session/src/audit.js.map +1 -0
  306. package/dist/bone/output/session/src/auth.d.ts +8 -0
  307. package/dist/bone/output/session/src/auth.js +56 -0
  308. package/dist/bone/output/session/src/auth.js.map +1 -0
  309. package/dist/bone/output/session/src/db.d.ts +6 -0
  310. package/dist/bone/output/session/src/db.js +63 -0
  311. package/dist/bone/output/session/src/db.js.map +1 -0
  312. package/dist/bone/output/session/src/events.d.ts +26 -0
  313. package/dist/bone/output/session/src/events.js +212 -0
  314. package/dist/bone/output/session/src/events.js.map +1 -0
  315. package/dist/bone/output/session/src/extensions.d.ts +41 -0
  316. package/dist/bone/output/session/src/extensions.js +217 -0
  317. package/dist/bone/output/session/src/extensions.js.map +1 -0
  318. package/dist/bone/output/session/src/logger.d.ts +28 -0
  319. package/dist/bone/output/session/src/logger.js +44 -0
  320. package/dist/bone/output/session/src/logger.js.map +1 -0
  321. package/dist/bone/output/session/src/metrics.d.ts +5 -0
  322. package/dist/bone/output/session/src/metrics.js +60 -0
  323. package/dist/bone/output/session/src/metrics.js.map +1 -0
  324. package/dist/bone/output/session/src/routes/message.d.ts +1 -0
  325. package/dist/bone/output/session/src/routes/message.js +120 -0
  326. package/dist/bone/output/session/src/routes/message.js.map +1 -0
  327. package/dist/bone/output/session/src/routes/part.d.ts +1 -0
  328. package/dist/bone/output/session/src/routes/part.js +106 -0
  329. package/dist/bone/output/session/src/routes/part.js.map +1 -0
  330. package/dist/bone/output/session/src/routes/permission.d.ts +1 -0
  331. package/dist/bone/output/session/src/routes/permission.js +106 -0
  332. package/dist/bone/output/session/src/routes/permission.js.map +1 -0
  333. package/dist/bone/output/session/src/routes/project.d.ts +1 -0
  334. package/dist/bone/output/session/src/routes/project.js +106 -0
  335. package/dist/bone/output/session/src/routes/project.js.map +1 -0
  336. package/dist/bone/output/session/src/routes/session.d.ts +1 -0
  337. package/dist/bone/output/session/src/routes/session.js +308 -0
  338. package/dist/bone/output/session/src/routes/session.js.map +1 -0
  339. package/dist/bone/output/session/src/state_machines/session.d.ts +9 -0
  340. package/dist/bone/output/session/src/state_machines/session.js +21 -0
  341. package/dist/bone/output/session/src/state_machines/session.js.map +1 -0
  342. package/dist/bone/output/session/src/websocket.d.ts +15 -0
  343. package/dist/bone/output/session/src/websocket.js +215 -0
  344. package/dist/bone/output/session/src/websocket.js.map +1 -0
  345. package/dist/bone/output/workspace/src/algorithms.d.ts +1 -0
  346. package/dist/bone/output/workspace/src/algorithms.js +3 -0
  347. package/dist/bone/output/workspace/src/algorithms.js.map +1 -0
  348. package/dist/bone/output/workspace/src/auth.d.ts +8 -0
  349. package/dist/bone/output/workspace/src/auth.js +56 -0
  350. package/dist/bone/output/workspace/src/auth.js.map +1 -0
  351. package/dist/bone/output/workspace/src/db.d.ts +6 -0
  352. package/dist/bone/output/workspace/src/db.js +63 -0
  353. package/dist/bone/output/workspace/src/db.js.map +1 -0
  354. package/dist/bone/output/workspace/src/events.d.ts +25 -0
  355. package/dist/bone/output/workspace/src/events.js +184 -0
  356. package/dist/bone/output/workspace/src/events.js.map +1 -0
  357. package/dist/bone/output/workspace/src/logger.d.ts +28 -0
  358. package/dist/bone/output/workspace/src/logger.js +45 -0
  359. package/dist/bone/output/workspace/src/logger.js.map +1 -0
  360. package/dist/bone/output/workspace/src/metrics.d.ts +5 -0
  361. package/dist/bone/output/workspace/src/metrics.js +60 -0
  362. package/dist/bone/output/workspace/src/metrics.js.map +1 -0
  363. package/dist/bone/output/workspace/src/routes/codebase.d.ts +1 -0
  364. package/dist/bone/output/workspace/src/routes/codebase.js +113 -0
  365. package/dist/bone/output/workspace/src/routes/codebase.js.map +1 -0
  366. package/dist/bone/output/workspace/src/routes/snapshot.d.ts +1 -0
  367. package/dist/bone/output/workspace/src/routes/snapshot.js +151 -0
  368. package/dist/bone/output/workspace/src/routes/snapshot.js.map +1 -0
  369. package/dist/bone/output/workspace/src/routes/workspace.d.ts +1 -0
  370. package/dist/bone/output/workspace/src/routes/workspace.js +209 -0
  371. package/dist/bone/output/workspace/src/routes/workspace.js.map +1 -0
  372. package/dist/bone/output/workspace/src/state_machines/codebase.d.ts +9 -0
  373. package/dist/bone/output/workspace/src/state_machines/codebase.js +19 -0
  374. package/dist/bone/output/workspace/src/state_machines/codebase.js.map +1 -0
  375. package/dist/bone/output/workspace/src/state_machines/snapshot.d.ts +9 -0
  376. package/dist/bone/output/workspace/src/state_machines/snapshot.js +18 -0
  377. package/dist/bone/output/workspace/src/state_machines/snapshot.js.map +1 -0
  378. package/dist/bone/output/workspace/src/state_machines/workspace.d.ts +9 -0
  379. package/dist/bone/output/workspace/src/state_machines/workspace.js +19 -0
  380. package/dist/bone/output/workspace/src/state_machines/workspace.js.map +1 -0
  381. package/dist/compat/opencode_adapter.d.ts +25 -0
  382. package/dist/compat/opencode_adapter.js +599 -0
  383. package/dist/compat/opencode_adapter.js.map +1 -0
  384. package/dist/extensions/chunker.d.ts +24 -0
  385. package/dist/extensions/chunker.js +360 -0
  386. package/dist/extensions/chunker.js.map +1 -0
  387. package/dist/extensions/embedding_provider.d.ts +18 -0
  388. package/dist/extensions/embedding_provider.js +150 -0
  389. package/dist/extensions/embedding_provider.js.map +1 -0
  390. package/dist/extensions/llm_provider.d.ts +33 -0
  391. package/dist/extensions/llm_provider.js +338 -0
  392. package/dist/extensions/llm_provider.js.map +1 -0
  393. package/dist/extensions/mcp_bridge.d.ts +44 -0
  394. package/dist/extensions/mcp_bridge.js +151 -0
  395. package/dist/extensions/mcp_bridge.js.map +1 -0
  396. package/dist/extensions/rag_search.d.ts +38 -0
  397. package/dist/extensions/rag_search.js +242 -0
  398. package/dist/extensions/rag_search.js.map +1 -0
  399. package/dist/extensions/snapshot.d.ts +14 -0
  400. package/dist/extensions/snapshot.js +158 -0
  401. package/dist/extensions/snapshot.js.map +1 -0
  402. package/dist/extensions/tool_executor.d.ts +28 -0
  403. package/dist/extensions/tool_executor.js +268 -0
  404. package/dist/extensions/tool_executor.js.map +1 -0
  405. package/dist/src/cli.d.ts +15 -0
  406. package/dist/src/cli.js +687 -0
  407. package/dist/src/cli.js.map +1 -0
  408. package/dist/src/config.d.ts +44 -0
  409. package/dist/src/config.js +165 -0
  410. package/dist/src/config.js.map +1 -0
  411. package/dist/src/context_builder.d.ts +51 -0
  412. package/dist/src/context_builder.js +558 -0
  413. package/dist/src/context_builder.js.map +1 -0
  414. package/dist/src/db_adapter.d.ts +24 -0
  415. package/dist/src/db_adapter.js +341 -0
  416. package/dist/src/db_adapter.js.map +1 -0
  417. package/dist/src/engine/session/compaction_logic.d.ts +11 -0
  418. package/dist/src/engine/session/compaction_logic.js +113 -0
  419. package/dist/src/engine/session/compaction_logic.js.map +1 -0
  420. package/dist/src/engine/session/instruction_loader.d.ts +5 -0
  421. package/dist/src/engine/session/instruction_loader.js +78 -0
  422. package/dist/src/engine/session/instruction_loader.js.map +1 -0
  423. package/dist/src/engine/session/overflow_check.d.ts +14 -0
  424. package/dist/src/engine/session/overflow_check.js +45 -0
  425. package/dist/src/engine/session/overflow_check.js.map +1 -0
  426. package/dist/src/engine/session/prompt.d.ts +45 -0
  427. package/dist/src/engine/session/prompt.js +584 -0
  428. package/dist/src/engine/session/prompt.js.map +1 -0
  429. package/dist/src/engine/session/provider_transform.d.ts +59 -0
  430. package/dist/src/engine/session/provider_transform.js +193 -0
  431. package/dist/src/engine/session/provider_transform.js.map +1 -0
  432. package/dist/src/engine/session/retry_logic.d.ts +12 -0
  433. package/dist/src/engine/session/retry_logic.js +72 -0
  434. package/dist/src/engine/session/retry_logic.js.map +1 -0
  435. package/dist/src/engine/session/system_prompt.d.ts +9 -0
  436. package/dist/src/engine/session/system_prompt.js +96 -0
  437. package/dist/src/engine/session/system_prompt.js.map +1 -0
  438. package/dist/src/engine/session/tool_registry.d.ts +5 -0
  439. package/dist/src/engine/session/tool_registry.js +117 -0
  440. package/dist/src/engine/session/tool_registry.js.map +1 -0
  441. package/dist/src/export.d.ts +13 -0
  442. package/dist/src/export.js +103 -0
  443. package/dist/src/export.js.map +1 -0
  444. package/dist/src/mdns.d.ts +7 -0
  445. package/dist/src/mdns.js +60 -0
  446. package/dist/src/mdns.js.map +1 -0
  447. package/dist/src/rag_worker.d.ts +38 -0
  448. package/dist/src/rag_worker.js +435 -0
  449. package/dist/src/rag_worker.js.map +1 -0
  450. package/dist/src/server.d.ts +11 -0
  451. package/dist/src/server.js +214 -0
  452. package/dist/src/server.js.map +1 -0
  453. package/dist/src/stats.d.ts +45 -0
  454. package/dist/src/stats.js +233 -0
  455. package/dist/src/stats.js.map +1 -0
  456. package/dist/src/tui.d.ts +29 -0
  457. package/dist/src/tui.js +1053 -0
  458. package/dist/src/tui.js.map +1 -0
  459. package/package.json +21 -5
  460. package/src/cli.ts +314 -113
  461. package/src/db_adapter.ts +354 -0
  462. package/src/engine/account/account.sql.ts +39 -39
  463. package/src/engine/account/account.ts +456 -456
  464. package/src/engine/account/repo.ts +166 -166
  465. package/src/engine/account/schema.ts +99 -99
  466. package/src/engine/account/url.ts +8 -8
  467. package/src/engine/acp/README.md +174 -174
  468. package/src/engine/acp/agent.ts +1968 -1968
  469. package/src/engine/acp/runtime.ts +22 -22
  470. package/src/engine/acp/session.ts +122 -122
  471. package/src/engine/acp/types.ts +24 -24
  472. package/src/engine/agent/agent.ts +463 -463
  473. package/src/engine/agent/generate.txt +75 -75
  474. package/src/engine/agent/prompt/compaction.txt +9 -9
  475. package/src/engine/agent/prompt/explore.txt +18 -18
  476. package/src/engine/agent/prompt/scout.txt +36 -36
  477. package/src/engine/agent/prompt/summary.txt +11 -11
  478. package/src/engine/agent/prompt/title.txt +44 -44
  479. package/src/engine/agent/subagent-permissions.ts +34 -34
  480. package/src/engine/auth/index.ts +96 -96
  481. package/src/engine/background/background/job.ts +200 -200
  482. package/src/engine/background/job.ts +200 -200
  483. package/src/engine/bus/bus-event.ts +45 -45
  484. package/src/engine/bus/global.ts +22 -22
  485. package/src/engine/bus/index.ts +203 -203
  486. package/src/engine/command/command/index.ts +181 -181
  487. package/src/engine/command/command/template/initialize.txt +66 -66
  488. package/src/engine/command/command/template/review.txt +101 -101
  489. package/src/engine/command/index.ts +181 -181
  490. package/src/engine/command/template/initialize.txt +66 -66
  491. package/src/engine/command/template/review.txt +101 -101
  492. package/src/engine/config/agent.ts +172 -172
  493. package/src/engine/config/attachment.ts +25 -25
  494. package/src/engine/config/command.ts +62 -62
  495. package/src/engine/config/config.ts +833 -833
  496. package/src/engine/config/console-state.ts +14 -14
  497. package/src/engine/config/entry-name.ts +16 -16
  498. package/src/engine/config/error.ts +23 -23
  499. package/src/engine/config/formatter.ts +13 -13
  500. package/src/engine/config/layout.ts +6 -6
  501. package/src/engine/config/lsp.ts +43 -43
  502. package/src/engine/config/managed.ts +71 -71
  503. package/src/engine/config/markdown.ts +96 -96
  504. package/src/engine/config/mcp.ts +56 -56
  505. package/src/engine/config/model-id.ts +5 -5
  506. package/src/engine/config/parse.ts +79 -79
  507. package/src/engine/config/paths.ts +45 -45
  508. package/src/engine/config/permission.ts +58 -58
  509. package/src/engine/config/plugin.ts +84 -84
  510. package/src/engine/config/provider.ts +111 -111
  511. package/src/engine/config/reference.ts +23 -23
  512. package/src/engine/config/server.ts +19 -19
  513. package/src/engine/config/skills.ts +14 -14
  514. package/src/engine/config/variable.ts +90 -90
  515. package/src/engine/control-plane/adapters/index.ts +41 -41
  516. package/src/engine/control-plane/adapters/worktree.ts +96 -96
  517. package/src/engine/control-plane/dev/README.md +19 -19
  518. package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -73
  519. package/src/engine/control-plane/schema.ts +14 -14
  520. package/src/engine/control-plane/types.ts +59 -59
  521. package/src/engine/control-plane/util.ts +39 -39
  522. package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -51
  523. package/src/engine/control-plane/workspace-context.ts +26 -26
  524. package/src/engine/control-plane/workspace.sql.ts +20 -20
  525. package/src/engine/control-plane/workspace.ts +1072 -1072
  526. package/src/engine/data-migration.ts +161 -161
  527. package/src/engine/effect/app-runtime.ts +143 -143
  528. package/src/engine/effect/bootstrap-runtime.ts +29 -29
  529. package/src/engine/effect/bridge.ts +84 -84
  530. package/src/engine/effect/config-service.ts +67 -67
  531. package/src/engine/effect/instance-ref.ts +11 -11
  532. package/src/engine/effect/instance-registry.ts +12 -12
  533. package/src/engine/effect/instance-state.ts +72 -72
  534. package/src/engine/effect/promise.ts +17 -17
  535. package/src/engine/effect/run-service.ts +47 -47
  536. package/src/engine/effect/runner.ts +217 -217
  537. package/src/engine/effect/runtime-flags.ts +74 -74
  538. package/src/engine/effect/service-use.ts +38 -38
  539. package/src/engine/env/index.ts +37 -37
  540. package/src/engine/event-v2-bridge.ts +89 -89
  541. package/src/engine/file/file/ignore.ts +81 -81
  542. package/src/engine/file/file/index.ts +651 -651
  543. package/src/engine/file/file/protected.ts +59 -59
  544. package/src/engine/file/file/ripgrep.ts +481 -481
  545. package/src/engine/file/file/watcher.ts +167 -167
  546. package/src/engine/file/ignore.ts +81 -81
  547. package/src/engine/file/index.ts +651 -651
  548. package/src/engine/file/protected.ts +59 -59
  549. package/src/engine/file/ripgrep.ts +481 -481
  550. package/src/engine/file/watcher.ts +167 -167
  551. package/src/engine/format/format/formatter.ts +404 -404
  552. package/src/engine/format/format/index.ts +209 -209
  553. package/src/engine/format/formatter.ts +404 -404
  554. package/src/engine/format/index.ts +209 -209
  555. package/src/engine/git/git/index.ts +347 -347
  556. package/src/engine/git/index.ts +347 -347
  557. package/src/engine/id/id.ts +80 -80
  558. package/src/engine/ide/index.ts +70 -70
  559. package/src/engine/image/image/image.ts +176 -176
  560. package/src/engine/image/image.ts +176 -176
  561. package/src/engine/index.ts +251 -251
  562. package/src/engine/installation/index.ts +327 -327
  563. package/src/engine/lsp/client.ts +707 -707
  564. package/src/engine/lsp/diagnostic.ts +29 -29
  565. package/src/engine/lsp/language.ts +121 -121
  566. package/src/engine/lsp/launch.ts +21 -21
  567. package/src/engine/lsp/lsp/client.ts +707 -707
  568. package/src/engine/lsp/lsp/diagnostic.ts +29 -29
  569. package/src/engine/lsp/lsp/language.ts +121 -121
  570. package/src/engine/lsp/lsp/launch.ts +21 -21
  571. package/src/engine/lsp/lsp/lsp.ts +507 -507
  572. package/src/engine/lsp/lsp/server.ts +2064 -2064
  573. package/src/engine/lsp/lsp.ts +507 -507
  574. package/src/engine/lsp/server.ts +2064 -2064
  575. package/src/engine/mcp/auth.ts +146 -146
  576. package/src/engine/mcp/index.ts +958 -958
  577. package/src/engine/mcp/mcp/auth.ts +146 -146
  578. package/src/engine/mcp/mcp/index.ts +958 -958
  579. package/src/engine/mcp/mcp/oauth-callback.ts +232 -232
  580. package/src/engine/mcp/mcp/oauth-provider.ts +214 -214
  581. package/src/engine/mcp/oauth-callback.ts +232 -232
  582. package/src/engine/mcp/oauth-provider.ts +214 -214
  583. package/src/engine/node.ts +6 -6
  584. package/src/engine/patch/index.ts +689 -689
  585. package/src/engine/patch/patch/index.ts +689 -689
  586. package/src/engine/permission/arity.ts +163 -163
  587. package/src/engine/permission/evaluate.ts +15 -15
  588. package/src/engine/permission/index.ts +306 -306
  589. package/src/engine/permission/permission/arity.ts +163 -163
  590. package/src/engine/permission/permission/evaluate.ts +15 -15
  591. package/src/engine/permission/permission/index.ts +306 -306
  592. package/src/engine/permission/permission/schema.ts +13 -13
  593. package/src/engine/permission/schema.ts +13 -13
  594. package/src/engine/plugin/azure.ts +26 -26
  595. package/src/engine/plugin/cloudflare.ts +76 -76
  596. package/src/engine/plugin/codex.ts +622 -622
  597. package/src/engine/plugin/digitalocean.ts +411 -411
  598. package/src/engine/plugin/github-copilot/copilot.ts +394 -394
  599. package/src/engine/plugin/github-copilot/models.ts +196 -196
  600. package/src/engine/plugin/index.ts +295 -295
  601. package/src/engine/plugin/install.ts +439 -439
  602. package/src/engine/plugin/loader.ts +216 -216
  603. package/src/engine/plugin/meta.ts +188 -188
  604. package/src/engine/plugin/shared.ts +323 -323
  605. package/src/engine/project/bootstrap-service.ts +9 -9
  606. package/src/engine/project/bootstrap.ts +75 -75
  607. package/src/engine/project/instance-context.ts +24 -24
  608. package/src/engine/project/instance-layer.ts +11 -11
  609. package/src/engine/project/instance-runtime.ts +16 -16
  610. package/src/engine/project/instance-store.ts +193 -193
  611. package/src/engine/project/project.sql.ts +17 -17
  612. package/src/engine/project/project.ts +537 -537
  613. package/src/engine/project/schema.ts +13 -13
  614. package/src/engine/project/vcs.ts +405 -405
  615. package/src/engine/provider/auth.ts +225 -225
  616. package/src/engine/provider/error.ts +204 -204
  617. package/src/engine/provider/model-status.ts +8 -8
  618. package/src/engine/provider/provider.ts +1843 -1843
  619. package/src/engine/provider/schema.ts +30 -30
  620. package/src/engine/provider/transform.ts +1376 -1376
  621. package/src/engine/pty/index.ts +365 -365
  622. package/src/engine/pty/input.ts +24 -24
  623. package/src/engine/pty/pty/index.ts +365 -365
  624. package/src/engine/pty/pty/input.ts +24 -24
  625. package/src/engine/pty/pty/pty.bun.ts +26 -26
  626. package/src/engine/pty/pty/pty.node.ts +27 -27
  627. package/src/engine/pty/pty/pty.ts +25 -25
  628. package/src/engine/pty/pty/schema.ts +14 -14
  629. package/src/engine/pty/pty/ticket.ts +68 -68
  630. package/src/engine/pty/pty.bun.ts +26 -26
  631. package/src/engine/pty/pty.node.ts +27 -27
  632. package/src/engine/pty/pty.ts +25 -25
  633. package/src/engine/pty/schema.ts +14 -14
  634. package/src/engine/pty/ticket.ts +68 -68
  635. package/src/engine/question/index.ts +213 -213
  636. package/src/engine/question/question/index.ts +213 -213
  637. package/src/engine/question/question/schema.ts +10 -10
  638. package/src/engine/question/schema.ts +10 -10
  639. package/src/engine/reference/reference/reference.ts +241 -241
  640. package/src/engine/reference/reference/repository-cache.ts +147 -147
  641. package/src/engine/reference/reference.ts +241 -241
  642. package/src/engine/reference/repository-cache.ts +147 -147
  643. package/src/engine/session/compaction.ts +651 -651
  644. package/src/engine/session/instruction.ts +238 -238
  645. package/src/engine/session/llm.ts +459 -459
  646. package/src/engine/session/message-error.ts +14 -14
  647. package/src/engine/session/message-v2.ts +1202 -1202
  648. package/src/engine/session/message.ts +146 -146
  649. package/src/engine/session/overflow.ts +32 -32
  650. package/src/engine/session/processor.ts +823 -823
  651. package/src/engine/session/prompt/anthropic.txt +105 -105
  652. package/src/engine/session/prompt/beast.txt +147 -147
  653. package/src/engine/session/prompt/build-switch.txt +5 -5
  654. package/src/engine/session/prompt/codex.txt +79 -79
  655. package/src/engine/session/prompt/copilot-gpt-5.txt +143 -143
  656. package/src/engine/session/prompt/default.txt +105 -105
  657. package/src/engine/session/prompt/gemini.txt +155 -155
  658. package/src/engine/session/prompt/gpt.txt +107 -107
  659. package/src/engine/session/prompt/kimi.txt +95 -95
  660. package/src/engine/session/prompt/max-steps.txt +15 -15
  661. package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -67
  662. package/src/engine/session/prompt/plan.txt +26 -26
  663. package/src/engine/session/prompt/trinity.txt +97 -97
  664. package/src/engine/session/prompt.ts +66 -9
  665. package/src/engine/session/retry.ts +200 -200
  666. package/src/engine/session/revert.ts +162 -162
  667. package/src/engine/session/run-state.ts +153 -153
  668. package/src/engine/session/schema.ts +26 -26
  669. package/src/engine/session/session.sql.ts +137 -137
  670. package/src/engine/session/session.ts +1011 -1011
  671. package/src/engine/session/status.ts +94 -94
  672. package/src/engine/session/summary.ts +164 -164
  673. package/src/engine/session/system.ts +84 -84
  674. package/src/engine/session/todo.ts +81 -81
  675. package/src/engine/share/session.ts +61 -61
  676. package/src/engine/share/share-next.ts +376 -376
  677. package/src/engine/share/share.sql.ts +13 -13
  678. package/src/engine/shell/shell/shell.ts +215 -215
  679. package/src/engine/shell/shell.ts +215 -215
  680. package/src/engine/skill/discovery.ts +116 -116
  681. package/src/engine/skill/index.ts +336 -336
  682. package/src/engine/skill/prompt/customize-opencode.md +377 -377
  683. package/src/engine/skill/skill/discovery.ts +116 -116
  684. package/src/engine/skill/skill/index.ts +336 -336
  685. package/src/engine/skill/skill/prompt/customize-opencode.md +377 -377
  686. package/src/engine/snapshot/index.ts +762 -762
  687. package/src/engine/snapshot/snapshot/index.ts +762 -762
  688. package/src/engine/sync/README.md +179 -179
  689. package/src/engine/sync/event.sql.ts +17 -17
  690. package/src/engine/sync/index.ts +410 -410
  691. package/src/engine/sync/schema.ts +11 -11
  692. package/src/engine/temporary.ts +33 -33
  693. package/src/engine/tool/apply_patch.ts +313 -313
  694. package/src/engine/tool/apply_patch.txt +33 -33
  695. package/src/engine/tool/edit.ts +711 -711
  696. package/src/engine/tool/edit.txt +10 -10
  697. package/src/engine/tool/external-directory.ts +49 -49
  698. package/src/engine/tool/glob.ts +103 -103
  699. package/src/engine/tool/glob.txt +6 -6
  700. package/src/engine/tool/grep.ts +156 -156
  701. package/src/engine/tool/grep.txt +8 -8
  702. package/src/engine/tool/invalid.ts +21 -21
  703. package/src/engine/tool/json-schema.ts +164 -164
  704. package/src/engine/tool/lsp.ts +113 -113
  705. package/src/engine/tool/lsp.txt +24 -24
  706. package/src/engine/tool/mcp-websearch.ts +96 -96
  707. package/src/engine/tool/plan-enter.txt +14 -14
  708. package/src/engine/tool/plan-exit.txt +13 -13
  709. package/src/engine/tool/plan.ts +78 -78
  710. package/src/engine/tool/question.ts +44 -44
  711. package/src/engine/tool/question.txt +10 -10
  712. package/src/engine/tool/read.ts +337 -337
  713. package/src/engine/tool/read.txt +14 -14
  714. package/src/engine/tool/registry.ts +472 -472
  715. package/src/engine/tool/repo_clone.ts +80 -80
  716. package/src/engine/tool/repo_clone.txt +5 -5
  717. package/src/engine/tool/repo_overview.ts +279 -279
  718. package/src/engine/tool/repo_overview.txt +4 -4
  719. package/src/engine/tool/schema.ts +14 -14
  720. package/src/engine/tool/shell/id.ts +19 -19
  721. package/src/engine/tool/shell/prompt.ts +295 -295
  722. package/src/engine/tool/shell/shell.txt +77 -77
  723. package/src/engine/tool/shell.ts +647 -647
  724. package/src/engine/tool/skill.ts +75 -75
  725. package/src/engine/tool/skill.txt +5 -5
  726. package/src/engine/tool/task.ts +337 -337
  727. package/src/engine/tool/task.txt +58 -58
  728. package/src/engine/tool/task_status.ts +179 -179
  729. package/src/engine/tool/task_status.txt +13 -13
  730. package/src/engine/tool/todo.ts +57 -57
  731. package/src/engine/tool/todowrite.txt +167 -167
  732. package/src/engine/tool/tool/apply_patch.ts +313 -313
  733. package/src/engine/tool/tool/apply_patch.txt +33 -33
  734. package/src/engine/tool/tool/edit.ts +711 -711
  735. package/src/engine/tool/tool/edit.txt +10 -10
  736. package/src/engine/tool/tool/external-directory.ts +49 -49
  737. package/src/engine/tool/tool/glob.ts +103 -103
  738. package/src/engine/tool/tool/glob.txt +6 -6
  739. package/src/engine/tool/tool/grep.ts +156 -156
  740. package/src/engine/tool/tool/grep.txt +8 -8
  741. package/src/engine/tool/tool/invalid.ts +21 -21
  742. package/src/engine/tool/tool/json-schema.ts +164 -164
  743. package/src/engine/tool/tool/lsp.ts +113 -113
  744. package/src/engine/tool/tool/lsp.txt +24 -24
  745. package/src/engine/tool/tool/mcp-websearch.ts +96 -96
  746. package/src/engine/tool/tool/plan-enter.txt +14 -14
  747. package/src/engine/tool/tool/plan-exit.txt +13 -13
  748. package/src/engine/tool/tool/plan.ts +78 -78
  749. package/src/engine/tool/tool/question.ts +44 -44
  750. package/src/engine/tool/tool/question.txt +10 -10
  751. package/src/engine/tool/tool/read.ts +337 -337
  752. package/src/engine/tool/tool/read.txt +14 -14
  753. package/src/engine/tool/tool/registry.ts +472 -472
  754. package/src/engine/tool/tool/repo_clone.ts +80 -80
  755. package/src/engine/tool/tool/repo_clone.txt +5 -5
  756. package/src/engine/tool/tool/repo_overview.ts +279 -279
  757. package/src/engine/tool/tool/repo_overview.txt +4 -4
  758. package/src/engine/tool/tool/schema.ts +14 -14
  759. package/src/engine/tool/tool/shell/id.ts +19 -19
  760. package/src/engine/tool/tool/shell/prompt.ts +295 -295
  761. package/src/engine/tool/tool/shell/shell.txt +77 -77
  762. package/src/engine/tool/tool/shell.ts +647 -647
  763. package/src/engine/tool/tool/skill.ts +75 -75
  764. package/src/engine/tool/tool/skill.txt +5 -5
  765. package/src/engine/tool/tool/task.ts +337 -337
  766. package/src/engine/tool/tool/task.txt +58 -58
  767. package/src/engine/tool/tool/task_status.ts +179 -179
  768. package/src/engine/tool/tool/task_status.txt +13 -13
  769. package/src/engine/tool/tool/todo.ts +57 -57
  770. package/src/engine/tool/tool/todowrite.txt +167 -167
  771. package/src/engine/tool/tool/tool.ts +164 -164
  772. package/src/engine/tool/tool/truncate.ts +160 -160
  773. package/src/engine/tool/tool/truncation-dir.ts +4 -4
  774. package/src/engine/tool/tool/webfetch.ts +192 -192
  775. package/src/engine/tool/tool/webfetch.txt +13 -13
  776. package/src/engine/tool/tool/websearch.ts +143 -143
  777. package/src/engine/tool/tool/websearch.txt +14 -14
  778. package/src/engine/tool/tool/write.ts +104 -104
  779. package/src/engine/tool/tool/write.txt +8 -8
  780. package/src/engine/tool/tool.ts +164 -164
  781. package/src/engine/tool/truncate.ts +160 -160
  782. package/src/engine/tool/truncation-dir.ts +4 -4
  783. package/src/engine/tool/webfetch.ts +192 -192
  784. package/src/engine/tool/webfetch.txt +13 -13
  785. package/src/engine/tool/websearch.ts +143 -143
  786. package/src/engine/tool/websearch.txt +14 -14
  787. package/src/engine/tool/write.ts +104 -104
  788. package/src/engine/tool/write.txt +8 -8
  789. package/src/engine/util/archive.ts +17 -17
  790. package/src/engine/util/bom.ts +31 -31
  791. package/src/engine/util/data-url.ts +9 -9
  792. package/src/engine/util/defer.ts +10 -10
  793. package/src/engine/util/effect-http-client.ts +11 -11
  794. package/src/engine/util/error.ts +88 -88
  795. package/src/engine/util/filesystem.ts +252 -252
  796. package/src/engine/util/format.ts +20 -20
  797. package/src/engine/util/iife.ts +3 -3
  798. package/src/engine/util/lazy.ts +20 -20
  799. package/src/engine/util/local-context.ts +25 -25
  800. package/src/engine/util/locale.ts +86 -86
  801. package/src/engine/util/media.ts +26 -26
  802. package/src/engine/util/process.ts +176 -176
  803. package/src/engine/util/queue.ts +32 -32
  804. package/src/engine/util/record.ts +3 -3
  805. package/src/engine/util/repository.ts +158 -158
  806. package/src/engine/util/rpc.ts +66 -66
  807. package/src/engine/util/signal.ts +12 -12
  808. package/src/engine/util/timeout.ts +13 -13
  809. package/src/engine/util/token.ts +7 -7
  810. package/src/engine/util/util/archive.ts +17 -17
  811. package/src/engine/util/util/bom.ts +31 -31
  812. package/src/engine/util/util/data-url.ts +9 -9
  813. package/src/engine/util/util/defer.ts +10 -10
  814. package/src/engine/util/util/effect-http-client.ts +11 -11
  815. package/src/engine/util/util/error.ts +88 -88
  816. package/src/engine/util/util/filesystem.ts +252 -252
  817. package/src/engine/util/util/format.ts +20 -20
  818. package/src/engine/util/util/iife.ts +3 -3
  819. package/src/engine/util/util/lazy.ts +20 -20
  820. package/src/engine/util/util/local-context.ts +25 -25
  821. package/src/engine/util/util/locale.ts +86 -86
  822. package/src/engine/util/util/media.ts +26 -26
  823. package/src/engine/util/util/process.ts +176 -176
  824. package/src/engine/util/util/queue.ts +32 -32
  825. package/src/engine/util/util/record.ts +3 -3
  826. package/src/engine/util/util/repository.ts +158 -158
  827. package/src/engine/util/util/rpc.ts +66 -66
  828. package/src/engine/util/util/signal.ts +12 -12
  829. package/src/engine/util/util/timeout.ts +13 -13
  830. package/src/engine/util/util/token.ts +7 -7
  831. package/src/engine/util/util/which.ts +14 -14
  832. package/src/engine/util/util/wildcard.ts +59 -59
  833. package/src/engine/util/which.ts +14 -14
  834. package/src/engine/util/wildcard.ts +59 -59
  835. package/src/engine/worktree/index.ts +621 -621
  836. package/src/export.ts +122 -0
  837. package/src/mdns.ts +53 -0
  838. package/src/server.ts +151 -156
  839. package/src/stats.ts +290 -0
  840. package/src/tui.ts +964 -480
@@ -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"