summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/desktop/src/components/prompt-input.tsx4
-rw-r--r--packages/opencode/src/agent/agent.ts3
-rw-r--r--packages/opencode/src/cli/cmd/models.ts2
-rw-r--r--packages/opencode/src/config/config.ts74
-rw-r--r--packages/opencode/src/provider/models.ts78
-rw-r--r--packages/opencode/src/provider/provider.ts509
-rw-r--r--packages/opencode/src/provider/transform.ts62
-rw-r--r--packages/opencode/src/server/server.ts17
-rw-r--r--packages/opencode/src/session/compaction.ts129
-rw-r--r--packages/opencode/src/session/index.ts20
-rw-r--r--packages/opencode/src/session/processor.ts25
-rw-r--r--packages/opencode/src/session/prompt.ts277
-rw-r--r--packages/opencode/src/session/summary.ts21
-rw-r--r--packages/opencode/src/session/system.ts14
-rw-r--r--packages/opencode/src/share/share-next.ts7
-rw-r--r--packages/opencode/src/tool/batch.ts2
-rw-r--r--packages/opencode/src/tool/read.ts2
-rw-r--r--packages/opencode/src/tool/registry.ts8
-rw-r--r--packages/opencode/test/provider/provider.test.ts102
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts254
20 files changed, 891 insertions, 719 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 976924223..a311ae763 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -456,9 +456,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
- <Show when={i.release_date}>
+ <Show when={false}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
- {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
+ {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index b901b95c2..0e7a7c5d3 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -224,6 +224,7 @@ export namespace Agent {
export async function generate(input: { description: string }) {
const defaultModel = await Provider.defaultModel()
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
+ const language = await Provider.getLanguage(model)
const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE)
const existing = await list()
@@ -241,7 +242,7 @@ export namespace Agent {
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
},
],
- model: model.language,
+ model: language,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts
index 1ae4ae12c..156dae91c 100644
--- a/packages/opencode/src/cli/cmd/models.ts
+++ b/packages/opencode/src/cli/cmd/models.ts
@@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
function printModels(providerID: string, verbose?: boolean) {
const provider = providers[providerID]
- const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
+ const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
for (const [modelID, model] of sortedModels) {
process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL)
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 2bdbbca5b..2c691cedb 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -470,6 +470,42 @@ export namespace Config {
})
export type Layout = z.infer<typeof Layout>
+ export const Provider = ModelsDev.Provider.partial()
+ .extend({
+ whitelist: z.array(z.string()).optional(),
+ blacklist: z.array(z.string()).optional(),
+ models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
+ options: z
+ .object({
+ apiKey: z.string().optional(),
+ baseURL: z.string().optional(),
+ enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
+ setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
+ timeout: z
+ .union([
+ z
+ .number()
+ .int()
+ .positive()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ z.literal(false).describe("Disable timeout for this provider entirely."),
+ ])
+ .optional()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ })
+ .catchall(z.any())
+ .optional(),
+ })
+ .strict()
+ .meta({
+ ref: "ProviderConfig",
+ })
+ export type Provider = z.infer<typeof Provider>
+
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -536,43 +572,7 @@ export namespace Config {
.optional()
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
provider: z
- .record(
- z.string(),
- ModelsDev.Provider.partial()
- .extend({
- whitelist: z.array(z.string()).optional(),
- blacklist: z.array(z.string()).optional(),
- models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
- options: z
- .object({
- apiKey: z.string().optional(),
- baseURL: z.string().optional(),
- enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
- setCacheKey: z
- .boolean()
- .optional()
- .describe("Enable promptCacheKey for this provider (default false)"),
- timeout: z
- .union([
- z
- .number()
- .int()
- .positive()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- z.literal(false).describe("Disable timeout for this provider entirely."),
- ])
- .optional()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- })
- .catchall(z.any())
- .optional(),
- })
- .strict(),
- )
+ .record(z.string(), Provider)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts
index 676837e15..3d28787c8 100644
--- a/packages/opencode/src/provider/models.ts
+++ b/packages/opencode/src/provider/models.ts
@@ -9,16 +9,16 @@ export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
- export const Model = z
- .object({
- id: z.string(),
- name: z.string(),
- release_date: z.string(),
- attachment: z.boolean(),
- reasoning: z.boolean(),
- temperature: z.boolean(),
- tool_call: z.boolean(),
- cost: z.object({
+ export const Model = z.object({
+ id: z.string(),
+ name: z.string(),
+ release_date: z.string(),
+ attachment: z.boolean(),
+ reasoning: z.boolean(),
+ temperature: z.boolean(),
+ tool_call: z.boolean(),
+ cost: z
+ .object({
input: z.number(),
output: z.number(),
cache_read: z.number().optional(),
@@ -31,40 +31,34 @@ export namespace ModelsDev {
cache_write: z.number().optional(),
})
.optional(),
- }),
- limit: z.object({
- context: z.number(),
- output: z.number(),
- }),
- modalities: z
- .object({
- input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- })
- .optional(),
- experimental: z.boolean().optional(),
- status: z.enum(["alpha", "beta", "deprecated"]).optional(),
- options: z.record(z.string(), z.any()),
- headers: z.record(z.string(), z.string()).optional(),
- provider: z.object({ npm: z.string() }).optional(),
- })
- .meta({
- ref: "Model",
- })
+ })
+ .optional(),
+ limit: z.object({
+ context: z.number(),
+ output: z.number(),
+ }),
+ modalities: z
+ .object({
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ })
+ .optional(),
+ experimental: z.boolean().optional(),
+ status: z.enum(["alpha", "beta", "deprecated"]).optional(),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()).optional(),
+ provider: z.object({ npm: z.string() }).optional(),
+ })
export type Model = z.infer<typeof Model>
- export const Provider = z
- .object({
- api: z.string().optional(),
- name: z.string(),
- env: z.array(z.string()),
- id: z.string(),
- npm: z.string().optional(),
- models: z.record(z.string(), Model),
- })
- .meta({
- ref: "Provider",
- })
+ export const Provider = z.object({
+ api: z.string().optional(),
+ name: z.string(),
+ env: z.array(z.string()),
+ id: z.string(),
+ npm: z.string().optional(),
+ models: z.record(z.string(), Model),
+ })
export type Provider = z.infer<typeof Provider>
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 1123e6bbe..2df4bc96b 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -1,8 +1,8 @@
import z from "zod"
import fuzzysort from "fuzzysort"
import { Config } from "../config/config"
-import { mergeDeep, sortBy } from "remeda"
-import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
+import { mapValues, mergeDeep, sortBy } from "remeda"
+import { NoSuchModelError, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { Plugin } from "../plugin"
@@ -23,7 +23,7 @@ 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"
+import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
export namespace Provider {
@@ -43,14 +43,13 @@ export namespace Provider {
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}
- type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
+ type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+ type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
- getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+ getModel?: CustomModelLoader
options?: Record<string, any>
}>
- type Source = "env" | "config" | "custom" | "api"
-
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic() {
return {
@@ -280,7 +279,7 @@ export namespace Provider {
project,
location,
},
- async getModel(sdk: any, modelID: string) {
+ async getModel(sdk, modelID) {
const id = String(modelID).trim()
return sdk.languageModel(id)
},
@@ -299,10 +298,155 @@ export namespace Provider {
},
}
+ export const Model = z
+ .object({
+ id: z.string(),
+ providerID: z.string(),
+ api: z.object({
+ id: z.string(),
+ url: z.string(),
+ npm: z.string(),
+ }),
+ name: z.string(),
+ capabilities: z.object({
+ temperature: z.boolean(),
+ reasoning: z.boolean(),
+ attachment: z.boolean(),
+ toolcall: z.boolean(),
+ input: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
+ }),
+ output: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
+ }),
+ }),
+ cost: z.object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
+ }),
+ experimentalOver200K: z
+ .object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
+ }),
+ })
+ .optional(),
+ }),
+ limit: z.object({
+ context: z.number(),
+ output: z.number(),
+ }),
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()),
+ })
+ .meta({
+ ref: "Model",
+ })
+ export type Model = z.infer<typeof Model>
+
+ export const Info = z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ source: z.enum(["env", "config", "custom", "api"]),
+ env: z.string().array(),
+ key: z.string().optional(),
+ options: z.record(z.string(), z.any()),
+ models: z.record(z.string(), Model),
+ })
+ .meta({
+ ref: "Provider",
+ })
+ export type Info = z.infer<typeof Info>
+
+ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
+ return {
+ id: model.id,
+ providerID: provider.id,
+ name: model.name,
+ api: {
+ id: model.id,
+ url: provider.api!,
+ npm: model.provider?.npm ?? provider.npm ?? provider.id,
+ },
+ status: model.status ?? "active",
+ headers: model.headers ?? {},
+ options: model.options ?? {},
+ cost: {
+ input: model.cost?.input ?? 0,
+ output: model.cost?.output ?? 0,
+ cache: {
+ read: model.cost?.cache_read ?? 0,
+ write: model.cost?.cache_write ?? 0,
+ },
+ experimentalOver200K: model.cost?.context_over_200k
+ ? {
+ cache: {
+ read: model.cost.context_over_200k.cache_read ?? 0,
+ write: model.cost.context_over_200k.cache_write ?? 0,
+ },
+ input: model.cost.context_over_200k.input,
+ output: model.cost.context_over_200k.output,
+ }
+ : undefined,
+ },
+ limit: {
+ context: model.limit.context,
+ output: model.limit.output,
+ },
+ capabilities: {
+ temperature: model.temperature,
+ reasoning: model.reasoning,
+ attachment: model.attachment,
+ toolcall: model.tool_call,
+ input: {
+ text: model.modalities?.input?.includes("text") ?? false,
+ audio: model.modalities?.input?.includes("audio") ?? false,
+ image: model.modalities?.input?.includes("image") ?? false,
+ video: model.modalities?.input?.includes("video") ?? false,
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
+ },
+ output: {
+ text: model.modalities?.output?.includes("text") ?? false,
+ audio: model.modalities?.output?.includes("audio") ?? false,
+ image: model.modalities?.output?.includes("image") ?? false,
+ video: model.modalities?.output?.includes("video") ?? false,
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
+ },
+ },
+ }
+ }
+
+ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+ return {
+ id: provider.id,
+ source: "custom",
+ name: provider.name,
+ env: provider.env ?? [],
+ options: {},
+ models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
+ }
+ }
+
const state = Instance.state(async () => {
using _ = log.time("state")
const config = await Config.get()
- const database = await ModelsDev.get()
+ const database = mapValues(await ModelsDev.get(), fromModelsDevProvider)
const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
@@ -313,54 +457,15 @@ export namespace Provider {
return true
}
- const providers: {
- [providerID: string]: {
- source: Source
- info: ModelsDev.Provider
- getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
- options: Record<string, any>
- }
+ const providers: { [providerID: string]: Info } = {}
+ const languages = new Map<string, LanguageModelV2>()
+ const modelLoaders: {
+ [providerID: string]: CustomModelLoader
} = {}
- const models = new Map<
- string,
- {
- providerID: string
- modelID: string
- info: ModelsDev.Model
- language: LanguageModel
- npm?: string
- }
- >()
const sdk = new Map<number, SDK>()
- // Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
- const realIdByKey = new Map<string, string>()
log.info("init")
- function mergeProvider(
- id: string,
- options: Record<string, any>,
- source: Source,
- getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
- ) {
- const provider = providers[id]
- if (!provider) {
- const info = database[id]
- if (!info) return
- if (info.api && !options["baseURL"]) options["baseURL"] = info.api
- providers[id] = {
- source,
- info,
- options,
- getModel,
- }
- return
- }
- provider.options = mergeDeep(provider.options, options)
- provider.source = source
- provider.getModel = getModel ?? provider.getModel
- }
-
const configProviders = Object.entries(config.provider ?? {})
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
@@ -370,19 +475,31 @@ export namespace Provider {
...githubCopilot,
id: "github-copilot-enterprise",
name: "GitHub Copilot Enterprise",
- // Enterprise uses a different API endpoint - will be set dynamically based on auth
- api: undefined,
}
}
+ function mergeProvider(providerID: string, provider: Partial<Info>) {
+ const existing = providers[providerID]
+ if (existing) {
+ // @ts-expect-error
+ providers[providerID] = mergeDeep(existing, provider)
+ return
+ }
+ const match = database[providerID]
+ if (!match) return
+ // @ts-expect-error
+ providers[providerID] = mergeDeep(match, provider)
+ }
+
+ // extend database from config
for (const [providerID, provider] of configProviders) {
const existing = database[providerID]
- const parsed: ModelsDev.Provider = {
+ const parsed: Info = {
id: providerID,
- npm: provider.npm ?? existing?.npm,
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
- api: provider.api ?? existing?.api,
+ options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
+ source: "config",
models: existing?.models ?? {},
}
@@ -393,51 +510,53 @@ export namespace Provider {
if (model.id && model.id !== modelID) return modelID
return existing?.name ?? modelID
})
- const parsedModel: ModelsDev.Model = {
+ const parsedModel: Model = {
id: modelID,
- name,
- release_date: model.release_date ?? existing?.release_date,
- attachment: model.attachment ?? existing?.attachment ?? false,
- reasoning: model.reasoning ?? existing?.reasoning ?? false,
- temperature: model.temperature ?? existing?.temperature ?? false,
- tool_call: model.tool_call ?? existing?.tool_call ?? true,
- cost:
- !model.cost && !existing?.cost
- ? {
- input: 0,
- output: 0,
- cache_read: 0,
- cache_write: 0,
- }
- : {
- cache_read: 0,
- cache_write: 0,
- ...existing?.cost,
- ...model.cost,
- },
- options: {
- ...existing?.options,
- ...model.options,
+ api: {
+ id: model.id ?? existing?.api.id ?? modelID,
+ npm: model.provider?.npm ?? provider.npm ?? existing?.api.npm ?? providerID,
+ url: provider?.api ?? existing?.api.url,
},
- limit: model.limit ??
- existing?.limit ?? {
- context: 0,
- output: 0,
+ status: model.status ?? existing?.status ?? "active",
+ name,
+ providerID,
+ capabilities: {
+ temperature: model.temperature ?? existing?.capabilities.temperature ?? false,
+ reasoning: model.reasoning ?? existing?.capabilities.reasoning ?? false,
+ attachment: model.attachment ?? existing?.capabilities.attachment ?? false,
+ toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true,
+ input: {
+ text: model.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true,
+ audio: model.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false,
+ image: model.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false,
+ video: model.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false,
+ pdf: model.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.pdf ?? false,
},
- modalities: model.modalities ??
- existing?.modalities ?? {
- input: ["text"],
- output: ["text"],
+ output: {
+ text: model.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true,
+ audio: model.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false,
+ image: model.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false,
+ video: model.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false,
+ pdf: model.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false,
},
- headers: model.headers,
- provider: model.provider ?? existing?.provider,
- }
- if (model.id && model.id !== modelID) {
- realIdByKey.set(`${providerID}/${modelID}`, model.id)
+ },
+ cost: {
+ input: model?.cost?.input ?? existing?.cost?.input ?? 0,
+ output: model?.cost?.output ?? existing?.cost?.output ?? 0,
+ cache: {
+ read: model?.cost?.cache_read ?? existing?.cost?.cache.read ?? 0,
+ write: model?.cost?.cache_write ?? existing?.cost?.cache.write ?? 0,
+ },
+ },
+ options: mergeDeep(existing?.options ?? {}, model.options ?? {}),
+ limit: {
+ context: model.limit?.context ?? existing?.limit?.context ?? 0,
+ output: model.limit?.output ?? existing?.limit?.output ?? 0,
+ },
+ headers: mergeDeep(existing?.headers ?? {}, model.headers ?? {}),
}
parsed.models[modelID] = parsedModel
}
-
database[providerID] = parsed
}
@@ -447,19 +566,20 @@ export namespace Provider {
if (disabled.has(providerID)) continue
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
if (!apiKey) continue
- mergeProvider(
- providerID,
- // only include apiKey if there's only one potential option
- provider.env.length === 1 ? { apiKey } : {},
- "env",
- )
+ mergeProvider(providerID, {
+ source: "env",
+ key: provider.env.length === 1 ? apiKey : undefined,
+ })
}
// load apikeys
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (disabled.has(providerID)) continue
if (provider.type === "api") {
- mergeProvider(providerID, { apiKey: provider.key }, "api")
+ mergeProvider(providerID, {
+ source: "api",
+ key: provider.key,
+ })
}
}
@@ -485,7 +605,10 @@ export namespace Provider {
// Load for the main provider if auth exists
if (auth) {
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
- mergeProvider(plugin.auth.provider, options ?? {}, "custom")
+ mergeProvider(plugin.auth.provider, {
+ source: "custom",
+ options: options,
+ })
}
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
@@ -498,7 +621,10 @@ export namespace Provider {
() => Auth.get(enterpriseProviderID) as any,
database[enterpriseProviderID],
)
- mergeProvider(enterpriseProviderID, enterpriseOptions ?? {}, "custom")
+ mergeProvider(enterpriseProviderID, {
+ source: "custom",
+ options: enterpriseOptions,
+ })
}
}
}
@@ -508,13 +634,21 @@ export namespace Provider {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
- mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
+ if (result.getModel) modelLoaders[providerID] = result.getModel
+ mergeProvider(providerID, {
+ source: "custom",
+ options: result.options,
+ })
}
}
// load config
for (const [providerID, provider] of configProviders) {
- mergeProvider(providerID, provider.options ?? {}, "config")
+ const partial: Partial<Info> = { source: "config" }
+ if (provider.env) partial.env = provider.env
+ if (provider.name) partial.name = provider.name
+ if (provider.options) partial.options = provider.options
+ mergeProvider(providerID, partial)
}
for (const [providerID, provider] of Object.entries(providers)) {
@@ -524,49 +658,43 @@ export namespace Provider {
}
if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
- provider.info.npm = "@ai-sdk/github-copilot"
+ provider.models = mapValues(provider.models, (model) => ({
+ ...model,
+ api: {
+ ...model.api,
+ npm: "@ai-sdk/github-copilot",
+ },
+ }))
}
const configProvider = config.provider?.[providerID]
- const filteredModels = Object.fromEntries(
- Object.entries(provider.info.models)
- // Filter out blacklisted models
- .filter(
- ([modelID]) =>
- modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
- )
- // Filter out experimental models
- .filter(
- ([, model]) =>
- ((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
- model.status !== "deprecated",
- )
- // 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))
- )
- }),
- )
- provider.info.models = filteredModels
+ for (const [modelID, model] of Object.entries(provider.models)) {
+ model.api.id = model.api.id ?? model.id ?? modelID
+ if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
+ delete provider.models[modelID]
+ if ((model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) || model.status === "deprecated")
+ delete provider.models[modelID]
+ if (
+ (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
+ (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
+ )
+ delete provider.models[modelID]
+ }
- if (Object.keys(provider.info.models).length === 0) {
+ if (Object.keys(provider.models).length === 0) {
delete providers[providerID]
continue
}
- log.info("found", { providerID, npm: provider.info.npm })
+ log.info("found", { providerID })
}
return {
- models,
+ models: languages,
providers,
sdk,
- realIdByKey,
+ modelLoaders,
}
})
@@ -574,19 +702,28 @@ export namespace Provider {
return state().then((state) => state.providers)
}
- async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
- return (async () => {
+ async function getSDK(model: Model) {
+ try {
using _ = log.time("getSDK", {
- providerID: provider.id,
+ providerID: model.providerID,
})
const s = await state()
- const pkg = model.provider?.npm ?? provider.npm ?? provider.id
- const options = { ...s.providers[provider.id]?.options }
- if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
+ const provider = s.providers[model.providerID]
+ const options = { ...provider.options }
+
+ if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
options["includeUsage"] = true
}
- const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
+ if (!options["baseURL"]) options["baseURL"] = model.api.url
+ if (!options["apiKey"]) options["apiKey"] = provider.key
+ if (model.headers)
+ options["headers"] = {
+ ...options["headers"],
+ ...model.headers,
+ }
+
+ const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
const existing = s.sdk.get(key)
if (existing) return existing
@@ -615,12 +752,13 @@ export namespace Provider {
}
// Special case: google-vertex-anthropic uses a subpath import
- const bundledKey = provider.id === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : pkg
+ const bundledKey =
+ model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
if (bundledFn) {
- log.info("using bundled provider", { providerID: provider.id, pkg: bundledKey })
+ log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
const loaded = bundledFn({
- name: provider.id,
+ name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
@@ -628,25 +766,25 @@ export namespace Provider {
}
let installedPath: string
- if (!pkg.startsWith("file://")) {
- installedPath = await BunProc.install(pkg, "latest")
+ if (!model.api.npm.startsWith("file://")) {
+ installedPath = await BunProc.install(model.api.npm, "latest")
} else {
- log.info("loading local provider", { pkg })
- installedPath = pkg
+ log.info("loading local provider", { pkg: model.api.npm })
+ installedPath = model.api.npm
}
const mod = await import(installedPath)
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn({
- name: provider.id,
+ name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
return loaded as SDK
- })().catch((e) => {
- throw new InitError({ providerID: provider.id }, { cause: e })
- })
+ } catch (e) {
+ throw new InitError({ providerID: model.providerID }, { cause: e })
+ }
}
export async function getProvider(providerID: string) {
@@ -654,15 +792,7 @@ export namespace Provider {
}
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)!
-
- log.info("getModel", {
- providerID,
- modelID,
- })
-
const provider = s.providers[providerID]
if (!provider) {
const availableProviders = Object.keys(s.providers)
@@ -671,43 +801,36 @@ export namespace Provider {
throw new ModelNotFoundError({ providerID, modelID, suggestions })
}
- const info = provider.info.models[modelID]
+ const info = provider.models[modelID]
if (!info) {
- const availableModels = Object.keys(provider.info.models)
+ const availableModels = Object.keys(provider.models)
const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
const suggestions = matches.map((m) => m.target)
throw new ModelNotFoundError({ providerID, modelID, suggestions })
}
+ return info
+ }
+
+ export async function getLanguage(model: Model) {
+ const s = await state()
+ const key = `${model.providerID}/${model.id}`
+ if (s.models.has(key)) return s.models.get(key)!
- const sdk = await getSDK(provider.info, info)
+ const provider = s.providers[model.providerID]
+ const sdk = await getSDK(model)
try {
- const keyReal = `${providerID}/${modelID}`
- const realID = s.realIdByKey.get(keyReal) ?? info.id
- const language = provider.getModel
- ? await provider.getModel(sdk, realID, provider.options)
- : sdk.languageModel(realID)
- log.info("found", { providerID, modelID })
- s.models.set(key, {
- providerID,
- modelID,
- info,
- language,
- npm: info.provider?.npm ?? provider.info.npm,
- })
- return {
- modelID,
- providerID,
- info,
- language,
- npm: info.provider?.npm ?? provider.info.npm,
- }
+ const language = s.modelLoaders[model.providerID]
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
+ : sdk.languageModel(model.api.id)
+ s.models.set(key, language)
+ return language
} catch (e) {
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
- modelID: modelID,
- providerID,
+ modelID: model.id,
+ providerID: model.providerID,
},
{ cause: e },
)
@@ -720,7 +843,7 @@ export namespace Provider {
const provider = s.providers[providerID]
if (!provider) return undefined
for (const item of query) {
- for (const modelID of Object.keys(provider.info.models)) {
+ for (const modelID of Object.keys(provider.models)) {
if (modelID.includes(item))
return {
providerID,
@@ -756,7 +879,7 @@ export namespace Provider {
priority = ["gpt-5-nano"]
}
for (const item of priority) {
- for (const model of Object.keys(provider.info.models)) {
+ for (const model of Object.keys(provider.models)) {
if (model.includes(item)) return getModel(providerID, model)
}
}
@@ -764,7 +887,7 @@ export namespace Provider {
// 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"]) {
+ if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
return getModel("opencode", "gpt-5-nano")
}
@@ -772,7 +895,7 @@ export namespace Provider {
}
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
- export function sort(models: ModelsDev.Model[]) {
+ export function sort(models: Model[]) {
return sortBy(
models,
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
@@ -787,12 +910,12 @@ export namespace Provider {
const provider = await list()
.then((val) => Object.values(val))
- .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
+ .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
if (!provider) throw new Error("no providers found")
- const [model] = sort(Object.values(provider.info.models))
+ const [model] = sort(Object.values(provider.models))
if (!model) throw new Error("no models found")
return {
- providerID: provider.info.id,
+ providerID: provider.id,
modelID: model.id,
}
}
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index abe269d5d..8afac3a65 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -1,10 +1,11 @@
import type { APICallError, ModelMessage } from "ai"
import { unique } from "remeda"
import type { JSONSchema } from "zod/v4/core"
+import type { Provider } from "./provider"
export namespace ProviderTransform {
- function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
- if (modelID.includes("claude")) {
+ function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+ if (model.api.id.includes("claude")) {
return msgs.map((msg) => {
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
msg.content = msg.content.map((part) => {
@@ -20,7 +21,7 @@ export namespace ProviderTransform {
return msg
})
}
- if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
+ if (model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral")) {
const result: ModelMessage[] = []
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]
@@ -107,67 +108,68 @@ export namespace ProviderTransform {
return msgs
}
- export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
- msgs = normalizeMessages(msgs, providerID, modelID)
- if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
- msgs = applyCaching(msgs, providerID)
+ export function message(msgs: ModelMessage[], model: Provider.Model) {
+ msgs = normalizeMessages(msgs, model)
+ if (model.providerID === "anthropic" || model.api.id.includes("anthropic") || model.api.id.includes("claude")) {
+ msgs = applyCaching(msgs, model.providerID)
}
return msgs
}
- export function temperature(_providerID: string, modelID: string) {
- if (modelID.toLowerCase().includes("qwen")) return 0.55
- if (modelID.toLowerCase().includes("claude")) return undefined
- if (modelID.toLowerCase().includes("gemini-3-pro")) return 1.0
+ export function temperature(model: Provider.Model) {
+ if (model.api.id.toLowerCase().includes("qwen")) return 0.55
+ if (model.api.id.toLowerCase().includes("claude")) return undefined
+ if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
return 0
}
- export function topP(_providerID: string, modelID: string) {
- if (modelID.toLowerCase().includes("qwen")) return 1
+ export function topP(model: Provider.Model) {
+ if (model.api.id.toLowerCase().includes("qwen")) return 1
return undefined
}
export function options(
- providerID: string,
- modelID: string,
- npm: string,
+ model: Provider.Model,
sessionID: string,
providerOptions?: Record<string, any>,
): Record<string, any> {
const result: Record<string, any> = {}
// switch to providerID later, for now use this
- if (npm === "@openrouter/ai-sdk-provider") {
+ if (model.api.npm === "@openrouter/ai-sdk-provider") {
result["usage"] = {
include: true,
}
}
- if (providerID === "openai" || providerOptions?.setCacheKey) {
+ if (model.providerID === "openai" || providerOptions?.setCacheKey) {
result["promptCacheKey"] = sessionID
}
- if (providerID === "google" || (providerID.startsWith("opencode") && modelID.includes("gemini-3"))) {
+ if (
+ model.providerID === "google" ||
+ (model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
+ ) {
result["thinkingConfig"] = {
includeThoughts: true,
}
}
- if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
- if (modelID.includes("codex")) {
+ if (model.providerID.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
+ if (model.providerID.includes("codex")) {
result["store"] = false
}
- if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
+ if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
}
- if (modelID.endsWith("gpt-5.1") && providerID !== "azure") {
+ if (model.api.id.endsWith("gpt-5.1") && model.providerID !== "azure") {
result["textVerbosity"] = "low"
}
- if (providerID.startsWith("opencode")) {
+ if (model.providerID.startsWith("opencode")) {
result["promptCacheKey"] = sessionID
result["include"] = ["reasoning.encrypted_content"]
result["reasoningSummary"] = "auto"
@@ -176,17 +178,17 @@ export namespace ProviderTransform {
return result
}
- export function smallOptions(input: { providerID: string; modelID: string }) {
+ export function smallOptions(model: Provider.Model) {
const options: Record<string, any> = {}
- if (input.providerID === "openai" || input.modelID.includes("gpt-5")) {
- if (input.modelID.includes("5.1")) {
+ if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
+ if (model.api.id.includes("5.1")) {
options["reasoningEffort"] = "low"
} else {
options["reasoningEffort"] = "minimal"
}
}
- if (input.providerID === "google") {
+ if (model.providerID === "google") {
options["thinkingConfig"] = {
thinkingBudget: 0,
}
@@ -254,7 +256,7 @@ export namespace ProviderTransform {
return standardLimit
}
- export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) {
+ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) {
/*
if (["openai", "azure"].includes(providerID)) {
if (schema.type === "object" && schema.properties) {
@@ -274,7 +276,7 @@ export namespace ProviderTransform {
*/
// Convert integer enums to string enums for Google/Gemini
- if (providerID === "google" || modelID.includes("gemini")) {
+ if (model.providerID === "google" || model.api.id.includes("gemini")) {
const sanitizeGemini = (obj: any): any => {
if (obj === null || typeof obj !== "object") {
return obj
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index fe4ad195a..31d082276 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
-import { mapValues } from "remeda"
+import { mapValues, pipe } from "remeda"
import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../file/ripgrep"
@@ -296,8 +296,8 @@ export namespace Server {
}),
),
async (c) => {
- const { provider, model } = c.req.valid("query")
- const tools = await ToolRegistry.tools(provider, model)
+ const { provider } = c.req.valid("query")
+ const tools = await ToolRegistry.tools(provider)
return c.json(
tools.map((t) => ({
id: t.id,
@@ -1025,7 +1025,7 @@ export namespace Server {
async (c) => {
c.status(204)
c.header("Content-Type", "application/json")
- return stream(c, async (stream) => {
+ return stream(c, async () => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
SessionPrompt.prompt({ ...body, sessionID })
@@ -1231,7 +1231,7 @@ export namespace Server {
"application/json": {
schema: resolver(
z.object({
- providers: ModelsDev.Provider.array(),
+ providers: Provider.Info.array(),
default: z.record(z.string(), z.string()),
}),
),
@@ -1242,7 +1242,7 @@ export namespace Server {
}),
async (c) => {
using _ = log.time("providers")
- const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
+ const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
return c.json({
providers: Object.values(providers),
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
@@ -1272,7 +1272,10 @@ export namespace Server {
},
}),
async (c) => {
- const providers = await ModelsDev.get()
+ const providers = pipe(
+ await ModelsDev.get(),
+ mapValues((x) => Provider.fromModelsDevProvider(x)),
+ )
const connected = await Provider.list().then((x) => Object.keys(x))
return c.json({
all: Object.values(providers),
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index a6b71edce..b83adafbe 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -1,4 +1,4 @@
-import { streamText, wrapLanguageModel, type ModelMessage } from "ai"
+import { wrapLanguageModel, type ModelMessage } from "ai"
import { Session } from "."
import { Identifier } from "../id/id"
import { Instance } from "../project/instance"
@@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system"
import { Bus } from "../bus"
import z from "zod"
-import type { ModelsDev } from "../provider/models"
import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
import { Token } from "../util/token"
@@ -29,7 +28,7 @@ export namespace SessionCompaction {
),
}
- export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) {
+ export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
const context = input.model.limit.context
if (context === 0) return false
@@ -98,6 +97,7 @@ export namespace SessionCompaction {
auto: boolean
}) {
const model = await Provider.getModel(input.model.providerID, input.model.modelID)
+ const language = await Provider.getLanguage(model)
const system = [...SystemPrompt.compaction(model.providerID)]
const msg = (await Session.updateMessage({
id: Identifier.ascending("message"),
@@ -126,79 +126,72 @@ export namespace SessionCompaction {
const processor = SessionProcessor.create({
assistantMessage: msg,
sessionID: input.sessionID,
- providerID: input.model.providerID,
- model: model.info,
+ model: model,
abort: input.abort,
})
- const result = await processor.process(() =>
- streamText({
- onError(error) {
- log.error("stream error", {
- error,
- })
- },
- // set to 0, we handle loop
- maxRetries: 0,
- providerOptions: ProviderTransform.providerOptions(
- model.npm,
- model.providerID,
- pipe(
- {},
- mergeDeep(ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", input.sessionID)),
- mergeDeep(model.info.options),
- ),
+ const result = await processor.process({
+ onError(error) {
+ log.error("stream error", {
+ error,
+ })
+ },
+ // set to 0, we handle loop
+ maxRetries: 0,
+ providerOptions: ProviderTransform.providerOptions(
+ model.api.npm,
+ model.providerID,
+ pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
+ ),
+ headers: model.headers,
+ abortSignal: input.abort,
+ tools: model.capabilities.toolcall ? {} : undefined,
+ messages: [
+ ...system.map(
+ (x): ModelMessage => ({
+ role: "system",
+ content: x,
+ }),
),
- headers: model.info.headers,
- abortSignal: input.abort,
- tools: model.info.tool_call ? {} : undefined,
- messages: [
- ...system.map(
- (x): ModelMessage => ({
- role: "system",
- content: x,
- }),
- ),
- ...MessageV2.toModelMessage(
- input.messages.filter((m) => {
- if (m.info.role !== "assistant" || m.info.error === undefined) {
- return true
- }
- if (
- MessageV2.AbortedError.isInstance(m.info.error) &&
- m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
- ) {
- return true
- }
+ ...MessageV2.toModelMessage(
+ input.messages.filter((m) => {
+ if (m.info.role !== "assistant" || m.info.error === undefined) {
+ return true
+ }
+ if (
+ MessageV2.AbortedError.isInstance(m.info.error) &&
+ m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
+ ) {
+ return true
+ }
- return false
- }),
- ),
- {
- role: "user",
- content: [
- {
- type: "text",
- text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
- },
- ],
- },
- ],
- model: wrapLanguageModel({
- model: model.language,
- middleware: [
+ return false
+ }),
+ ),
+ {
+ role: "user",
+ content: [
{
- async transformParams(args) {
- if (args.type === "stream") {
- // @ts-expect-error
- args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
- }
- return args.params
- },
+ type: "text",
+ text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
},
],
- }),
+ },
+ ],
+ model: wrapLanguageModel({
+ model: language,
+ middleware: [
+ {
+ async transformParams(args) {
+ if (args.type === "stream") {
+ // @ts-expect-error
+ args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
+ }
+ return args.params
+ },
+ },
+ ],
}),
- )
+ })
if (result === "continue" && input.auto) {
const continueMsg = await Session.updateMessage({
id: Identifier.ascending("message"),
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index f09818caa..6a148e973 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -6,8 +6,7 @@ import { Config } from "../config/config"
import { Flag } from "../flag/flag"
import { Identifier } from "../id/id"
import { Installation } from "../installation"
-import type { ModelsDev } from "../provider/models"
-import { Share } from "../share/share"
+
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import { MessageV2 } from "./message-v2"
@@ -16,7 +15,8 @@ import { SessionPrompt } from "./prompt"
import { fn } from "@/util/fn"
import { Command } from "../command"
import { Snapshot } from "@/snapshot"
-import { ShareNext } from "@/share/share-next"
+
+import type { Provider } from "@/provider/provider"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -223,6 +223,7 @@ export namespace Session {
}
if (cfg.enterprise?.url) {
+ const { ShareNext } = await import("@/share/share-next")
const share = await ShareNext.create(id)
await update(id, (draft) => {
draft.share = {
@@ -233,6 +234,7 @@ export namespace Session {
const session = await get(id)
if (session.share) return session.share
+ const { Share } = await import("../share/share")
const share = await Share.create(id)
await update(id, (draft) => {
draft.share = {
@@ -253,6 +255,7 @@ export namespace Session {
export const unshare = fn(Identifier.schema("session"), async (id) => {
const cfg = await Config.get()
if (cfg.enterprise?.url) {
+ const { ShareNext } = await import("@/share/share-next")
await ShareNext.remove(id)
await update(id, (draft) => {
draft.share = undefined
@@ -264,6 +267,7 @@ export namespace Session {
await update(id, (draft) => {
draft.share = undefined
})
+ const { Share } = await import("../share/share")
await Share.remove(id, share.secret)
})
@@ -389,7 +393,7 @@ export namespace Session {
export const getUsage = fn(
z.object({
- model: z.custom<ModelsDev.Model>(),
+ model: z.custom<Provider.Model>(),
usage: z.custom<LanguageModelUsage>(),
metadata: z.custom<ProviderMetadata>().optional(),
}),
@@ -420,16 +424,16 @@ export namespace Session {
}
const costInfo =
- input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000
- ? input.model.cost.context_over_200k
+ input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
+ ? input.model.cost.experimentalOver200K
: input.model.cost
return {
cost: safe(
new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
- .add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
- .add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
+ .add(new Decimal(tokens.cache.read).mul(costInfo?.cache.read ?? 0).div(1_000_000))
+ .add(new Decimal(tokens.cache.write).mul(costInfo?.cache.write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index 8655781d5..8b4faf026 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -1,6 +1,5 @@
-import type { ModelsDev } from "@/provider/models"
import { MessageV2 } from "./message-v2"
-import { type StreamTextResult, type Tool as AITool, APICallError } from "ai"
+import { streamText } from "ai"
import { Log } from "@/util/log"
import { Identifier } from "@/id/id"
import { Session } from "."
@@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
import { Bus } from "@/bus"
import { SessionRetry } from "./retry"
import { SessionStatus } from "./status"
+import type { Provider } from "@/provider/provider"
export namespace SessionProcessor {
const DOOM_LOOP_THRESHOLD = 3
@@ -19,11 +19,19 @@ export namespace SessionProcessor {
export type Info = Awaited<ReturnType<typeof create>>
export type Result = Awaited<ReturnType<Info["process"]>>
+ export type StreamInput = Parameters<typeof streamText>[0]
+
+ export type TBD = {
+ model: {
+ modelID: string
+ providerID: string
+ }
+ }
+
export function create(input: {
assistantMessage: MessageV2.Assistant
sessionID: string
- providerID: string
- model: ModelsDev.Model
+ model: Provider.Model
abort: AbortSignal
}) {
const toolcalls: Record<string, MessageV2.ToolPart> = {}
@@ -38,13 +46,13 @@ export namespace SessionProcessor {
partFromToolCall(toolCallID: string) {
return toolcalls[toolCallID]
},
- async process(fn: () => StreamTextResult<Record<string, AITool>, never>) {
+ async process(streamInput: StreamInput) {
log.info("process")
while (true) {
try {
let currentText: MessageV2.TextPart | undefined
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
- const stream = fn()
+ const stream = streamText(streamInput)
for await (const value of stream.fullStream) {
input.abort.throwIfAborted()
@@ -328,11 +336,12 @@ export namespace SessionProcessor {
continue
}
}
- } catch (e) {
+ } catch (e: any) {
log.error("process", {
error: e,
+ stack: JSON.stringify(e.stack),
})
- const error = MessageV2.fromError(e, { providerID: input.providerID })
+ const error = MessageV2.fromError(e, { providerID: input.sessionID })
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
attempt++
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index ee58bb338..d82cbd718 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -11,7 +11,6 @@ import { Agent } from "../agent/agent"
import { Provider } from "../provider/provider"
import {
generateText,
- streamText,
type ModelMessage,
type Tool as AITool,
tool,
@@ -288,6 +287,7 @@ export namespace SessionPrompt {
})
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
+ const language = await Provider.getLanguage(model)
const task = tasks.pop()
// pending subtask
@@ -311,7 +311,7 @@ export namespace SessionPrompt {
reasoning: 0,
cache: { read: 0, write: 0 },
},
- modelID: model.modelID,
+ modelID: model.id,
providerID: model.providerID,
time: {
created: Date.now(),
@@ -408,7 +408,7 @@ export namespace SessionPrompt {
agent: lastUser.agent,
model: {
providerID: model.providerID,
- modelID: model.modelID,
+ modelID: model.id,
},
sessionID,
auto: task.auto,
@@ -421,7 +421,7 @@ export namespace SessionPrompt {
if (
lastFinished &&
lastFinished.summary !== true &&
- SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info })
+ SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
) {
await SessionCompaction.create({
sessionID,
@@ -455,7 +455,7 @@ export namespace SessionPrompt {
reasoning: 0,
cache: { read: 0, write: 0 },
},
- modelID: model.modelID,
+ modelID: model.id,
providerID: model.providerID,
time: {
created: Date.now(),
@@ -463,20 +463,18 @@ export namespace SessionPrompt {
sessionID,
})) as MessageV2.Assistant,
sessionID: sessionID,
- model: model.info,
- providerID: model.providerID,
+ model,
abort,
})
const system = await resolveSystemPrompt({
- providerID: model.providerID,
- modelID: model.info.id,
+ model,
agent,
system: lastUser.system,
})
const tools = await resolveTools({
agent,
sessionID,
- model: lastUser.model,
+ model,
tools: lastUser.tools,
processor,
})
@@ -486,21 +484,19 @@ export namespace SessionPrompt {
{
sessionID: sessionID,
agent: lastUser.agent,
- model: model.info,
+ model: model,
provider,
message: lastUser,
},
{
- temperature: model.info.temperature
- ? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
+ temperature: model.capabilities.temperature
+ ? (agent.temperature ?? ProviderTransform.temperature(model))
: undefined,
- topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
+ topP: agent.topP ?? ProviderTransform.topP(model),
options: pipe(
{},
- mergeDeep(
- ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", sessionID, provider?.options),
- ),
- mergeDeep(model.info.options),
+ mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
+ mergeDeep(model.options),
mergeDeep(agent.options),
),
},
@@ -513,113 +509,111 @@ export namespace SessionPrompt {
})
}
- const result = await processor.process(() =>
- streamText({
- onError(error) {
- log.error("stream error", {
- error,
+ const result = await processor.process({
+ onError(error) {
+ log.error("stream error", {
+ error,
+ })
+ },
+ async experimental_repairToolCall(input) {
+ const lower = input.toolCall.toolName.toLowerCase()
+ if (lower !== input.toolCall.toolName && tools[lower]) {
+ log.info("repairing tool call", {
+ tool: input.toolCall.toolName,
+ repaired: lower,
})
- },
- async experimental_repairToolCall(input) {
- const lower = input.toolCall.toolName.toLowerCase()
- if (lower !== input.toolCall.toolName && tools[lower]) {
- log.info("repairing tool call", {
- tool: input.toolCall.toolName,
- repaired: lower,
- })
- return {
- ...input.toolCall,
- toolName: lower,
- }
- }
return {
...input.toolCall,
- input: JSON.stringify({
- tool: input.toolCall.toolName,
- error: input.error.message,
- }),
- toolName: "invalid",
+ toolName: lower,
}
- },
- headers: {
- ...(model.providerID.startsWith("opencode")
- ? {
- "x-opencode-project": Instance.project.id,
- "x-opencode-session": sessionID,
- "x-opencode-request": lastUser.id,
- }
- : undefined),
- ...model.info.headers,
- },
- // set to 0, we handle loop
- maxRetries: 0,
- activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
- maxOutputTokens: ProviderTransform.maxOutputTokens(
- model.providerID,
- params.options,
- model.info.limit.output,
- OUTPUT_TOKEN_MAX,
+ }
+ return {
+ ...input.toolCall,
+ input: JSON.stringify({
+ tool: input.toolCall.toolName,
+ error: input.error.message,
+ }),
+ toolName: "invalid",
+ }
+ },
+ headers: {
+ ...(model.providerID.startsWith("opencode")
+ ? {
+ "x-opencode-project": Instance.project.id,
+ "x-opencode-session": sessionID,
+ "x-opencode-request": lastUser.id,
+ }
+ : undefined),
+ ...model.headers,
+ },
+ // set to 0, we handle loop
+ maxRetries: 0,
+ activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
+ maxOutputTokens: ProviderTransform.maxOutputTokens(
+ model.api.npm,
+ params.options,
+ model.limit.output,
+ OUTPUT_TOKEN_MAX,
+ ),
+ abortSignal: abort,
+ providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
+ stopWhen: stepCountIs(1),
+ temperature: params.temperature,
+ topP: params.topP,
+ messages: [
+ ...system.map(
+ (x): ModelMessage => ({
+ role: "system",
+ content: x,
+ }),
),
- abortSignal: abort,
- providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
- stopWhen: stepCountIs(1),
- temperature: params.temperature,
- topP: params.topP,
- messages: [
- ...system.map(
- (x): ModelMessage => ({
- role: "system",
- content: x,
- }),
- ),
- ...MessageV2.toModelMessage(
- msgs.filter((m) => {
- if (m.info.role !== "assistant" || m.info.error === undefined) {
- return true
- }
- if (
- MessageV2.AbortedError.isInstance(m.info.error) &&
- m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
- ) {
- return true
- }
+ ...MessageV2.toModelMessage(
+ msgs.filter((m) => {
+ if (m.info.role !== "assistant" || m.info.error === undefined) {
+ return true
+ }
+ if (
+ MessageV2.AbortedError.isInstance(m.info.error) &&
+ m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
+ ) {
+ return true
+ }
- return false
- }),
- ),
- ],
- tools: model.info.tool_call === false ? undefined : tools,
- model: wrapLanguageModel({
- model: model.language,
- middleware: [
- {
- async transformParams(args) {
- if (args.type === "stream") {
- // @ts-expect-error
- args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
- }
- // Transform tool schemas for provider compatibility
- if (args.params.tools && Array.isArray(args.params.tools)) {
- args.params.tools = args.params.tools.map((tool: any) => {
- // Tools at middleware level have inputSchema, not parameters
- if (tool.inputSchema && typeof tool.inputSchema === "object") {
- // Transform the inputSchema for provider compatibility
- return {
- ...tool,
- inputSchema: ProviderTransform.schema(model.providerID, model.modelID, tool.inputSchema),
- }
+ return false
+ }),
+ ),
+ ],
+ tools: model.capabilities.toolcall === false ? undefined : tools,
+ model: wrapLanguageModel({
+ model: language,
+ middleware: [
+ {
+ async transformParams(args) {
+ if (args.type === "stream") {
+ // @ts-expect-error - prompt types are compatible at runtime
+ args.params.prompt = ProviderTransform.message(args.params.prompt, model)
+ }
+ // Transform tool schemas for provider compatibility
+ if (args.params.tools && Array.isArray(args.params.tools)) {
+ args.params.tools = args.params.tools.map((tool: any) => {
+ // Tools at middleware level have inputSchema, not parameters
+ if (tool.inputSchema && typeof tool.inputSchema === "object") {
+ // Transform the inputSchema for provider compatibility
+ return {
+ ...tool,
+ inputSchema: ProviderTransform.schema(model, tool.inputSchema),
}
- // If no inputSchema, return tool unchanged
- return tool
- })
- }
- return args.params
- },
+ }
+ // If no inputSchema, return tool unchanged
+ return tool
+ })
+ }
+ return args.params
},
- ],
- }),
+ },
+ ],
}),
- )
+ })
if (result === "stop") break
continue
}
@@ -642,18 +636,13 @@ export namespace SessionPrompt {
return Provider.defaultModel()
}
- async function resolveSystemPrompt(input: {
- system?: string
- agent: Agent.Info
- providerID: string
- modelID: string
- }) {
- let system = SystemPrompt.header(input.providerID)
+ async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
+ let system = SystemPrompt.header(input.model.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (input.agent.prompt) return [input.agent.prompt]
- return SystemPrompt.provider(input.modelID)
+ return SystemPrompt.provider(input.model)
})(),
)
system.push(...(await SystemPrompt.environment()))
@@ -666,10 +655,7 @@ export namespace SessionPrompt {
async function resolveTools(input: {
agent: Agent.Info
- model: {
- providerID: string
- modelID: string
- }
+ model: Provider.Model
sessionID: string
tools?: Record<string, boolean>
processor: SessionProcessor.Info
@@ -677,16 +663,12 @@ export namespace SessionPrompt {
const tools: Record<string, AITool> = {}
const enabledTools = pipe(
input.agent.tools,
- mergeDeep(await ToolRegistry.enabled(input.model.providerID, input.model.modelID, input.agent)),
+ mergeDeep(await ToolRegistry.enabled(input.agent)),
mergeDeep(input.tools ?? {}),
)
- for (const item of await ToolRegistry.tools(input.model.providerID, input.model.modelID)) {
+ for (const item of await ToolRegistry.tools(input.model.providerID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
- const schema = ProviderTransform.schema(
- input.model.providerID,
- input.model.modelID,
- z.toJSONSchema(item.parameters),
- )
+ const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
tools[item.id] = tool({
id: item.id as any,
description: item.description,
@@ -1437,25 +1419,18 @@ export namespace SessionPrompt {
if (!isFirst) return
const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
+ const language = await Provider.getLanguage(small)
const provider = await Provider.getProvider(small.providerID)
const options = pipe(
{},
- mergeDeep(
- ProviderTransform.options(
- small.providerID,
- small.modelID,
- small.npm ?? "",
- input.session.id,
- provider?.options,
- ),
- ),
- mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
- mergeDeep(small.info.options),
+ mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
+ mergeDeep(ProviderTransform.smallOptions(small)),
+ mergeDeep(small.options),
)
await generateText({
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
- maxOutputTokens: small.info.reasoning ? 3000 : 20,
- providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+ maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
+ providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
@@ -1486,8 +1461,8 @@ export namespace SessionPrompt {
},
]),
],
- headers: small.info.headers,
- model: small.language,
+ headers: small.headers,
+ model: language,
})
.then((result) => {
if (result.text)
@@ -1504,7 +1479,7 @@ export namespace SessionPrompt {
})
})
.catch((error) => {
- log.error("failed to generate title", { error, model: small.info.id })
+ log.error("failed to generate title", { error, model: small.id })
})
}
}
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
index d9247f182..8d366e499 100644
--- a/packages/opencode/src/session/summary.ts
+++ b/packages/opencode/src/session/summary.ts
@@ -76,19 +76,20 @@ export namespace SessionSummary {
const small =
(await Provider.getSmallModel(assistantMsg.providerID)) ??
(await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
+ const language = await Provider.getLanguage(small)
const options = pipe(
{},
- mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
- mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
- mergeDeep(small.info.options),
+ mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)),
+ mergeDeep(ProviderTransform.smallOptions(small)),
+ mergeDeep(small.options),
)
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
- maxOutputTokens: small.info.reasoning ? 1500 : 20,
- providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+ maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
+ providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
@@ -106,8 +107,8 @@ export namespace SessionSummary {
`,
},
],
- headers: small.info.headers,
- model: small.language,
+ headers: small.headers,
+ model: language,
})
log.info("title", { title: result.text })
userMsg.summary.title = result.text
@@ -132,9 +133,9 @@ export namespace SessionSummary {
}
}
const result = await generateText({
- model: small.language,
+ model: language,
maxOutputTokens: 100,
- providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+ providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [
...SystemPrompt.summarize(small.providerID).map(
(x): ModelMessage => ({
@@ -148,7 +149,7 @@ export namespace SessionSummary {
content: `Summarize the above conversation according to your system prompts.`,
},
],
- headers: small.info.headers,
+ headers: small.headers,
}).catch(() => {})
if (result) summary = result.text
}
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index 399cad8cd..3146110cf 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -17,6 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_CODEX from "./prompt/codex.txt"
+import type { Provider } from "@/provider/provider"
export namespace SystemPrompt {
export function header(providerID: string) {
@@ -24,12 +25,13 @@ export namespace SystemPrompt {
return []
}
- export function provider(modelID: string) {
- if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
- if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
- if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
- if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
- if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS]
+ export function provider(model: Provider.Model) {
+ if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
+ if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
+ return [PROMPT_BEAST]
+ if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
+ if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
+ if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
index 9543149a8..996400280 100644
--- a/packages/opencode/src/share/share-next.ts
+++ b/packages/opencode/src/share/share-next.ts
@@ -1,7 +1,6 @@
import { Bus } from "@/bus"
import { Config } from "@/config/config"
import { ulid } from "ulid"
-import type { ModelsDev } from "@/provider/models"
import { Provider } from "@/provider/provider"
import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2"
@@ -36,7 +35,7 @@ export namespace ShareNext {
type: "model",
data: [
await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
- (m) => m.info,
+ (m) => m,
),
],
},
@@ -105,7 +104,7 @@ export namespace ShareNext {
}
| {
type: "model"
- data: ModelsDev.Model[]
+ data: SDK.Model[]
}
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
@@ -171,7 +170,7 @@ export namespace ShareNext {
messages
.filter((m) => m.info.role === "user")
.map((m) => (m.info as SDK.UserMessage).model)
- .map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m.info)),
+ .map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
)
await sync(sessionID, [
{
diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts
index 7d6449e7d..cc61b090a 100644
--- a/packages/opencode/src/tool/batch.ts
+++ b/packages/opencode/src/tool/batch.ts
@@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => {
const discardedCalls = params.tool_calls.slice(10)
const { ToolRegistry } = await import("./registry")
- const availableTools = await ToolRegistry.tools("", "")
+ const availableTools = await ToolRegistry.tools("")
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
const executeCall = async (call: (typeof toolCalls)[0]) => {
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index cf7b20e8b..7e01246b5 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -101,7 +101,7 @@ export const ReadTool = Tool.define("read", {
const modelID = ctx.extra["modelID"] as string
const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
if (!model) return false
- return model.info.modalities?.input?.includes("image") ?? false
+ return model.capabilities.input.image
})()
if (isImage) {
if (!supportsImages) {
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 26b6ea9fc..33a54675f 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -108,7 +108,7 @@ export namespace ToolRegistry {
return all().then((x) => x.map((t) => t.id))
}
- export async function tools(providerID: string, _modelID: string) {
+ export async function tools(providerID: string) {
const tools = await all()
const result = await Promise.all(
tools
@@ -124,11 +124,7 @@ export namespace ToolRegistry {
return result
}
- export async function enabled(
- _providerID: string,
- _modelID: string,
- agent: Agent.Info,
- ): Promise<Record<string, boolean>> {
+ export async function enabled(agent: Agent.Info): Promise<Record<string, boolean>> {
const result: Record<string, boolean> = {}
if (agent.permission.edit === "deny") {
diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts
index fa31d9d4f..698fdddfb 100644
--- a/packages/opencode/test/provider/provider.test.ts
+++ b/packages/opencode/test/provider/provider.test.ts
@@ -132,7 +132,7 @@ test("model whitelist filters models for provider", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].info.models)
+ const models = Object.keys(providers["anthropic"].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models.length).toBe(1)
},
@@ -163,7 +163,7 @@ test("model blacklist excludes specific models", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].info.models)
+ const models = Object.keys(providers["anthropic"].models)
expect(models).not.toContain("claude-sonnet-4-20250514")
},
})
@@ -198,8 +198,8 @@ test("custom model alias via config", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
- expect(providers["anthropic"].info.models["my-alias"]).toBeDefined()
- expect(providers["anthropic"].info.models["my-alias"].name).toBe("My Custom Alias")
+ expect(providers["anthropic"].models["my-alias"]).toBeDefined()
+ expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias")
},
})
})
@@ -241,8 +241,8 @@ test("custom provider with npm package", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["custom-provider"]).toBeDefined()
- expect(providers["custom-provider"].info.name).toBe("Custom Provider")
- expect(providers["custom-provider"].info.models["custom-model"]).toBeDefined()
+ expect(providers["custom-provider"].name).toBe("Custom Provider")
+ expect(providers["custom-provider"].models["custom-model"]).toBeDefined()
},
})
})
@@ -299,8 +299,9 @@ test("getModel returns model for valid provider/model", async () => {
const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
expect(model).toBeDefined()
expect(model.providerID).toBe("anthropic")
- expect(model.modelID).toBe("claude-sonnet-4-20250514")
- expect(model.language).toBeDefined()
+ expect(model.id).toBe("claude-sonnet-4-20250514")
+ const language = await Provider.getLanguage(model)
+ expect(language).toBeDefined()
},
})
})
@@ -478,11 +479,11 @@ test("model cost defaults to zero when not specified", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- const model = providers["test-provider"].info.models["test-model"]
+ const model = providers["test-provider"].models["test-model"]
expect(model.cost.input).toBe(0)
expect(model.cost.output).toBe(0)
- expect(model.cost.cache_read).toBe(0)
- expect(model.cost.cache_write).toBe(0)
+ expect(model.cost.cache.read).toBe(0)
+ expect(model.cost.cache.write).toBe(0)
},
})
})
@@ -516,7 +517,7 @@ test("model options are merged from existing model", async () => {
},
fn: async () => {
const providers = await Provider.list()
- const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+ const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
expect(model.options.customOption).toBe("custom-value")
},
})
@@ -623,17 +624,17 @@ test("getModel uses realIdByKey for aliased models", async () => {
},
fn: async () => {
const providers = await Provider.list()
- expect(providers["anthropic"].info.models["my-sonnet"]).toBeDefined()
+ expect(providers["anthropic"].models["my-sonnet"]).toBeDefined()
const model = await Provider.getModel("anthropic", "my-sonnet")
expect(model).toBeDefined()
- expect(model.modelID).toBe("my-sonnet")
- expect(model.info.name).toBe("My Sonnet Alias")
+ expect(model.id).toBe("my-sonnet")
+ expect(model.name).toBe("My Sonnet Alias")
},
})
})
-test("provider api field sets default baseURL", async () => {
+test("provider api field sets model api.url", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
@@ -666,7 +667,8 @@ test("provider api field sets default baseURL", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- expect(providers["custom-api"].options.baseURL).toBe("https://api.example.com/v1")
+ // api field is stored on model.api.url, used by getSDK to set baseURL
+ expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1")
},
})
})
@@ -737,10 +739,10 @@ test("model inherits properties from existing database model", async () => {
},
fn: async () => {
const providers = await Provider.list()
- const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+ const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
expect(model.name).toBe("Custom Name for Sonnet")
- expect(model.tool_call).toBe(true)
- expect(model.attachment).toBe(true)
+ expect(model.capabilities.toolcall).toBe(true)
+ expect(model.capabilities.attachment).toBe(true)
expect(model.limit.context).toBeGreaterThan(0)
},
})
@@ -820,7 +822,7 @@ test("whitelist and blacklist can be combined", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].info.models)
+ const models = Object.keys(providers["anthropic"].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models).not.toContain("claude-opus-4-20250514")
expect(models.length).toBe(1)
@@ -858,11 +860,9 @@ test("model modalities default correctly", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- const model = providers["test-provider"].info.models["test-model"]
- expect(model.modalities).toEqual({
- input: ["text"],
- output: ["text"],
- })
+ const model = providers["test-provider"].models["test-model"]
+ expect(model.capabilities.input.text).toBe(true)
+ expect(model.capabilities.output.text).toBe(true)
},
})
})
@@ -903,11 +903,11 @@ test("model with custom cost values", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- const model = providers["test-provider"].info.models["test-model"]
+ const model = providers["test-provider"].models["test-model"]
expect(model.cost.input).toBe(5)
expect(model.cost.output).toBe(15)
- expect(model.cost.cache_read).toBe(2.5)
- expect(model.cost.cache_write).toBe(7.5)
+ expect(model.cost.cache.read).toBe(2.5)
+ expect(model.cost.cache.write).toBe(7.5)
},
})
})
@@ -931,7 +931,7 @@ test("getSmallModel returns appropriate small model", async () => {
fn: async () => {
const model = await Provider.getSmallModel("anthropic")
expect(model).toBeDefined()
- expect(model?.modelID).toContain("haiku")
+ expect(model?.id).toContain("haiku")
},
})
})
@@ -957,7 +957,7 @@ test("getSmallModel respects config small_model override", async () => {
const model = await Provider.getSmallModel("anthropic")
expect(model).toBeDefined()
expect(model?.providerID).toBe("anthropic")
- expect(model?.modelID).toBe("claude-sonnet-4-20250514")
+ expect(model?.id).toBe("claude-sonnet-4-20250514")
},
})
})
@@ -1046,7 +1046,7 @@ test("provider with custom npm package", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["local-llm"]).toBeDefined()
- expect(providers["local-llm"].info.npm).toBe("@ai-sdk/openai-compatible")
+ expect(providers["local-llm"].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1")
},
})
@@ -1082,7 +1082,7 @@ test("model alias name defaults to alias key when id differs", async () => {
},
fn: async () => {
const providers = await Provider.list()
- expect(providers["anthropic"].info.models["sonnet"].name).toBe("sonnet")
+ expect(providers["anthropic"].models["sonnet"].name).toBe("sonnet")
},
})
})
@@ -1123,8 +1123,8 @@ test("provider with multiple env var options only includes apiKey when single en
fn: async () => {
const providers = await Provider.list()
expect(providers["multi-env"]).toBeDefined()
- // When multiple env options exist, apiKey should NOT be auto-set
- expect(providers["multi-env"].options.apiKey).toBeUndefined()
+ // When multiple env options exist, key should NOT be auto-set
+ expect(providers["multi-env"].key).toBeUndefined()
},
})
})
@@ -1165,8 +1165,8 @@ test("provider with single env var includes apiKey automatically", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["single-env"]).toBeDefined()
- // Single env option should auto-set apiKey
- expect(providers["single-env"].options.apiKey).toBe("my-api-key")
+ // Single env option should auto-set key
+ expect(providers["single-env"].key).toBe("my-api-key")
},
})
})
@@ -1201,7 +1201,7 @@ test("model cost overrides existing cost values", async () => {
},
fn: async () => {
const providers = await Provider.list()
- const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+ const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
expect(model.cost.input).toBe(999)
expect(model.cost.output).toBe(888)
},
@@ -1249,11 +1249,11 @@ test("completely new provider not in database can be configured", async () => {
fn: async () => {
const providers = await Provider.list()
expect(providers["brand-new-provider"]).toBeDefined()
- expect(providers["brand-new-provider"].info.name).toBe("Brand New")
- const model = providers["brand-new-provider"].info.models["new-model"]
- expect(model.reasoning).toBe(true)
- expect(model.attachment).toBe(true)
- expect(model.modalities?.input).toContain("image")
+ expect(providers["brand-new-provider"].name).toBe("Brand New")
+ const model = providers["brand-new-provider"].models["new-model"]
+ expect(model.capabilities.reasoning).toBe(true)
+ expect(model.capabilities.attachment).toBe(true)
+ expect(model.capabilities.input.image).toBe(true)
},
})
})
@@ -1322,7 +1322,7 @@ test("model with tool_call false", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- expect(providers["no-tools"].info.models["basic-model"].tool_call).toBe(false)
+ expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false)
},
})
})
@@ -1357,7 +1357,7 @@ test("model defaults tool_call to true when not specified", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- expect(providers["default-tools"].info.models["model"].tool_call).toBe(true)
+ expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true)
},
})
})
@@ -1396,7 +1396,7 @@ test("model headers are preserved", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- const model = providers["headers-provider"].info.models["model"]
+ const model = providers["headers-provider"].models["model"]
expect(model.headers).toEqual({
"X-Custom-Header": "custom-value",
Authorization: "Bearer special-token",
@@ -1465,8 +1465,8 @@ test("getModel returns consistent results", async () => {
const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
expect(model1.providerID).toEqual(model2.providerID)
- expect(model1.modelID).toEqual(model2.modelID)
- expect(model1.info).toEqual(model2.info)
+ expect(model1.id).toEqual(model2.id)
+ expect(model1).toEqual(model2)
},
})
})
@@ -1501,7 +1501,7 @@ test("provider name defaults to id when not in database", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- expect(providers["my-custom-id"].info.name).toBe("my-custom-id")
+ expect(providers["my-custom-id"].name).toBe("my-custom-id")
},
})
})
@@ -1601,7 +1601,7 @@ test("getProvider returns provider info", async () => {
fn: async () => {
const provider = await Provider.getProvider("anthropic")
expect(provider).toBeDefined()
- expect(provider?.info.id).toBe("anthropic")
+ expect(provider?.id).toBe("anthropic")
},
})
})
@@ -1684,7 +1684,7 @@ test("model limit defaults to zero when not specified", async () => {
directory: tmp.path,
fn: async () => {
const providers = await Provider.list()
- const model = providers["no-limit"].info.models["model"]
+ const model = providers["no-limit"].models["model"]
expect(model.limit.context).toBe(0)
expect(model.limit.output).toBe(0)
},
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 80348fb9a..6c80f0b7c 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -942,6 +942,75 @@ export type AgentConfig = {
| undefined
}
+export type ProviderConfig = {
+ api?: string
+ name?: string
+ env?: Array<string>
+ id?: string
+ npm?: string
+ models?: {
+ [key: string]: {
+ id?: string
+ name?: string
+ release_date?: string
+ attachment?: boolean
+ reasoning?: boolean
+ temperature?: boolean
+ tool_call?: boolean
+ cost?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ context_over_200k?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ }
+ }
+ limit?: {
+ context: number
+ output: number
+ }
+ modalities?: {
+ input: Array<"text" | "audio" | "image" | "video" | "pdf">
+ output: Array<"text" | "audio" | "image" | "video" | "pdf">
+ }
+ experimental?: boolean
+ status?: "alpha" | "beta" | "deprecated"
+ options?: {
+ [key: string]: unknown
+ }
+ headers?: {
+ [key: string]: string
+ }
+ provider?: {
+ npm: string
+ }
+ }
+ }
+ whitelist?: Array<string>
+ blacklist?: Array<string>
+ options?: {
+ apiKey?: string
+ baseURL?: string
+ /**
+ * GitHub Enterprise URL for copilot authentication
+ */
+ enterpriseUrl?: string
+ /**
+ * Enable promptCacheKey for this provider (default false)
+ */
+ setCacheKey?: boolean
+ /**
+ * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
+ */
+ timeout?: number | false
+ [key: string]: unknown | string | boolean | (number | false) | undefined
+ }
+}
+
export type McpLocalConfig = {
/**
* Type of MCP server connection
@@ -1100,74 +1169,7 @@ export type Config = {
* Custom provider configurations and model overrides
*/
provider?: {
- [key: string]: {
- api?: string
- name?: string
- env?: Array<string>
- id?: string
- npm?: string
- models?: {
- [key: string]: {
- id?: string
- name?: string
- release_date?: string
- attachment?: boolean
- reasoning?: boolean
- temperature?: boolean
- tool_call?: boolean
- cost?: {
- input: number
- output: number
- cache_read?: number
- cache_write?: number
- context_over_200k?: {
- input: number
- output: number
- cache_read?: number
- cache_write?: number
- }
- }
- limit?: {
- context: number
- output: number
- }
- modalities?: {
- input: Array<"text" | "audio" | "image" | "video" | "pdf">
- output: Array<"text" | "audio" | "image" | "video" | "pdf">
- }
- experimental?: boolean
- status?: "alpha" | "beta" | "deprecated"
- options?: {
- [key: string]: unknown
- }
- headers?: {
- [key: string]: string
- }
- provider?: {
- npm: string
- }
- }
- }
- whitelist?: Array<string>
- blacklist?: Array<string>
- options?: {
- apiKey?: string
- baseURL?: string
- /**
- * GitHub Enterprise URL for copilot authentication
- */
- enterpriseUrl?: string
- /**
- * Enable promptCacheKey for this provider (default false)
- */
- setCacheKey?: boolean
- /**
- * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
- */
- timeout?: number | false
- [key: string]: unknown | string | boolean | (number | false) | undefined
- }
- }
+ [key: string]: ProviderConfig
}
/**
* MCP (Model Context Protocol) server configurations
@@ -1354,51 +1356,71 @@ export type Command = {
export type Model = {
id: string
+ providerID: string
+ api: {
+ id: string
+ url: string
+ npm: string
+ }
name: string
- release_date: string
- attachment: boolean
- reasoning: boolean
- temperature: boolean
- tool_call: boolean
+ capabilities: {
+ temperature: boolean
+ reasoning: boolean
+ attachment: boolean
+ toolcall: boolean
+ input: {
+ text: boolean
+ audio: boolean
+ image: boolean
+ video: boolean
+ pdf: boolean
+ }
+ output: {
+ text: boolean
+ audio: boolean
+ image: boolean
+ video: boolean
+ pdf: boolean
+ }
+ }
cost: {
input: number
output: number
- cache_read?: number
- cache_write?: number
- context_over_200k?: {
+ cache: {
+ read: number
+ write: number
+ }
+ experimentalOver200K?: {
input: number
output: number
- cache_read?: number
- cache_write?: number
+ cache: {
+ read: number
+ write: number
+ }
}
}
limit: {
context: number
output: number
}
- modalities?: {
- input: Array<"text" | "audio" | "image" | "video" | "pdf">
- output: Array<"text" | "audio" | "image" | "video" | "pdf">
- }
- experimental?: boolean
- status?: "alpha" | "beta" | "deprecated"
+ status: "alpha" | "beta" | "deprecated" | "active"
options: {
[key: string]: unknown
}
- headers?: {
+ headers: {
[key: string]: string
}
- provider?: {
- npm: string
- }
}
export type Provider = {
- api?: string
+ id: string
name: string
+ source: "env" | "config" | "custom" | "api"
env: Array<string>
- id: string
- npm?: string
+ key?: string
+ options: {
+ [key: string]: unknown
+ }
models: {
[key: string]: Model
}
@@ -2665,7 +2687,55 @@ export type ProviderListResponses = {
* List of providers
*/
200: {
- all: Array<Provider>
+ all: Array<{
+ api?: string
+ name: string
+ env: Array<string>
+ id: string
+ npm?: string
+ models: {
+ [key: string]: {
+ id: string
+ name: string
+ release_date: string
+ attachment: boolean
+ reasoning: boolean
+ temperature: boolean
+ tool_call: boolean
+ cost?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ context_over_200k?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ }
+ }
+ limit: {
+ context: number
+ output: number
+ }
+ modalities?: {
+ input: Array<"text" | "audio" | "image" | "video" | "pdf">
+ output: Array<"text" | "audio" | "image" | "video" | "pdf">
+ }
+ experimental?: boolean
+ status?: "alpha" | "beta" | "deprecated"
+ options: {
+ [key: string]: unknown
+ }
+ headers?: {
+ [key: string]: string
+ }
+ provider?: {
+ npm: string
+ }
+ }
+ }
+ }>
default: {
[key: string]: string
}