provider.ts 33 KB

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