provider.ts 41 KB

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