From 2dbb029472a6e4fc9c154eb02e20cabbb712db8c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 3 Dec 2025 15:14:30 -0500 Subject: [PATCH] sync --- .../desktop/src/components/prompt-input.tsx | 4 +- packages/opencode/src/agent/agent.ts | 3 +- packages/opencode/src/cli/cmd/models.ts | 2 +- packages/opencode/src/provider/models.ts | 96 +++---- packages/opencode/src/provider/provider.ts | 93 +++---- packages/opencode/src/provider/transform.ts | 63 ++--- packages/opencode/src/server/server.ts | 13 +- packages/opencode/src/session/compaction.ts | 21 +- packages/opencode/src/session/index.ts | 12 +- packages/opencode/src/session/processor.ts | 7 +- packages/opencode/src/session/prompt.ts | 80 +++--- packages/opencode/src/session/summary.ts | 21 +- packages/opencode/src/session/system.ts | 14 +- packages/opencode/src/share/share-next.ts | 7 +- packages/opencode/src/tool/read.ts | 2 +- packages/sdk/js/src/gen/types.gen.ts | 260 +++++++++++------- 16 files changed, 365 insertions(+), 333 deletions(-) diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 9769242237..a311ae7638 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 = (props) => {
{i.name} - + - {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")} + {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index b901b95c2f..0e7a7c5d3b 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 1ae4ae12ca..156dae91c6 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/provider/models.ts b/packages/opencode/src/provider/models.ts index f8ff2e86a4..cedeed69a2 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -9,63 +9,55 @@ 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(), - target: 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(), - cache_write: z.number().optional(), - context_over_200k: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - }) - .optional(), - }), - limit: z.object({ - context: z.number(), - output: z.number(), - }), - modalities: z + export const Model = z.object({ + id: z.string(), + name: z.string(), + target: 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(), + cache_write: z.number().optional(), + context_over_200k: z .object({ - input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), }) .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", - }) + }), + 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 - 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 diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8341faf3d3..3a29655a02 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -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 CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise type CustomLoader = (provider: Info) => Promise<{ autoload: boolean - getModel?: (sdk: any, modelID: string, options?: Record) => Promise + getModel?: CustomModelLoader options?: Record }> - type Source = "env" | "config" | "custom" | "api" - const CUSTOM_LOADERS: Record = { async anthropic() { return { @@ -314,20 +313,20 @@ export namespace Provider { reasoning: z.boolean(), attachment: z.boolean(), toolcall: z.boolean(), - input: { + input: z.object({ text: z.boolean(), audio: z.boolean(), image: z.boolean(), video: z.boolean(), pdf: z.boolean(), - }, - output: { + }), + output: z.object({ text: z.boolean(), audio: z.boolean(), image: z.boolean(), video: z.boolean(), pdf: z.boolean(), - }, + }), }), cost: z.object({ input: z.number(), @@ -433,7 +432,7 @@ export namespace Provider { } } - function fromModelsDevProvider(provider: ModelsDev.Provider): Info { + export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { return { id: provider.id, source: "custom", @@ -444,11 +443,6 @@ export namespace Provider { } } - export type ModelWithStuff = { - language: LanguageModel - info: Model - } - const state = Instance.state(async () => { using _ = log.time("state") const config = await Config.get() @@ -464,7 +458,10 @@ export namespace Provider { } const providers: { [providerID: string]: Info } = {} - const models = new Map() + const languages = new Map() + const modelLoaders: { + [providerID: string]: CustomModelLoader + } = {} const sdk = new Map() log.info("init") @@ -631,6 +628,7 @@ export namespace Provider { if (disabled.has(providerID)) continue const result = await fn(database[providerID]) if (result && (result.autoload || providers[providerID])) { + if (result.getModel) modelLoaders[providerID] = result.getModel mergeProvider(providerID, { source: "custom", options: result.options, @@ -645,7 +643,6 @@ export namespace Provider { env: provider.env, name: provider.name, options: provider.options, - // TODO: merge models }) } @@ -689,9 +686,10 @@ export namespace Provider { } return { - models, + models: languages, providers, sdk, + modelLoaders, } }) @@ -789,15 +787,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) @@ -813,38 +803,29 @@ export namespace Provider { const suggestions = matches.map((m) => m.target) throw new ModelNotFoundError({ providerID, modelID, suggestions }) } + return info + } - const sdk = await getSDK(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 provider = s.providers[model.providerID] + const sdk = await getSDK(model) try { - const language = provider.getModel - ? await provider.getModel(sdk, info.api.id, provider.options) - : sdk.languageModel(info.api.id) - log.info("found", { providerID, modelID }) - const cached: ModelWithStuff = { - info, - language, - } - s.models.set(key, { - providerID, - modelID, - info, - language, - npm, - }) - return { - modelID, - providerID, - info, - language, - 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 }, ) @@ -857,7 +838,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, @@ -893,7 +874,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) } } @@ -901,7 +882,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") } @@ -924,12 +905,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 d3b30575ad..8afac3a65e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,11 +1,11 @@ import type { APICallError, ModelMessage } from "ai" import { unique } from "remeda" import type { JSONSchema } from "zod/v4/core" -import type { ModelsDev } from "./models" +import type { Provider } from "./provider" export namespace ProviderTransform { - function normalizeMessages(msgs: ModelMessage[], providerID: string, model: ModelsDev.Model): ModelMessage[] { - if (model.target.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) => { @@ -21,7 +21,7 @@ export namespace ProviderTransform { return msg }) } - if (providerID === "mistral" || model.target.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] @@ -108,67 +108,68 @@ export namespace ProviderTransform { return msgs } - export function message(msgs: ModelMessage[], providerID: string, model: ModelsDev.Model) { - msgs = normalizeMessages(msgs, providerID, model) - if (providerID === "anthropic" || model.target.includes("anthropic") || model.target.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(model: ModelsDev.Model) { - if (model.target.toLowerCase().includes("qwen")) return 0.55 - if (model.target.toLowerCase().includes("claude")) return undefined - if (model.target.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(model: ModelsDev.Model) { - if (model.target.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, - model: ModelsDev.Model, - npm: string, + model: Provider.Model, sessionID: string, providerOptions?: Record, ): Record { const result: Record = {} // 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") && model.target.includes("gemini-3"))) { + if ( + model.providerID === "google" || + (model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3")) + ) { result["thinkingConfig"] = { includeThoughts: true, } } - if (model.target.includes("gpt-5") && !model.target.includes("gpt-5-chat")) { - if (model.target.includes("codex")) { + if (model.providerID.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) { + if (model.providerID.includes("codex")) { result["store"] = false } - if (!model.target.includes("codex") && !model.target.includes("gpt-5-pro")) { + if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) { result["reasoningEffort"] = "medium" } - if (model.target.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" @@ -177,17 +178,17 @@ export namespace ProviderTransform { return result } - export function smallOptions(input: { providerID: string; model: ModelsDev.Model }) { + export function smallOptions(model: Provider.Model) { const options: Record = {} - if (input.providerID === "openai" || input.model.target.includes("gpt-5")) { - if (input.model.target.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, } @@ -255,7 +256,7 @@ export namespace ProviderTransform { return standardLimit } - export function schema(providerID: string, model: ModelsDev.Model, schema: JSONSchema.BaseSchema) { + export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) { /* if (["openai", "azure"].includes(providerID)) { if (schema.type === "object" && schema.properties) { @@ -275,7 +276,7 @@ export namespace ProviderTransform { */ // Convert integer enums to string enums for Google/Gemini - if (providerID === "google" || model.target.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 4dfd3ac743..31d0822762 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" @@ -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 d4c9eb99a3..b83adafbe3 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -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,8 +126,7 @@ 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({ @@ -139,17 +138,13 @@ export namespace SessionCompaction { // set to 0, we handle loop maxRetries: 0, providerOptions: ProviderTransform.providerOptions( - model.npm, + model.api.npm, model.providerID, - pipe( - {}, - mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm ?? "", input.sessionID)), - mergeDeep(model.info.options), - ), + pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)), ), - headers: model.info.headers, + headers: model.headers, abortSignal: input.abort, - tools: model.info.tool_call ? {} : undefined, + tools: model.capabilities.toolcall ? {} : undefined, messages: [ ...system.map( (x): ModelMessage => ({ @@ -183,7 +178,7 @@ export namespace SessionCompaction { }, ], model: wrapLanguageModel({ - model: model.language, + model: language, middleware: [ { async transformParams(args) { diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index f09818caa2..1e69f7644e 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -6,7 +6,6 @@ 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" @@ -17,6 +16,7 @@ import { fn } from "@/util/fn" import { Command } from "../command" import { Snapshot } from "@/snapshot" import { ShareNext } from "@/share/share-next" +import { Provider } from "@/provider/provider" export namespace Session { const log = Log.create({ service: "session" }) @@ -389,7 +389,7 @@ export namespace Session { export const getUsage = fn( z.object({ - model: z.custom(), + model: Provider.Model, usage: z.custom(), metadata: z.custom().optional(), }), @@ -420,16 +420,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 5823d6191c..21d50abe9d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -1,4 +1,3 @@ -import type { ModelsDev } from "@/provider/models" import { MessageV2 } from "./message-v2" import { streamText } from "ai" import { Log } from "@/util/log" @@ -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 @@ -31,8 +31,7 @@ export namespace SessionProcessor { export function create(input: { assistantMessage: MessageV2.Assistant sessionID: string - providerID: string - model: ModelsDev.Model + model: Provider.Model abort: AbortSignal }) { const toolcalls: Record = {} @@ -341,7 +340,7 @@ export namespace SessionProcessor { log.error("process", { error: e, }) - 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 2433c582b4..17981e4993 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -47,7 +47,6 @@ import { fn } from "@/util/fn" import { SessionProcessor } from "./processor" import { TaskTool } from "@/tool/task" import { SessionStatus } from "./status" -import type { ModelsDev } from "@/provider/models" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -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,21 +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, - model: model.info, + model, agent, system: lastUser.system, }) const tools = await resolveTools({ agent, sessionID, - providerID: model.providerID, - model: model.info, + model, tools: lastUser.tools, processor, }) @@ -487,19 +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.info)) + temperature: model.capabilities.temperature + ? (agent.temperature ?? ProviderTransform.temperature(model)) : undefined, - topP: agent.topP ?? ProviderTransform.topP(model.info), + topP: agent.topP ?? ProviderTransform.topP(model), options: pipe( {}, - mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm, sessionID, provider?.options)), - mergeDeep(model.info.options), + mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)), + mergeDeep(model.options), mergeDeep(agent.options), ), }, @@ -547,19 +544,19 @@ export namespace SessionPrompt { "x-opencode-request": lastUser.id, } : undefined), - ...model.info.headers, + ...model.headers, }, // set to 0, we handle loop maxRetries: 0, activeTools: Object.keys(tools).filter((x) => x !== "invalid"), maxOutputTokens: ProviderTransform.maxOutputTokens( - model.providerID, + model.api.npm, params.options, - model.info.limit.output, + model.limit.output, OUTPUT_TOKEN_MAX, ), abortSignal: abort, - providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options), + providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options), stopWhen: stepCountIs(1), temperature: params.temperature, topP: params.topP, @@ -586,9 +583,9 @@ export namespace SessionPrompt { }), ), ], - tools: model.info.tool_call === false ? undefined : tools, + tools: model.capabilities.toolcall === false ? undefined : tools, model: wrapLanguageModel({ - model: model.language, + model: language, middleware: [ { async transformParams(args) { @@ -604,7 +601,7 @@ export namespace SessionPrompt { // Transform the inputSchema for provider compatibility return { ...tool, - inputSchema: ProviderTransform.schema(model.providerID, model.info, tool.inputSchema), + inputSchema: ProviderTransform.schema(model, tool.inputSchema), } } // If no inputSchema, return tool unchanged @@ -639,13 +636,8 @@ export namespace SessionPrompt { return Provider.defaultModel() } - async function resolveSystemPrompt(input: { - system?: string - agent: Agent.Info - providerID: string - model: ModelsDev.Model - }) { - 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] @@ -663,8 +655,7 @@ export namespace SessionPrompt { async function resolveTools(input: { agent: Agent.Info - providerID: string - model: ModelsDev.Model + model: Provider.Model sessionID: string tools?: Record processor: SessionProcessor.Info @@ -675,9 +666,9 @@ export namespace SessionPrompt { mergeDeep(await ToolRegistry.enabled(input.agent)), mergeDeep(input.tools ?? {}), ) - for (const item of await ToolRegistry.tools(input.providerID)) { + for (const item of await ToolRegistry.tools(input.model.providerID)) { if (Wildcard.all(item.id, enabledTools) === false) continue - const schema = ProviderTransform.schema(input.providerID, input.model, 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, @@ -1428,19 +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.info, small.npm ?? "", input.session.id, provider?.options), - ), - mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })), - 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 => ({ @@ -1471,8 +1461,8 @@ export namespace SessionPrompt { }, ]), ], - headers: small.info.headers, - model: small.language, + headers: small.headers, + model: language, }) .then((result) => { if (result.text) @@ -1489,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 9f56b084e0..8d366e4991 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.info, small.npm ?? "", assistantMsg.sessionID)), - mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })), - 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 42b398948d..3146110cf3 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -17,7 +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 { ModelsDev } from "@/provider/models" +import type { Provider } from "@/provider/provider" export namespace SystemPrompt { export function header(providerID: string) { @@ -25,13 +25,13 @@ export namespace SystemPrompt { return [] } - export function provider(model: ModelsDev.Model) { - if (model.target.includes("gpt-5")) return [PROMPT_CODEX] - if (model.target.includes("gpt-") || model.target.includes("o1") || model.target.includes("o3")) + 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.target.includes("gemini-")) return [PROMPT_GEMINI] - if (model.target.includes("claude")) return [PROMPT_ANTHROPIC] - if (model.target.includes("polaris-alpha")) return [PROMPT_POLARIS] + 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 9543149a81..996400280d 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 }>() @@ -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/read.ts b/packages/opencode/src/tool/read.ts index cf7b20e8b3..7e01246b53 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/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index fcf04444ed..88e3f0f35c 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -942,6 +942,76 @@ export type AgentConfig = { | undefined } +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + target?: 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 + blacklist?: Array + 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,75 +1170,7 @@ export type Config = { * Custom provider configurations and model overrides */ provider?: { - [key: string]: { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { - [key: string]: { - id?: string - name?: string - target?: 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 - blacklist?: Array - 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 @@ -1355,52 +1357,71 @@ export type Command = { export type Model = { id: string + providerID: string + api: { + id: string + url: string + npm: string + } name: string - target: 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 - name: string - env: Array id: string - npm?: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } models: { [key: string]: Model } @@ -2667,7 +2688,56 @@ export type ProviderListResponses = { * List of providers */ 200: { - all: Array + all: Array<{ + api?: string + name: string + env: Array + id: string + npm?: string + models: { + [key: string]: { + id: string + name: string + target: 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 }