2025-10-26 19:50:41 +00:00
|
|
|
|
import z from "zod"
|
2025-11-21 08:01:19 +00:00
|
|
|
|
import fuzzysort from "fuzzysort"
|
2025-05-31 20:05:12 +00:00
|
|
|
|
import { Config } from "../config/config"
|
2025-06-13 04:53:46 +00:00
|
|
|
|
import { mergeDeep, sortBy } from "remeda"
|
2025-05-31 20:05:12 +00:00
|
|
|
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
|
|
|
|
|
import { Log } from "../util/log"
|
|
|
|
|
|
import { BunProc } from "../bun"
|
2025-08-14 20:24:46 +00:00
|
|
|
|
import { Plugin } from "../plugin"
|
2025-06-05 18:59:07 +00:00
|
|
|
|
import { ModelsDev } from "./models"
|
2025-11-24 17:56:00 +00:00
|
|
|
|
import { NamedError } from "@opencode-ai/util/error"
|
2025-06-11 04:21:46 +00:00
|
|
|
|
import { Auth } from "../auth"
|
2025-09-01 21:15:49 +00:00
|
|
|
|
import { Instance } from "../project/instance"
|
2025-09-09 07:16:44 +00:00
|
|
|
|
import { Flag } from "../flag/flag"
|
2025-11-12 19:55:13 +00:00
|
|
|
|
import { iife } from "@/util/iife"
|
2025-05-29 15:58:40 +00:00
|
|
|
|
|
2025-11-24 21:48:54 +00:00
|
|
|
|
// Direct imports for bundled providers
|
|
|
|
|
|
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|
|
|
|
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
|
|
|
|
|
import { createAzure } from "@ai-sdk/azure"
|
|
|
|
|
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
|
|
|
|
import { createVertex } from "@ai-sdk/google-vertex"
|
|
|
|
|
|
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
|
|
|
|
|
import { createOpenAI } from "@ai-sdk/openai"
|
|
|
|
|
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
|
|
|
|
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
|
|
|
|
|
|
2025-05-29 15:58:40 +00:00
|
|
|
|
export namespace Provider {
|
2025-05-31 20:05:12 +00:00
|
|
|
|
const log = Log.create({ service: "provider" })
|
|
|
|
|
|
|
2025-11-24 21:48:54 +00:00
|
|
|
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
|
|
|
|
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
|
|
|
|
|
"@ai-sdk/anthropic": createAnthropic,
|
|
|
|
|
|
"@ai-sdk/azure": createAzure,
|
|
|
|
|
|
"@ai-sdk/google": createGoogleGenerativeAI,
|
|
|
|
|
|
"@ai-sdk/google-vertex": createVertex,
|
|
|
|
|
|
"@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
|
|
|
|
|
|
"@ai-sdk/openai": createOpenAI,
|
|
|
|
|
|
"@ai-sdk/openai-compatible": createOpenAICompatible,
|
|
|
|
|
|
"@openrouter/ai-sdk-provider": createOpenRouter,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 08:43:25 +00:00
|
|
|
|
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
2025-06-25 14:01:35 +00:00
|
|
|
|
autoload: boolean
|
2025-10-13 19:16:21 +00:00
|
|
|
|
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
2025-06-25 14:01:35 +00:00
|
|
|
|
options?: Record<string, any>
|
|
|
|
|
|
}>
|
2025-05-31 20:05:12 +00:00
|
|
|
|
|
2025-06-13 04:53:46 +00:00
|
|
|
|
type Source = "env" | "config" | "custom" | "api"
|
2025-06-10 19:43:14 +00:00
|
|
|
|
|
2025-06-13 03:10:03 +00:00
|
|
|
|
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
2025-08-14 20:24:46 +00:00
|
|
|
|
async anthropic() {
|
2025-06-13 03:10:03 +00:00
|
|
|
|
return {
|
2025-08-14 20:24:46 +00:00
|
|
|
|
autoload: false,
|
2025-06-22 23:11:37 +00:00
|
|
|
|
options: {
|
2025-08-14 20:24:46 +00:00
|
|
|
|
headers: {
|
|
|
|
|
|
"anthropic-beta":
|
|
|
|
|
|
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
2025-06-22 23:11:37 +00:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-08-22 21:04:28 +00:00
|
|
|
|
async opencode(input) {
|
2025-09-21 08:43:25 +00:00
|
|
|
|
const hasKey = await (async () => {
|
|
|
|
|
|
if (input.env.some((item) => process.env[item])) return true
|
|
|
|
|
|
if (await Auth.get(input.id)) return true
|
|
|
|
|
|
return false
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasKey) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(input.models)) {
|
|
|
|
|
|
if (value.cost.input === 0) continue
|
|
|
|
|
|
delete input.models[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 05:02:41 +00:00
|
|
|
|
return {
|
2025-08-22 21:04:28 +00:00
|
|
|
|
autoload: Object.keys(input.models).length > 0,
|
2025-11-14 05:59:00 +00:00
|
|
|
|
options: hasKey ? {} : { apiKey: "public" },
|
2025-08-20 05:02:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-06-25 14:01:35 +00:00
|
|
|
|
openai: async () => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
2025-10-13 19:51:12 +00:00
|
|
|
|
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
2025-06-25 14:01:35 +00:00
|
|
|
|
return sdk.responses(modelID)
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-07-31 03:22:59 +00:00
|
|
|
|
azure: async () => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
2025-10-13 19:16:21 +00:00
|
|
|
|
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
|
|
|
if (options?.["useCompletionUrls"]) {
|
2025-10-16 05:59:49 +00:00
|
|
|
|
return sdk.chat(modelID)
|
2025-10-13 19:16:21 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
return sdk.responses(modelID)
|
|
|
|
|
|
}
|
2025-07-31 03:22:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
options: {},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-17 06:01:45 +00:00
|
|
|
|
"azure-cognitive-services": async () => {
|
|
|
|
|
|
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
|
|
|
|
|
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
|
|
|
if (options?.["useCompletionUrls"]) {
|
|
|
|
|
|
return sdk.chat(modelID)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return sdk.responses(modelID)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-06-13 03:39:52 +00:00
|
|
|
|
"amazon-bedrock": async () => {
|
2025-11-08 01:59:02 +00:00
|
|
|
|
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
2025-07-17 14:12:30 +00:00
|
|
|
|
return { autoload: false }
|
2025-06-20 18:57:33 +00:00
|
|
|
|
|
|
|
|
|
|
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
|
|
|
|
|
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
2025-06-13 03:39:52 +00:00
|
|
|
|
return {
|
2025-06-25 14:01:35 +00:00
|
|
|
|
autoload: true,
|
2025-06-20 18:57:33 +00:00
|
|
|
|
options: {
|
|
|
|
|
|
region,
|
|
|
|
|
|
credentialProvider: fromNodeProviderChain(),
|
|
|
|
|
|
},
|
2025-10-13 19:51:12 +00:00
|
|
|
|
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
2025-11-26 00:16:13 +00:00
|
|
|
|
// Skip region prefixing if model already has global prefix
|
|
|
|
|
|
if (modelID.startsWith("global.")) {
|
|
|
|
|
|
return sdk.languageModel(modelID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-30 15:12:30 +00:00
|
|
|
|
let regionPrefix = region.split("-")[0]
|
|
|
|
|
|
|
|
|
|
|
|
switch (regionPrefix) {
|
|
|
|
|
|
case "us": {
|
2025-10-22 16:41:38 +00:00
|
|
|
|
const modelRequiresPrefix = [
|
|
|
|
|
|
"nova-micro",
|
|
|
|
|
|
"nova-lite",
|
|
|
|
|
|
"nova-pro",
|
|
|
|
|
|
"nova-premier",
|
|
|
|
|
|
"claude",
|
2025-10-26 06:21:34 +00:00
|
|
|
|
"deepseek",
|
2025-10-22 16:41:38 +00:00
|
|
|
|
].some((m) => modelID.includes(m))
|
2025-09-08 16:54:06 +00:00
|
|
|
|
const isGovCloud = region.startsWith("us-gov")
|
|
|
|
|
|
if (modelRequiresPrefix && !isGovCloud) {
|
2025-06-30 15:12:30 +00:00
|
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
case "eu": {
|
|
|
|
|
|
const regionRequiresPrefix = [
|
|
|
|
|
|
"eu-west-1",
|
2025-11-05 16:14:08 +00:00
|
|
|
|
"eu-west-2",
|
2025-06-30 15:12:30 +00:00
|
|
|
|
"eu-west-3",
|
|
|
|
|
|
"eu-north-1",
|
|
|
|
|
|
"eu-central-1",
|
|
|
|
|
|
"eu-south-1",
|
|
|
|
|
|
"eu-south-2",
|
|
|
|
|
|
].some((r) => region.includes(r))
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
|
|
|
|
|
modelID.includes(m),
|
|
|
|
|
|
)
|
2025-06-30 15:12:30 +00:00
|
|
|
|
if (regionRequiresPrefix && modelRequiresPrefix) {
|
|
|
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
case "ap": {
|
2025-10-15 16:09:22 +00:00
|
|
|
|
const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
2025-10-22 15:41:08 +00:00
|
|
|
|
if (
|
|
|
|
|
|
isAustraliaRegion &&
|
2025-11-08 01:59:02 +00:00
|
|
|
|
["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
|
2025-10-22 15:41:08 +00:00
|
|
|
|
) {
|
2025-10-17 04:48:41 +00:00
|
|
|
|
regionPrefix = "au"
|
|
|
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
2025-10-15 16:09:22 +00:00
|
|
|
|
} else {
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
|
|
|
|
modelID.includes(m),
|
2025-10-15 16:09:22 +00:00
|
|
|
|
)
|
|
|
|
|
|
if (modelRequiresPrefix) {
|
|
|
|
|
|
regionPrefix = "apac"
|
|
|
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
|
|
|
|
|
}
|
2025-06-30 15:12:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2025-06-20 18:57:33 +00:00
|
|
|
|
}
|
2025-06-30 15:12:30 +00:00
|
|
|
|
|
2025-06-20 18:57:33 +00:00
|
|
|
|
return sdk.languageModel(modelID)
|
|
|
|
|
|
},
|
2025-06-13 03:39:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-07-02 21:08:41 +00:00
|
|
|
|
openrouter: async () => {
|
2025-07-02 18:43:59 +00:00
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"HTTP-Referer": "https://opencode.ai/",
|
|
|
|
|
|
"X-Title": "opencode",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-07-31 03:22:24 +00:00
|
|
|
|
vercel: async () => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"http-referer": "https://opencode.ai/",
|
|
|
|
|
|
"x-title": "opencode",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-10-04 05:10:38 +00:00
|
|
|
|
"google-vertex": async () => {
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
|
|
|
|
|
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
2025-10-04 05:10:38 +00:00
|
|
|
|
const autoload = Boolean(project)
|
|
|
|
|
|
if (!autoload) return { autoload: false }
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: true,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
project,
|
|
|
|
|
|
location,
|
|
|
|
|
|
},
|
|
|
|
|
|
async getModel(sdk: any, modelID: string) {
|
|
|
|
|
|
const id = String(modelID).trim()
|
|
|
|
|
|
return sdk.languageModel(id)
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"google-vertex-anthropic": async () => {
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
2025-11-10 19:25:03 +00:00
|
|
|
|
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
|
2025-10-04 05:10:38 +00:00
|
|
|
|
const autoload = Boolean(project)
|
|
|
|
|
|
if (!autoload) return { autoload: false }
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: true,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
project,
|
|
|
|
|
|
location,
|
|
|
|
|
|
},
|
|
|
|
|
|
async getModel(sdk: any, modelID: string) {
|
|
|
|
|
|
const id = String(modelID).trim()
|
|
|
|
|
|
return sdk.languageModel(id)
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-16 03:37:30 +00:00
|
|
|
|
zenmux: async () => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
autoload: false,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"HTTP-Referer": "https://opencode.ai/",
|
|
|
|
|
|
"X-Title": "opencode",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-06-05 15:50:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-01 21:15:49 +00:00
|
|
|
|
const state = Instance.state(async () => {
|
2025-11-09 01:17:46 +00:00
|
|
|
|
using _ = log.time("state")
|
2025-05-31 20:05:12 +00:00
|
|
|
|
const config = await Config.get()
|
2025-06-13 03:10:03 +00:00
|
|
|
|
const database = await ModelsDev.get()
|
2025-06-05 18:59:07 +00:00
|
|
|
|
|
2025-11-21 07:24:44 +00:00
|
|
|
|
const disabled = new Set(config.disabled_providers ?? [])
|
|
|
|
|
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
|
|
|
|
|
|
|
|
|
|
|
function isProviderAllowed(providerID: string): boolean {
|
|
|
|
|
|
if (enabled && !enabled.has(providerID)) return false
|
|
|
|
|
|
if (disabled.has(providerID)) return false
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 18:59:07 +00:00
|
|
|
|
const providers: {
|
|
|
|
|
|
[providerID: string]: {
|
2025-06-10 19:43:14 +00:00
|
|
|
|
source: Source
|
2025-06-13 03:10:03 +00:00
|
|
|
|
info: ModelsDev.Provider
|
2025-10-13 19:16:21 +00:00
|
|
|
|
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
2025-06-05 18:59:07 +00:00
|
|
|
|
options: Record<string, any>
|
|
|
|
|
|
}
|
|
|
|
|
|
} = {}
|
2025-09-01 21:15:49 +00:00
|
|
|
|
const models = new Map<
|
|
|
|
|
|
string,
|
2025-10-31 15:48:51 +00:00
|
|
|
|
{
|
|
|
|
|
|
providerID: string
|
|
|
|
|
|
modelID: string
|
|
|
|
|
|
info: ModelsDev.Model
|
|
|
|
|
|
language: LanguageModel
|
|
|
|
|
|
npm?: string
|
|
|
|
|
|
}
|
2025-09-01 21:15:49 +00:00
|
|
|
|
>()
|
2025-09-11 20:22:44 +00:00
|
|
|
|
const sdk = new Map<number, SDK>()
|
2025-10-14 00:58:19 +00:00
|
|
|
|
// Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
|
|
|
|
|
|
const realIdByKey = new Map<string, string>()
|
2025-05-31 20:05:12 +00:00
|
|
|
|
|
2025-06-15 17:33:24 +00:00
|
|
|
|
log.info("init")
|
2025-06-05 15:50:54 +00:00
|
|
|
|
|
2025-06-10 19:43:14 +00:00
|
|
|
|
function mergeProvider(
|
|
|
|
|
|
id: string,
|
|
|
|
|
|
options: Record<string, any>,
|
|
|
|
|
|
source: Source,
|
2025-10-13 19:16:21 +00:00
|
|
|
|
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
|
2025-06-10 19:43:14 +00:00
|
|
|
|
) {
|
|
|
|
|
|
const provider = providers[id]
|
|
|
|
|
|
if (!provider) {
|
2025-06-18 18:03:16 +00:00
|
|
|
|
const info = database[id]
|
|
|
|
|
|
if (!info) return
|
2025-07-11 22:01:28 +00:00
|
|
|
|
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
|
2025-06-10 19:43:14 +00:00
|
|
|
|
providers[id] = {
|
|
|
|
|
|
source,
|
2025-06-18 18:03:16 +00:00
|
|
|
|
info,
|
2025-06-10 19:43:14 +00:00
|
|
|
|
options,
|
2025-06-29 23:26:50 +00:00
|
|
|
|
getModel,
|
2025-06-10 19:43:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-06-10 20:18:54 +00:00
|
|
|
|
provider.options = mergeDeep(provider.options, options)
|
2025-06-10 19:43:14 +00:00
|
|
|
|
provider.source = source
|
2025-06-20 18:57:33 +00:00
|
|
|
|
provider.getModel = getModel ?? provider.getModel
|
2025-06-10 19:43:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 16:10:44 +00:00
|
|
|
|
const configProviders = Object.entries(config.provider ?? {})
|
|
|
|
|
|
|
2025-11-04 21:36:12 +00:00
|
|
|
|
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
|
|
|
|
|
|
if (database["github-copilot"]) {
|
|
|
|
|
|
const githubCopilot = database["github-copilot"]
|
|
|
|
|
|
database["github-copilot-enterprise"] = {
|
|
|
|
|
|
...githubCopilot,
|
|
|
|
|
|
id: "github-copilot-enterprise",
|
|
|
|
|
|
name: "GitHub Copilot Enterprise",
|
|
|
|
|
|
// Enterprise uses a different API endpoint - will be set dynamically based on auth
|
|
|
|
|
|
api: undefined,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 16:10:44 +00:00
|
|
|
|
for (const [providerID, provider] of configProviders) {
|
2025-06-13 03:10:03 +00:00
|
|
|
|
const existing = database[providerID]
|
|
|
|
|
|
const parsed: ModelsDev.Provider = {
|
|
|
|
|
|
id: providerID,
|
2025-06-16 19:02:25 +00:00
|
|
|
|
npm: provider.npm ?? existing?.npm,
|
2025-06-13 03:10:03 +00:00
|
|
|
|
name: provider.name ?? existing?.name ?? providerID,
|
|
|
|
|
|
env: provider.env ?? existing?.env ?? [],
|
2025-06-30 23:07:51 +00:00
|
|
|
|
api: provider.api ?? existing?.api,
|
2025-06-13 03:10:03 +00:00
|
|
|
|
models: existing?.models ?? {},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
2025-11-11 21:58:14 +00:00
|
|
|
|
const existing = parsed.models[model.id ?? modelID]
|
2025-11-12 19:55:13 +00:00
|
|
|
|
const name = iife(() => {
|
|
|
|
|
|
if (model.name) return model.name
|
|
|
|
|
|
if (model.id && model.id !== modelID) return modelID
|
|
|
|
|
|
return existing?.name ?? modelID
|
|
|
|
|
|
})
|
2025-06-13 03:10:03 +00:00
|
|
|
|
const parsedModel: ModelsDev.Model = {
|
2025-10-14 00:58:19 +00:00
|
|
|
|
id: modelID,
|
2025-11-12 19:55:13 +00:00
|
|
|
|
name,
|
2025-06-30 20:46:10 +00:00
|
|
|
|
release_date: model.release_date ?? existing?.release_date,
|
2025-06-13 03:10:03 +00:00
|
|
|
|
attachment: model.attachment ?? existing?.attachment ?? false,
|
|
|
|
|
|
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
|
|
|
|
|
temperature: model.temperature ?? existing?.temperature ?? false,
|
2025-06-20 19:03:41 +00:00
|
|
|
|
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
2025-07-18 10:08:35 +00:00
|
|
|
|
cost:
|
|
|
|
|
|
!model.cost && !existing?.cost
|
|
|
|
|
|
? {
|
2025-10-22 15:41:08 +00:00
|
|
|
|
input: 0,
|
|
|
|
|
|
output: 0,
|
|
|
|
|
|
cache_read: 0,
|
|
|
|
|
|
cache_write: 0,
|
|
|
|
|
|
}
|
2025-07-18 10:08:35 +00:00
|
|
|
|
: {
|
2025-10-22 15:41:08 +00:00
|
|
|
|
cache_read: 0,
|
|
|
|
|
|
cache_write: 0,
|
|
|
|
|
|
...existing?.cost,
|
|
|
|
|
|
...model.cost,
|
|
|
|
|
|
},
|
2025-06-20 19:03:41 +00:00
|
|
|
|
options: {
|
|
|
|
|
|
...existing?.options,
|
|
|
|
|
|
...model.options,
|
|
|
|
|
|
},
|
2025-06-13 03:10:03 +00:00
|
|
|
|
limit: model.limit ??
|
|
|
|
|
|
existing?.limit ?? {
|
2025-10-22 15:41:08 +00:00
|
|
|
|
context: 0,
|
|
|
|
|
|
output: 0,
|
|
|
|
|
|
},
|
2025-10-09 14:05:11 +00:00
|
|
|
|
modalities: model.modalities ??
|
|
|
|
|
|
existing?.modalities ?? {
|
2025-10-22 15:41:08 +00:00
|
|
|
|
input: ["text"],
|
|
|
|
|
|
output: ["text"],
|
|
|
|
|
|
},
|
2025-10-31 04:35:26 +00:00
|
|
|
|
headers: model.headers,
|
2025-09-09 19:47:24 +00:00
|
|
|
|
provider: model.provider ?? existing?.provider,
|
2025-06-13 03:10:03 +00:00
|
|
|
|
}
|
2025-10-14 00:58:19 +00:00
|
|
|
|
if (model.id && model.id !== modelID) {
|
|
|
|
|
|
realIdByKey.set(`${providerID}/${modelID}`, model.id)
|
|
|
|
|
|
}
|
2025-06-13 03:10:03 +00:00
|
|
|
|
parsed.models[modelID] = parsedModel
|
|
|
|
|
|
}
|
2025-11-21 07:01:30 +00:00
|
|
|
|
|
2025-06-13 03:10:03 +00:00
|
|
|
|
database[providerID] = parsed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// load env
|
|
|
|
|
|
for (const [providerID, provider] of Object.entries(database)) {
|
2025-06-17 16:23:04 +00:00
|
|
|
|
if (disabled.has(providerID)) continue
|
2025-06-30 23:07:51 +00:00
|
|
|
|
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
|
|
|
|
|
if (!apiKey) continue
|
|
|
|
|
|
mergeProvider(
|
|
|
|
|
|
providerID,
|
|
|
|
|
|
// only include apiKey if there's only one potential option
|
|
|
|
|
|
provider.env.length === 1 ? { apiKey } : {},
|
|
|
|
|
|
"env",
|
|
|
|
|
|
)
|
2025-06-10 19:43:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 04:53:46 +00:00
|
|
|
|
// load apikeys
|
|
|
|
|
|
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
2025-06-17 16:23:04 +00:00
|
|
|
|
if (disabled.has(providerID)) continue
|
2025-06-13 04:53:46 +00:00
|
|
|
|
if (provider.type === "api") {
|
|
|
|
|
|
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 03:10:03 +00:00
|
|
|
|
// load custom
|
|
|
|
|
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
2025-06-17 16:23:04 +00:00
|
|
|
|
if (disabled.has(providerID)) continue
|
2025-06-13 03:10:03 +00:00
|
|
|
|
const result = await fn(database[providerID])
|
2025-06-25 14:01:35 +00:00
|
|
|
|
if (result && (result.autoload || providers[providerID])) {
|
2025-07-07 19:53:43 +00:00
|
|
|
|
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
2025-06-22 23:11:37 +00:00
|
|
|
|
}
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 20:24:46 +00:00
|
|
|
|
for (const plugin of await Plugin.list()) {
|
|
|
|
|
|
if (!plugin.auth) continue
|
|
|
|
|
|
const providerID = plugin.auth.provider
|
|
|
|
|
|
if (disabled.has(providerID)) continue
|
2025-11-04 21:36:12 +00:00
|
|
|
|
|
|
|
|
|
|
// For github-copilot plugin, check if auth exists for either github-copilot or github-copilot-enterprise
|
|
|
|
|
|
let hasAuth = false
|
2025-08-14 20:24:46 +00:00
|
|
|
|
const auth = await Auth.get(providerID)
|
2025-11-04 21:36:12 +00:00
|
|
|
|
if (auth) hasAuth = true
|
|
|
|
|
|
|
|
|
|
|
|
// Special handling for github-copilot: also check for enterprise auth
|
|
|
|
|
|
if (providerID === "github-copilot" && !hasAuth) {
|
|
|
|
|
|
const enterpriseAuth = await Auth.get("github-copilot-enterprise")
|
|
|
|
|
|
if (enterpriseAuth) hasAuth = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasAuth) continue
|
2025-08-14 20:24:46 +00:00
|
|
|
|
if (!plugin.auth.loader) continue
|
2025-11-04 21:36:12 +00:00
|
|
|
|
|
|
|
|
|
|
// Load for the main provider if auth exists
|
|
|
|
|
|
if (auth) {
|
2025-11-08 01:59:02 +00:00
|
|
|
|
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
2025-11-04 21:36:12 +00:00
|
|
|
|
mergeProvider(plugin.auth.provider, options ?? {}, "custom")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
|
|
|
|
|
|
if (providerID === "github-copilot") {
|
|
|
|
|
|
const enterpriseProviderID = "github-copilot-enterprise"
|
|
|
|
|
|
if (!disabled.has(enterpriseProviderID)) {
|
|
|
|
|
|
const enterpriseAuth = await Auth.get(enterpriseProviderID)
|
|
|
|
|
|
if (enterpriseAuth) {
|
|
|
|
|
|
const enterpriseOptions = await plugin.auth.loader(
|
|
|
|
|
|
() => Auth.get(enterpriseProviderID) as any,
|
|
|
|
|
|
database[enterpriseProviderID],
|
|
|
|
|
|
)
|
|
|
|
|
|
mergeProvider(enterpriseProviderID, enterpriseOptions ?? {}, "custom")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-14 20:24:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 03:10:03 +00:00
|
|
|
|
// load config
|
2025-06-18 15:06:16 +00:00
|
|
|
|
for (const [providerID, provider] of configProviders) {
|
2025-06-13 03:10:03 +00:00
|
|
|
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-20 04:57:28 +00:00
|
|
|
|
for (const [providerID, provider] of Object.entries(providers)) {
|
2025-11-21 07:24:44 +00:00
|
|
|
|
if (!isProviderAllowed(providerID)) {
|
|
|
|
|
|
delete providers[providerID]
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 07:01:30 +00:00
|
|
|
|
const configProvider = config.provider?.[providerID]
|
2025-08-18 21:50:38 +00:00
|
|
|
|
const filteredModels = Object.fromEntries(
|
2025-09-09 07:16:44 +00:00
|
|
|
|
Object.entries(provider.info.models)
|
|
|
|
|
|
// Filter out blacklisted models
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
([modelID]) =>
|
2025-11-08 01:59:02 +00:00
|
|
|
|
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
2025-09-09 07:16:44 +00:00
|
|
|
|
)
|
|
|
|
|
|
// Filter out experimental models
|
2025-10-16 00:12:37 +00:00
|
|
|
|
.filter(
|
|
|
|
|
|
([, model]) =>
|
2025-11-08 01:59:02 +00:00
|
|
|
|
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
2025-10-31 15:48:51 +00:00
|
|
|
|
model.status !== "deprecated",
|
2025-11-21 07:01:30 +00:00
|
|
|
|
)
|
|
|
|
|
|
// Filter by provider's whitelist/blacklist from config
|
|
|
|
|
|
.filter(([modelID]) => {
|
|
|
|
|
|
if (!configProvider) return true
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
(!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
|
|
|
|
|
|
(!configProvider.whitelist || configProvider.whitelist.includes(modelID))
|
|
|
|
|
|
)
|
|
|
|
|
|
}),
|
2025-08-18 21:50:38 +00:00
|
|
|
|
)
|
2025-11-21 07:01:30 +00:00
|
|
|
|
|
2025-08-18 21:50:38 +00:00
|
|
|
|
provider.info.models = filteredModels
|
|
|
|
|
|
|
2025-06-20 04:57:28 +00:00
|
|
|
|
if (Object.keys(provider.info.models).length === 0) {
|
|
|
|
|
|
delete providers[providerID]
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2025-11-20 22:32:44 +00:00
|
|
|
|
|
|
|
|
|
|
log.info("found", { providerID, npm: provider.info.npm })
|
2025-06-09 18:01:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-31 20:05:12 +00:00
|
|
|
|
return {
|
|
|
|
|
|
models,
|
|
|
|
|
|
providers,
|
|
|
|
|
|
sdk,
|
2025-10-14 00:58:19 +00:00
|
|
|
|
realIdByKey,
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-06-10 19:43:14 +00:00
|
|
|
|
export async function list() {
|
|
|
|
|
|
return state().then((state) => state.providers)
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 19:47:24 +00:00
|
|
|
|
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
|
2025-06-09 18:01:11 +00:00
|
|
|
|
return (async () => {
|
2025-06-15 17:33:24 +00:00
|
|
|
|
using _ = log.time("getSDK", {
|
2025-06-16 19:02:25 +00:00
|
|
|
|
providerID: provider.id,
|
2025-06-15 17:33:24 +00:00
|
|
|
|
})
|
2025-06-09 18:01:11 +00:00
|
|
|
|
const s = await state()
|
2025-09-09 19:47:24 +00:00
|
|
|
|
const pkg = model.provider?.npm ?? provider.npm ?? provider.id
|
2025-09-11 20:22:44 +00:00
|
|
|
|
const options = { ...s.providers[provider.id]?.options }
|
2025-09-26 02:06:58 +00:00
|
|
|
|
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
|
|
|
|
|
options["includeUsage"] = true
|
|
|
|
|
|
}
|
2025-11-19 20:22:51 +00:00
|
|
|
|
|
2025-09-11 20:22:44 +00:00
|
|
|
|
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
|
|
|
|
|
const existing = s.sdk.get(key)
|
|
|
|
|
|
if (existing) return existing
|
2025-11-14 22:44:36 +00:00
|
|
|
|
|
2025-11-19 22:20:10 +00:00
|
|
|
|
const customFetch = options["fetch"]
|
|
|
|
|
|
|
|
|
|
|
|
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
2025-11-04 21:36:12 +00:00
|
|
|
|
// Preserve custom fetch if it exists, wrap it with timeout logic
|
2025-11-19 22:20:10 +00:00
|
|
|
|
const fetchFn = customFetch ?? fetch
|
|
|
|
|
|
const opts = init ?? {}
|
2025-10-16 22:47:41 +00:00
|
|
|
|
|
2025-11-19 22:20:10 +00:00
|
|
|
|
if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
2025-10-16 22:47:41 +00:00
|
|
|
|
const signals: AbortSignal[] = []
|
2025-11-19 22:20:10 +00:00
|
|
|
|
if (opts.signal) signals.push(opts.signal)
|
2025-10-25 17:08:27 +00:00
|
|
|
|
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
|
2025-10-16 22:47:41 +00:00
|
|
|
|
|
|
|
|
|
|
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
|
|
|
|
|
2025-11-19 22:20:10 +00:00
|
|
|
|
opts.signal = combined
|
2025-08-31 18:06:02 +00:00
|
|
|
|
}
|
2025-11-19 22:20:10 +00:00
|
|
|
|
|
|
|
|
|
|
return fetchFn(input, {
|
|
|
|
|
|
...opts,
|
|
|
|
|
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
|
|
|
|
timeout: false,
|
|
|
|
|
|
})
|
2025-08-31 18:06:02 +00:00
|
|
|
|
}
|
2025-11-24 21:48:54 +00:00
|
|
|
|
|
|
|
|
|
|
// Special case: google-vertex-anthropic uses a subpath import
|
|
|
|
|
|
const bundledKey = provider.id === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : pkg
|
|
|
|
|
|
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
|
|
|
|
|
|
if (bundledFn) {
|
|
|
|
|
|
log.info("using bundled provider", { providerID: provider.id, pkg: bundledKey })
|
|
|
|
|
|
const loaded = bundledFn({
|
|
|
|
|
|
name: provider.id,
|
|
|
|
|
|
...options,
|
|
|
|
|
|
})
|
|
|
|
|
|
s.sdk.set(key, loaded)
|
|
|
|
|
|
return loaded as SDK
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let installedPath: string
|
|
|
|
|
|
if (!pkg.startsWith("file://")) {
|
|
|
|
|
|
installedPath = await BunProc.install(pkg, "latest")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.info("loading local provider", { pkg })
|
|
|
|
|
|
installedPath = pkg
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const mod = await import(installedPath)
|
|
|
|
|
|
|
2025-09-11 20:22:44 +00:00
|
|
|
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
2025-07-19 16:40:28 +00:00
|
|
|
|
const loaded = fn({
|
|
|
|
|
|
name: provider.id,
|
2025-08-31 18:06:02 +00:00
|
|
|
|
...options,
|
2025-07-19 16:40:28 +00:00
|
|
|
|
})
|
2025-09-11 20:22:44 +00:00
|
|
|
|
s.sdk.set(key, loaded)
|
2025-06-09 18:01:11 +00:00
|
|
|
|
return loaded as SDK
|
|
|
|
|
|
})().catch((e) => {
|
2025-06-16 19:02:25 +00:00
|
|
|
|
throw new InitError({ providerID: provider.id }, { cause: e })
|
2025-06-09 18:01:11 +00:00
|
|
|
|
})
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
2025-08-02 22:50:19 +00:00
|
|
|
|
|
|
|
|
|
|
export async function getProvider(providerID: string) {
|
|
|
|
|
|
return state().then((s) => s.providers[providerID])
|
|
|
|
|
|
}
|
2025-05-31 20:05:12 +00:00
|
|
|
|
|
|
|
|
|
|
export async function getModel(providerID: string, modelID: string) {
|
|
|
|
|
|
const key = `${providerID}/${modelID}`
|
|
|
|
|
|
const s = await state()
|
|
|
|
|
|
if (s.models.has(key)) return s.models.get(key)!
|
|
|
|
|
|
|
2025-06-15 17:33:24 +00:00
|
|
|
|
log.info("getModel", {
|
2025-05-31 20:05:12 +00:00
|
|
|
|
providerID,
|
|
|
|
|
|
modelID,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-06-05 18:59:07 +00:00
|
|
|
|
const provider = s.providers[providerID]
|
2025-11-21 08:01:19 +00:00
|
|
|
|
if (!provider) {
|
|
|
|
|
|
const availableProviders = Object.keys(s.providers)
|
|
|
|
|
|
const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 })
|
|
|
|
|
|
const suggestions = matches.map((m) => m.target)
|
|
|
|
|
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-05 18:59:07 +00:00
|
|
|
|
const info = provider.info.models[modelID]
|
2025-11-21 08:01:19 +00:00
|
|
|
|
if (!info) {
|
|
|
|
|
|
const availableModels = Object.keys(provider.info.models)
|
|
|
|
|
|
const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
|
|
|
|
|
|
const suggestions = matches.map((m) => m.target)
|
|
|
|
|
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 19:47:24 +00:00
|
|
|
|
const sdk = await getSDK(provider.info, info)
|
2025-05-31 20:05:12 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-14 00:58:19 +00:00
|
|
|
|
const keyReal = `${providerID}/${modelID}`
|
|
|
|
|
|
const realID = s.realIdByKey.get(keyReal) ?? info.id
|
2025-10-13 19:16:21 +00:00
|
|
|
|
const language = provider.getModel
|
2025-10-14 00:58:19 +00:00
|
|
|
|
? await provider.getModel(sdk, realID, provider.options)
|
|
|
|
|
|
: sdk.languageModel(realID)
|
2025-05-31 20:05:12 +00:00
|
|
|
|
log.info("found", { providerID, modelID })
|
|
|
|
|
|
s.models.set(key, {
|
2025-09-01 21:15:49 +00:00
|
|
|
|
providerID,
|
|
|
|
|
|
modelID,
|
2025-05-31 20:05:12 +00:00
|
|
|
|
info,
|
|
|
|
|
|
language,
|
2025-09-16 07:10:17 +00:00
|
|
|
|
npm: info.provider?.npm ?? provider.info.npm,
|
2025-05-31 20:05:12 +00:00
|
|
|
|
})
|
|
|
|
|
|
return {
|
2025-09-01 21:15:49 +00:00
|
|
|
|
modelID,
|
|
|
|
|
|
providerID,
|
2025-05-31 20:05:12 +00:00
|
|
|
|
info,
|
|
|
|
|
|
language,
|
2025-09-16 07:10:17 +00:00
|
|
|
|
npm: info.provider?.npm ?? provider.info.npm,
|
2025-05-31 20:05:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2025-06-09 18:01:11 +00:00
|
|
|
|
if (e instanceof NoSuchModelError)
|
|
|
|
|
|
throw new ModelNotFoundError(
|
|
|
|
|
|
{
|
|
|
|
|
|
modelID: modelID,
|
|
|
|
|
|
providerID,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ cause: e },
|
|
|
|
|
|
)
|
2025-05-31 20:05:12 +00:00
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-29 02:13:07 +00:00
|
|
|
|
export async function closest(providerID: string, query: string[]) {
|
|
|
|
|
|
const s = await state()
|
|
|
|
|
|
const provider = s.providers[providerID]
|
|
|
|
|
|
if (!provider) return undefined
|
|
|
|
|
|
for (const item of query) {
|
|
|
|
|
|
for (const modelID of Object.keys(provider.info.models)) {
|
|
|
|
|
|
if (modelID.includes(item))
|
|
|
|
|
|
return {
|
|
|
|
|
|
providerID,
|
|
|
|
|
|
modelID,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 18:00:39 +00:00
|
|
|
|
export async function getSmallModel(providerID: string) {
|
2025-07-18 18:16:50 +00:00
|
|
|
|
const cfg = await Config.get()
|
|
|
|
|
|
|
|
|
|
|
|
if (cfg.small_model) {
|
|
|
|
|
|
const parsed = parseModel(cfg.small_model)
|
|
|
|
|
|
return getModel(parsed.providerID, parsed.modelID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-15 18:00:39 +00:00
|
|
|
|
const provider = await state().then((state) => state.providers[providerID])
|
2025-11-22 16:32:49 +00:00
|
|
|
|
if (provider) {
|
|
|
|
|
|
let priority = [
|
|
|
|
|
|
"claude-haiku-4-5",
|
|
|
|
|
|
"claude-haiku-4.5",
|
|
|
|
|
|
"3-5-haiku",
|
|
|
|
|
|
"3.5-haiku",
|
|
|
|
|
|
"gemini-2.5-flash",
|
|
|
|
|
|
"gpt-5-nano",
|
|
|
|
|
|
]
|
|
|
|
|
|
// claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen
|
|
|
|
|
|
if (providerID === "github-copilot") {
|
|
|
|
|
|
priority = priority.filter((m) => m !== "claude-haiku-4.5")
|
|
|
|
|
|
}
|
2025-11-23 20:21:47 +00:00
|
|
|
|
if (providerID.startsWith("opencode")) {
|
2025-11-22 16:32:49 +00:00
|
|
|
|
priority = ["gpt-5-nano"]
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const item of priority) {
|
|
|
|
|
|
for (const model of Object.keys(provider.info.models)) {
|
|
|
|
|
|
if (model.includes(item)) return getModel(providerID, model)
|
|
|
|
|
|
}
|
2025-07-15 18:00:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 04:51:15 +00:00
|
|
|
|
|
|
|
|
|
|
// Check if opencode provider is available before using it
|
|
|
|
|
|
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
|
|
|
|
|
if (opencodeProvider && opencodeProvider.info.models["gpt-5-nano"]) {
|
|
|
|
|
|
return getModel("opencode", "gpt-5-nano")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return undefined
|
2025-07-15 18:00:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 17:01:41 +00:00
|
|
|
|
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
2025-06-13 03:10:03 +00:00
|
|
|
|
export function sort(models: ModelsDev.Model[]) {
|
2025-06-05 18:59:07 +00:00
|
|
|
|
return sortBy(
|
|
|
|
|
|
models,
|
2025-07-07 19:53:43 +00:00
|
|
|
|
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
2025-06-05 18:59:07 +00:00
|
|
|
|
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
|
|
|
|
|
[(model) => model.id, "desc"],
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 16:38:48 +00:00
|
|
|
|
export async function defaultModel() {
|
2025-06-19 02:20:03 +00:00
|
|
|
|
const cfg = await Config.get()
|
2025-06-19 16:29:34 +00:00
|
|
|
|
if (cfg.model) return parseModel(cfg.model)
|
2025-09-05 22:25:25 +00:00
|
|
|
|
|
2025-06-17 15:27:07 +00:00
|
|
|
|
const provider = await list()
|
|
|
|
|
|
.then((val) => Object.values(val))
|
2025-07-07 19:53:43 +00:00
|
|
|
|
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
2025-06-03 16:38:48 +00:00
|
|
|
|
if (!provider) throw new Error("no providers found")
|
2025-06-10 19:43:14 +00:00
|
|
|
|
const [model] = sort(Object.values(provider.info.models))
|
2025-06-03 16:38:48 +00:00
|
|
|
|
if (!model) throw new Error("no models found")
|
|
|
|
|
|
return {
|
2025-06-10 19:43:14 +00:00
|
|
|
|
providerID: provider.info.id,
|
2025-06-03 16:38:48 +00:00
|
|
|
|
modelID: model.id,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-19 16:29:34 +00:00
|
|
|
|
export function parseModel(model: string) {
|
|
|
|
|
|
const [providerID, ...rest] = model.split("/")
|
|
|
|
|
|
return {
|
|
|
|
|
|
providerID: providerID,
|
|
|
|
|
|
modelID: rest.join("/"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 18:01:11 +00:00
|
|
|
|
export const ModelNotFoundError = NamedError.create(
|
|
|
|
|
|
"ProviderModelNotFoundError",
|
|
|
|
|
|
z.object({
|
|
|
|
|
|
providerID: z.string(),
|
|
|
|
|
|
modelID: z.string(),
|
2025-11-21 08:01:19 +00:00
|
|
|
|
suggestions: z.array(z.string()).optional(),
|
2025-06-09 18:01:11 +00:00
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
export const InitError = NamedError.create(
|
|
|
|
|
|
"ProviderInitError",
|
|
|
|
|
|
z.object({
|
|
|
|
|
|
providerID: z.string(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2025-05-29 15:58:40 +00:00
|
|
|
|
}
|