This commit is contained in:
Dax Raad 2025-12-03 15:14:30 -05:00
commit 2dbb029472
16 changed files with 364 additions and 332 deletions

View file

@ -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" /> <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]"> <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> <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"> <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> </span>
</Show> </Show>
</div> </div>

View file

@ -224,6 +224,7 @@ export namespace Agent {
export async function generate(input: { description: string }) { export async function generate(input: { description: string }) {
const defaultModel = await Provider.defaultModel() const defaultModel = await Provider.defaultModel()
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const language = await Provider.getLanguage(model)
const system = SystemPrompt.header(defaultModel.providerID) const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE) system.push(PROMPT_GENERATE)
const existing = await list() 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`, 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({ schema: z.object({
identifier: z.string(), identifier: z.string(),
whenToUse: z.string(), whenToUse: z.string(),

View file

@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
function printModels(providerID: string, verbose?: boolean) { function printModels(providerID: string, verbose?: boolean) {
const provider = providers[providerID] 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) { for (const [modelID, model] of sortedModels) {
process.stdout.write(`${providerID}/${modelID}`) process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL) process.stdout.write(EOL)

View file

@ -9,63 +9,55 @@ export namespace ModelsDev {
const log = Log.create({ service: "models.dev" }) const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json") const filepath = path.join(Global.Path.cache, "models.json")
export const Model = z export const Model = z.object({
.object({ id: z.string(),
id: z.string(), name: z.string(),
name: z.string(), target: z.string(),
target: z.string(), release_date: z.string(),
release_date: z.string(), attachment: z.boolean(),
attachment: z.boolean(), reasoning: z.boolean(),
reasoning: z.boolean(), temperature: z.boolean(),
temperature: z.boolean(), tool_call: z.boolean(),
tool_call: z.boolean(), cost: z.object({
cost: z.object({ input: z.number(),
input: z.number(), output: z.number(),
output: z.number(), cache_read: z.number().optional(),
cache_read: z.number().optional(), cache_write: z.number().optional(),
cache_write: z.number().optional(), context_over_200k: z
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
.object({ .object({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), input: z.number(),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), output: z.number(),
cache_read: z.number().optional(),
cache_write: z.number().optional(),
}) })
.optional(), .optional(),
experimental: z.boolean().optional(), }),
status: z.enum(["alpha", "beta", "deprecated"]).optional(), limit: z.object({
options: z.record(z.string(), z.any()), context: z.number(),
headers: z.record(z.string(), z.string()).optional(), output: z.number(),
provider: z.object({ npm: z.string() }).optional(), }),
}) modalities: z
.meta({ .object({
ref: "Model", 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 type Model = z.infer<typeof Model>
export const Provider = z export const Provider = z.object({
.object({ api: z.string().optional(),
api: z.string().optional(), name: z.string(),
name: z.string(), env: z.array(z.string()),
env: z.array(z.string()), id: z.string(),
id: z.string(), npm: z.string().optional(),
npm: z.string().optional(), models: z.record(z.string(), Model),
models: z.record(z.string(), Model), })
})
.meta({
ref: "Provider",
})
export type Provider = z.infer<typeof Provider> export type Provider = z.infer<typeof Provider>

View file

@ -23,7 +23,7 @@ import { createVertex } from "@ai-sdk/google-vertex"
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic" import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
import { createOpenAI } from "@ai-sdk/openai" import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible" 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" import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
export namespace Provider { export namespace Provider {
@ -43,14 +43,13 @@ export namespace Provider {
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
} }
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
type CustomLoader = (provider: Info) => Promise<{ type CustomLoader = (provider: Info) => Promise<{
autoload: boolean autoload: boolean
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any> getModel?: CustomModelLoader
options?: Record<string, any> options?: Record<string, any>
}> }>
type Source = "env" | "config" | "custom" | "api"
const CUSTOM_LOADERS: Record<string, CustomLoader> = { const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic() { async anthropic() {
return { return {
@ -314,20 +313,20 @@ export namespace Provider {
reasoning: z.boolean(), reasoning: z.boolean(),
attachment: z.boolean(), attachment: z.boolean(),
toolcall: z.boolean(), toolcall: z.boolean(),
input: { input: z.object({
text: z.boolean(), text: z.boolean(),
audio: z.boolean(), audio: z.boolean(),
image: z.boolean(), image: z.boolean(),
video: z.boolean(), video: z.boolean(),
pdf: z.boolean(), pdf: z.boolean(),
}, }),
output: { output: z.object({
text: z.boolean(), text: z.boolean(),
audio: z.boolean(), audio: z.boolean(),
image: z.boolean(), image: z.boolean(),
video: z.boolean(), video: z.boolean(),
pdf: z.boolean(), pdf: z.boolean(),
}, }),
}), }),
cost: z.object({ cost: z.object({
input: z.number(), input: z.number(),
@ -433,7 +432,7 @@ export namespace Provider {
} }
} }
function fromModelsDevProvider(provider: ModelsDev.Provider): Info { export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
return { return {
id: provider.id, id: provider.id,
source: "custom", source: "custom",
@ -444,11 +443,6 @@ export namespace Provider {
} }
} }
export type ModelWithStuff = {
language: LanguageModel
info: Model
}
const state = Instance.state(async () => { const state = Instance.state(async () => {
using _ = log.time("state") using _ = log.time("state")
const config = await Config.get() const config = await Config.get()
@ -464,7 +458,10 @@ export namespace Provider {
} }
const providers: { [providerID: string]: Info } = {} const providers: { [providerID: string]: Info } = {}
const models = new Map<string, ModelWithStuff>() const languages = new Map<string, LanguageModelV2>()
const modelLoaders: {
[providerID: string]: CustomModelLoader
} = {}
const sdk = new Map<number, SDK>() const sdk = new Map<number, SDK>()
log.info("init") log.info("init")
@ -631,6 +628,7 @@ export namespace Provider {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const result = await fn(database[providerID]) const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) { if (result && (result.autoload || providers[providerID])) {
if (result.getModel) modelLoaders[providerID] = result.getModel
mergeProvider(providerID, { mergeProvider(providerID, {
source: "custom", source: "custom",
options: result.options, options: result.options,
@ -645,7 +643,6 @@ export namespace Provider {
env: provider.env, env: provider.env,
name: provider.name, name: provider.name,
options: provider.options, options: provider.options,
// TODO: merge models
}) })
} }
@ -689,9 +686,10 @@ export namespace Provider {
} }
return { return {
models, models: languages,
providers, providers,
sdk, sdk,
modelLoaders,
} }
}) })
@ -789,15 +787,7 @@ export namespace Provider {
} }
export async function getModel(providerID: string, modelID: string) { export async function getModel(providerID: string, modelID: string) {
const key = `${providerID}/${modelID}`
const s = await state() const s = await state()
if (s.models.has(key)) return s.models.get(key)!
log.info("getModel", {
providerID,
modelID,
})
const provider = s.providers[providerID] const provider = s.providers[providerID]
if (!provider) { if (!provider) {
const availableProviders = Object.keys(s.providers) const availableProviders = Object.keys(s.providers)
@ -813,38 +803,29 @@ export namespace Provider {
const suggestions = matches.map((m) => m.target) const suggestions = matches.map((m) => m.target)
throw new ModelNotFoundError({ providerID, modelID, suggestions }) 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 { try {
const language = provider.getModel const language = s.modelLoaders[model.providerID]
? await provider.getModel(sdk, info.api.id, provider.options) ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
: sdk.languageModel(info.api.id) : sdk.languageModel(model.api.id)
log.info("found", { providerID, modelID }) s.models.set(key, language)
const cached: ModelWithStuff = { return language
info,
language,
}
s.models.set(key, {
providerID,
modelID,
info,
language,
npm,
})
return {
modelID,
providerID,
info,
language,
npm,
}
} catch (e) { } catch (e) {
if (e instanceof NoSuchModelError) if (e instanceof NoSuchModelError)
throw new ModelNotFoundError( throw new ModelNotFoundError(
{ {
modelID: modelID, modelID: model.id,
providerID, providerID: model.providerID,
}, },
{ cause: e }, { cause: e },
) )
@ -857,7 +838,7 @@ export namespace Provider {
const provider = s.providers[providerID] const provider = s.providers[providerID]
if (!provider) return undefined if (!provider) return undefined
for (const item of query) { 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)) if (modelID.includes(item))
return { return {
providerID, providerID,
@ -893,7 +874,7 @@ export namespace Provider {
priority = ["gpt-5-nano"] priority = ["gpt-5-nano"]
} }
for (const item of priority) { 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) if (model.includes(item)) return getModel(providerID, model)
} }
} }
@ -901,7 +882,7 @@ export namespace Provider {
// Check if opencode provider is available before using it // Check if opencode provider is available before using it
const opencodeProvider = await state().then((state) => state.providers["opencode"]) 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") return getModel("opencode", "gpt-5-nano")
} }
@ -924,12 +905,12 @@ export namespace Provider {
const provider = await list() const provider = await list()
.then((val) => Object.values(val)) .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") 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") if (!model) throw new Error("no models found")
return { return {
providerID: provider.info.id, providerID: provider.id,
modelID: model.id, modelID: model.id,
} }
} }

View file

@ -1,11 +1,11 @@
import type { APICallError, ModelMessage } from "ai" import type { APICallError, ModelMessage } from "ai"
import { unique } from "remeda" import { unique } from "remeda"
import type { JSONSchema } from "zod/v4/core" import type { JSONSchema } from "zod/v4/core"
import type { ModelsDev } from "./models" import type { Provider } from "./provider"
export namespace ProviderTransform { export namespace ProviderTransform {
function normalizeMessages(msgs: ModelMessage[], providerID: string, model: ModelsDev.Model): ModelMessage[] { function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
if (model.target.includes("claude")) { if (model.api.id.includes("claude")) {
return msgs.map((msg) => { return msgs.map((msg) => {
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) { if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
msg.content = msg.content.map((part) => { msg.content = msg.content.map((part) => {
@ -21,7 +21,7 @@ export namespace ProviderTransform {
return msg return msg
}) })
} }
if (providerID === "mistral" || model.target.toLowerCase().includes("mistral")) { if (model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral")) {
const result: ModelMessage[] = [] const result: ModelMessage[] = []
for (let i = 0; i < msgs.length; i++) { for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i] const msg = msgs[i]
@ -108,67 +108,68 @@ export namespace ProviderTransform {
return msgs return msgs
} }
export function message(msgs: ModelMessage[], providerID: string, model: ModelsDev.Model) { export function message(msgs: ModelMessage[], model: Provider.Model) {
msgs = normalizeMessages(msgs, providerID, model) msgs = normalizeMessages(msgs, model)
if (providerID === "anthropic" || model.target.includes("anthropic") || model.target.includes("claude")) { if (model.providerID === "anthropic" || model.api.id.includes("anthropic") || model.api.id.includes("claude")) {
msgs = applyCaching(msgs, providerID) msgs = applyCaching(msgs, model.providerID)
} }
return msgs return msgs
} }
export function temperature(model: ModelsDev.Model) { export function temperature(model: Provider.Model) {
if (model.target.toLowerCase().includes("qwen")) return 0.55 if (model.api.id.toLowerCase().includes("qwen")) return 0.55
if (model.target.toLowerCase().includes("claude")) return undefined if (model.api.id.toLowerCase().includes("claude")) return undefined
if (model.target.toLowerCase().includes("gemini-3-pro")) return 1.0 if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
return 0 return 0
} }
export function topP(model: ModelsDev.Model) { export function topP(model: Provider.Model) {
if (model.target.toLowerCase().includes("qwen")) return 1 if (model.api.id.toLowerCase().includes("qwen")) return 1
return undefined return undefined
} }
export function options( export function options(
providerID: string, model: Provider.Model,
model: ModelsDev.Model,
npm: string,
sessionID: string, sessionID: string,
providerOptions?: Record<string, any>, providerOptions?: Record<string, any>,
): Record<string, any> { ): Record<string, any> {
const result: Record<string, any> = {} const result: Record<string, any> = {}
// switch to providerID later, for now use this // switch to providerID later, for now use this
if (npm === "@openrouter/ai-sdk-provider") { if (model.api.npm === "@openrouter/ai-sdk-provider") {
result["usage"] = { result["usage"] = {
include: true, include: true,
} }
} }
if (providerID === "openai" || providerOptions?.setCacheKey) { if (model.providerID === "openai" || providerOptions?.setCacheKey) {
result["promptCacheKey"] = sessionID 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"] = { result["thinkingConfig"] = {
includeThoughts: true, includeThoughts: true,
} }
} }
if (model.target.includes("gpt-5") && !model.target.includes("gpt-5-chat")) { if (model.providerID.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
if (model.target.includes("codex")) { if (model.providerID.includes("codex")) {
result["store"] = false 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" 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" result["textVerbosity"] = "low"
} }
if (providerID.startsWith("opencode")) { if (model.providerID.startsWith("opencode")) {
result["promptCacheKey"] = sessionID result["promptCacheKey"] = sessionID
result["include"] = ["reasoning.encrypted_content"] result["include"] = ["reasoning.encrypted_content"]
result["reasoningSummary"] = "auto" result["reasoningSummary"] = "auto"
@ -177,17 +178,17 @@ export namespace ProviderTransform {
return result return result
} }
export function smallOptions(input: { providerID: string; model: ModelsDev.Model }) { export function smallOptions(model: Provider.Model) {
const options: Record<string, any> = {} const options: Record<string, any> = {}
if (input.providerID === "openai" || input.model.target.includes("gpt-5")) { if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
if (input.model.target.includes("5.1")) { if (model.api.id.includes("5.1")) {
options["reasoningEffort"] = "low" options["reasoningEffort"] = "low"
} else { } else {
options["reasoningEffort"] = "minimal" options["reasoningEffort"] = "minimal"
} }
} }
if (input.providerID === "google") { if (model.providerID === "google") {
options["thinkingConfig"] = { options["thinkingConfig"] = {
thinkingBudget: 0, thinkingBudget: 0,
} }
@ -255,7 +256,7 @@ export namespace ProviderTransform {
return standardLimit 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 (["openai", "azure"].includes(providerID)) {
if (schema.type === "object" && schema.properties) { if (schema.type === "object" && schema.properties) {
@ -275,7 +276,7 @@ export namespace ProviderTransform {
*/ */
// Convert integer enums to string enums for Google/Gemini // 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 => { const sanitizeGemini = (obj: any): any => {
if (obj === null || typeof obj !== "object") { if (obj === null || typeof obj !== "object") {
return obj return obj

View file

@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
import { Session } from "../session" import { Session } from "../session"
import z from "zod" import z from "zod"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { mapValues } from "remeda" import { mapValues, pipe } from "remeda"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "../provider/models" import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
@ -1025,7 +1025,7 @@ export namespace Server {
async (c) => { async (c) => {
c.status(204) c.status(204)
c.header("Content-Type", "application/json") c.header("Content-Type", "application/json")
return stream(c, async (stream) => { return stream(c, async () => {
const sessionID = c.req.valid("param").id const sessionID = c.req.valid("param").id
const body = c.req.valid("json") const body = c.req.valid("json")
SessionPrompt.prompt({ ...body, sessionID }) SessionPrompt.prompt({ ...body, sessionID })
@ -1231,7 +1231,7 @@ export namespace Server {
"application/json": { "application/json": {
schema: resolver( schema: resolver(
z.object({ z.object({
providers: ModelsDev.Provider.array(), providers: Provider.Info.array(),
default: z.record(z.string(), z.string()), default: z.record(z.string(), z.string()),
}), }),
), ),
@ -1242,7 +1242,7 @@ export namespace Server {
}), }),
async (c) => { async (c) => {
using _ = log.time("providers") 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({ return c.json({
providers: Object.values(providers), providers: Object.values(providers),
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
@ -1272,7 +1272,10 @@ export namespace Server {
}, },
}), }),
async (c) => { 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)) const connected = await Provider.list().then((x) => Object.keys(x))
return c.json({ return c.json({
all: Object.values(providers), all: Object.values(providers),

View file

@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system" import { SystemPrompt } from "./system"
import { Bus } from "../bus" import { Bus } from "../bus"
import z from "zod" import z from "zod"
import type { ModelsDev } from "../provider/models"
import { SessionPrompt } from "./prompt" import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Token } from "../util/token" 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 if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
const context = input.model.limit.context const context = input.model.limit.context
if (context === 0) return false if (context === 0) return false
@ -98,6 +97,7 @@ export namespace SessionCompaction {
auto: boolean auto: boolean
}) { }) {
const model = await Provider.getModel(input.model.providerID, input.model.modelID) const model = await Provider.getModel(input.model.providerID, input.model.modelID)
const language = await Provider.getLanguage(model)
const system = [...SystemPrompt.compaction(model.providerID)] const system = [...SystemPrompt.compaction(model.providerID)]
const msg = (await Session.updateMessage({ const msg = (await Session.updateMessage({
id: Identifier.ascending("message"), id: Identifier.ascending("message"),
@ -126,8 +126,7 @@ export namespace SessionCompaction {
const processor = SessionProcessor.create({ const processor = SessionProcessor.create({
assistantMessage: msg, assistantMessage: msg,
sessionID: input.sessionID, sessionID: input.sessionID,
providerID: input.model.providerID, model: model,
model: model.info,
abort: input.abort, abort: input.abort,
}) })
const result = await processor.process({ const result = await processor.process({
@ -139,17 +138,13 @@ export namespace SessionCompaction {
// set to 0, we handle loop // set to 0, we handle loop
maxRetries: 0, maxRetries: 0,
providerOptions: ProviderTransform.providerOptions( providerOptions: ProviderTransform.providerOptions(
model.npm, model.api.npm,
model.providerID, model.providerID,
pipe( pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
{},
mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm ?? "", input.sessionID)),
mergeDeep(model.info.options),
),
), ),
headers: model.info.headers, headers: model.headers,
abortSignal: input.abort, abortSignal: input.abort,
tools: model.info.tool_call ? {} : undefined, tools: model.capabilities.toolcall ? {} : undefined,
messages: [ messages: [
...system.map( ...system.map(
(x): ModelMessage => ({ (x): ModelMessage => ({
@ -183,7 +178,7 @@ export namespace SessionCompaction {
}, },
], ],
model: wrapLanguageModel({ model: wrapLanguageModel({
model: model.language, model: language,
middleware: [ middleware: [
{ {
async transformParams(args) { async transformParams(args) {

View file

@ -6,7 +6,6 @@ import { Config } from "../config/config"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Identifier } from "../id/id" import { Identifier } from "../id/id"
import { Installation } from "../installation" import { Installation } from "../installation"
import type { ModelsDev } from "../provider/models"
import { Share } from "../share/share" import { Share } from "../share/share"
import { Storage } from "../storage/storage" import { Storage } from "../storage/storage"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -17,6 +16,7 @@ import { fn } from "@/util/fn"
import { Command } from "../command" import { Command } from "../command"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
import { Provider } from "@/provider/provider"
export namespace Session { export namespace Session {
const log = Log.create({ service: "session" }) const log = Log.create({ service: "session" })
@ -389,7 +389,7 @@ export namespace Session {
export const getUsage = fn( export const getUsage = fn(
z.object({ z.object({
model: z.custom<ModelsDev.Model>(), model: Provider.Model,
usage: z.custom<LanguageModelUsage>(), usage: z.custom<LanguageModelUsage>(),
metadata: z.custom<ProviderMetadata>().optional(), metadata: z.custom<ProviderMetadata>().optional(),
}), }),
@ -420,16 +420,16 @@ export namespace Session {
} }
const costInfo = const costInfo =
input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000 input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
? input.model.cost.context_over_200k ? input.model.cost.experimentalOver200K
: input.model.cost : input.model.cost
return { return {
cost: safe( cost: safe(
new Decimal(0) new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) .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.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.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.write).mul(costInfo?.cache.write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now: // TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens // charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))

View file

@ -1,4 +1,3 @@
import type { ModelsDev } from "@/provider/models"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { streamText } from "ai" import { streamText } from "ai"
import { Log } from "@/util/log" import { Log } from "@/util/log"
@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { SessionRetry } from "./retry" import { SessionRetry } from "./retry"
import { SessionStatus } from "./status" import { SessionStatus } from "./status"
import type { Provider } from "@/provider/provider"
export namespace SessionProcessor { export namespace SessionProcessor {
const DOOM_LOOP_THRESHOLD = 3 const DOOM_LOOP_THRESHOLD = 3
@ -31,8 +31,7 @@ export namespace SessionProcessor {
export function create(input: { export function create(input: {
assistantMessage: MessageV2.Assistant assistantMessage: MessageV2.Assistant
sessionID: string sessionID: string
providerID: string model: Provider.Model
model: ModelsDev.Model
abort: AbortSignal abort: AbortSignal
}) { }) {
const toolcalls: Record<string, MessageV2.ToolPart> = {} const toolcalls: Record<string, MessageV2.ToolPart> = {}
@ -341,7 +340,7 @@ export namespace SessionProcessor {
log.error("process", { log.error("process", {
error: e, error: e,
}) })
const error = MessageV2.fromError(e, { providerID: input.providerID }) const error = MessageV2.fromError(e, { providerID: input.sessionID })
const retry = SessionRetry.retryable(error) const retry = SessionRetry.retryable(error)
if (retry !== undefined) { if (retry !== undefined) {
attempt++ attempt++

View file

@ -47,7 +47,6 @@ import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor" import { SessionProcessor } from "./processor"
import { TaskTool } from "@/tool/task" import { TaskTool } from "@/tool/task"
import { SessionStatus } from "./status" import { SessionStatus } from "./status"
import type { ModelsDev } from "@/provider/models"
// @ts-ignore // @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false 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 model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
const language = await Provider.getLanguage(model)
const task = tasks.pop() const task = tasks.pop()
// pending subtask // pending subtask
@ -311,7 +311,7 @@ export namespace SessionPrompt {
reasoning: 0, reasoning: 0,
cache: { read: 0, write: 0 }, cache: { read: 0, write: 0 },
}, },
modelID: model.modelID, modelID: model.id,
providerID: model.providerID, providerID: model.providerID,
time: { time: {
created: Date.now(), created: Date.now(),
@ -408,7 +408,7 @@ export namespace SessionPrompt {
agent: lastUser.agent, agent: lastUser.agent,
model: { model: {
providerID: model.providerID, providerID: model.providerID,
modelID: model.modelID, modelID: model.id,
}, },
sessionID, sessionID,
auto: task.auto, auto: task.auto,
@ -421,7 +421,7 @@ export namespace SessionPrompt {
if ( if (
lastFinished && lastFinished &&
lastFinished.summary !== true && lastFinished.summary !== true &&
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info }) SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
) { ) {
await SessionCompaction.create({ await SessionCompaction.create({
sessionID, sessionID,
@ -455,7 +455,7 @@ export namespace SessionPrompt {
reasoning: 0, reasoning: 0,
cache: { read: 0, write: 0 }, cache: { read: 0, write: 0 },
}, },
modelID: model.modelID, modelID: model.id,
providerID: model.providerID, providerID: model.providerID,
time: { time: {
created: Date.now(), created: Date.now(),
@ -463,21 +463,18 @@ export namespace SessionPrompt {
sessionID, sessionID,
})) as MessageV2.Assistant, })) as MessageV2.Assistant,
sessionID: sessionID, sessionID: sessionID,
model: model.info, model,
providerID: model.providerID,
abort, abort,
}) })
const system = await resolveSystemPrompt({ const system = await resolveSystemPrompt({
providerID: model.providerID, model,
model: model.info,
agent, agent,
system: lastUser.system, system: lastUser.system,
}) })
const tools = await resolveTools({ const tools = await resolveTools({
agent, agent,
sessionID, sessionID,
providerID: model.providerID, model,
model: model.info,
tools: lastUser.tools, tools: lastUser.tools,
processor, processor,
}) })
@ -487,19 +484,19 @@ export namespace SessionPrompt {
{ {
sessionID: sessionID, sessionID: sessionID,
agent: lastUser.agent, agent: lastUser.agent,
model: model.info, model: model,
provider, provider,
message: lastUser, message: lastUser,
}, },
{ {
temperature: model.info.temperature temperature: model.capabilities.temperature
? (agent.temperature ?? ProviderTransform.temperature(model.info)) ? (agent.temperature ?? ProviderTransform.temperature(model))
: undefined, : undefined,
topP: agent.topP ?? ProviderTransform.topP(model.info), topP: agent.topP ?? ProviderTransform.topP(model),
options: pipe( options: pipe(
{}, {},
mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm, sessionID, provider?.options)), mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
mergeDeep(model.info.options), mergeDeep(model.options),
mergeDeep(agent.options), mergeDeep(agent.options),
), ),
}, },
@ -547,19 +544,19 @@ export namespace SessionPrompt {
"x-opencode-request": lastUser.id, "x-opencode-request": lastUser.id,
} }
: undefined), : undefined),
...model.info.headers, ...model.headers,
}, },
// set to 0, we handle loop // set to 0, we handle loop
maxRetries: 0, maxRetries: 0,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"), activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: ProviderTransform.maxOutputTokens( maxOutputTokens: ProviderTransform.maxOutputTokens(
model.providerID, model.api.npm,
params.options, params.options,
model.info.limit.output, model.limit.output,
OUTPUT_TOKEN_MAX, OUTPUT_TOKEN_MAX,
), ),
abortSignal: abort, abortSignal: abort,
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options), providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
stopWhen: stepCountIs(1), stopWhen: stepCountIs(1),
temperature: params.temperature, temperature: params.temperature,
topP: params.topP, 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: wrapLanguageModel({
model: model.language, model: language,
middleware: [ middleware: [
{ {
async transformParams(args) { async transformParams(args) {
@ -604,7 +601,7 @@ export namespace SessionPrompt {
// Transform the inputSchema for provider compatibility // Transform the inputSchema for provider compatibility
return { return {
...tool, ...tool,
inputSchema: ProviderTransform.schema(model.providerID, model.info, tool.inputSchema), inputSchema: ProviderTransform.schema(model, tool.inputSchema),
} }
} }
// If no inputSchema, return tool unchanged // If no inputSchema, return tool unchanged
@ -639,13 +636,8 @@ export namespace SessionPrompt {
return Provider.defaultModel() return Provider.defaultModel()
} }
async function resolveSystemPrompt(input: { async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
system?: string let system = SystemPrompt.header(input.model.providerID)
agent: Agent.Info
providerID: string
model: ModelsDev.Model
}) {
let system = SystemPrompt.header(input.providerID)
system.push( system.push(
...(() => { ...(() => {
if (input.system) return [input.system] if (input.system) return [input.system]
@ -663,8 +655,7 @@ export namespace SessionPrompt {
async function resolveTools(input: { async function resolveTools(input: {
agent: Agent.Info agent: Agent.Info
providerID: string model: Provider.Model
model: ModelsDev.Model
sessionID: string sessionID: string
tools?: Record<string, boolean> tools?: Record<string, boolean>
processor: SessionProcessor.Info processor: SessionProcessor.Info
@ -675,9 +666,9 @@ export namespace SessionPrompt {
mergeDeep(await ToolRegistry.enabled(input.agent)), mergeDeep(await ToolRegistry.enabled(input.agent)),
mergeDeep(input.tools ?? {}), 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 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({ tools[item.id] = tool({
id: item.id as any, id: item.id as any,
description: item.description, description: item.description,
@ -1428,19 +1419,18 @@ export namespace SessionPrompt {
if (!isFirst) return if (!isFirst) return
const small = const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID)) (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 provider = await Provider.getProvider(small.providerID)
const options = pipe( const options = pipe(
{}, {},
mergeDeep( mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
ProviderTransform.options(small.providerID, small.info, small.npm ?? "", input.session.id, provider?.options), mergeDeep(ProviderTransform.smallOptions(small)),
), mergeDeep(small.options),
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })),
mergeDeep(small.info.options),
) )
await generateText({ await generateText({
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget // use higher # for reasoning models since reasoning tokens eat up a lot of the budget
maxOutputTokens: small.info.reasoning ? 3000 : 20, maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options), providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [ messages: [
...SystemPrompt.title(small.providerID).map( ...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({ (x): ModelMessage => ({
@ -1471,8 +1461,8 @@ export namespace SessionPrompt {
}, },
]), ]),
], ],
headers: small.info.headers, headers: small.headers,
model: small.language, model: language,
}) })
.then((result) => { .then((result) => {
if (result.text) if (result.text)
@ -1489,7 +1479,7 @@ export namespace SessionPrompt {
}) })
}) })
.catch((error) => { .catch((error) => {
log.error("failed to generate title", { error, model: small.info.id }) log.error("failed to generate title", { error, model: small.id })
}) })
} }
} }

View file

@ -76,19 +76,20 @@ export namespace SessionSummary {
const small = const small =
(await Provider.getSmallModel(assistantMsg.providerID)) ?? (await Provider.getSmallModel(assistantMsg.providerID)) ??
(await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID)) (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
const language = await Provider.getLanguage(small)
const options = pipe( const options = pipe(
{}, {},
mergeDeep(ProviderTransform.options(small.providerID, small.info, small.npm ?? "", assistantMsg.sessionID)), mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)),
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })), mergeDeep(ProviderTransform.smallOptions(small)),
mergeDeep(small.info.options), mergeDeep(small.options),
) )
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) { if (textPart && !userMsg.summary?.title) {
const result = await generateText({ const result = await generateText({
maxOutputTokens: small.info.reasoning ? 1500 : 20, maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options), providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [ messages: [
...SystemPrompt.title(small.providerID).map( ...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({ (x): ModelMessage => ({
@ -106,8 +107,8 @@ export namespace SessionSummary {
`, `,
}, },
], ],
headers: small.info.headers, headers: small.headers,
model: small.language, model: language,
}) })
log.info("title", { title: result.text }) log.info("title", { title: result.text })
userMsg.summary.title = result.text userMsg.summary.title = result.text
@ -132,9 +133,9 @@ export namespace SessionSummary {
} }
} }
const result = await generateText({ const result = await generateText({
model: small.language, model: language,
maxOutputTokens: 100, maxOutputTokens: 100,
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options), providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
messages: [ messages: [
...SystemPrompt.summarize(small.providerID).map( ...SystemPrompt.summarize(small.providerID).map(
(x): ModelMessage => ({ (x): ModelMessage => ({
@ -148,7 +149,7 @@ export namespace SessionSummary {
content: `Summarize the above conversation according to your system prompts.`, content: `Summarize the above conversation according to your system prompts.`,
}, },
], ],
headers: small.info.headers, headers: small.headers,
}).catch(() => {}) }).catch(() => {})
if (result) summary = result.text if (result) summary = result.text
} }

View file

@ -17,7 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt" import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_CODEX from "./prompt/codex.txt" import PROMPT_CODEX from "./prompt/codex.txt"
import type { ModelsDev } from "@/provider/models" import type { Provider } from "@/provider/provider"
export namespace SystemPrompt { export namespace SystemPrompt {
export function header(providerID: string) { export function header(providerID: string) {
@ -25,13 +25,13 @@ export namespace SystemPrompt {
return [] return []
} }
export function provider(model: ModelsDev.Model) { export function provider(model: Provider.Model) {
if (model.target.includes("gpt-5")) return [PROMPT_CODEX] if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
if (model.target.includes("gpt-") || model.target.includes("o1") || model.target.includes("o3")) if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
return [PROMPT_BEAST] return [PROMPT_BEAST]
if (model.target.includes("gemini-")) return [PROMPT_GEMINI] if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
if (model.target.includes("claude")) return [PROMPT_ANTHROPIC] if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
if (model.target.includes("polaris-alpha")) return [PROMPT_POLARIS] if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS]
return [PROMPT_ANTHROPIC_WITHOUT_TODO] return [PROMPT_ANTHROPIC_WITHOUT_TODO]
} }

View file

@ -1,7 +1,6 @@
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { ulid } from "ulid" import { ulid } from "ulid"
import type { ModelsDev } from "@/provider/models"
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { Session } from "@/session" import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2" import { MessageV2 } from "@/session/message-v2"
@ -36,7 +35,7 @@ export namespace ShareNext {
type: "model", type: "model",
data: [ data: [
await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then( 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" type: "model"
data: ModelsDev.Model[] data: SDK.Model[]
} }
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>() const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
@ -171,7 +170,7 @@ export namespace ShareNext {
messages messages
.filter((m) => m.info.role === "user") .filter((m) => m.info.role === "user")
.map((m) => (m.info as SDK.UserMessage).model) .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, [ await sync(sessionID, [
{ {

View file

@ -101,7 +101,7 @@ export const ReadTool = Tool.define("read", {
const modelID = ctx.extra["modelID"] as string const modelID = ctx.extra["modelID"] as string
const model = await Provider.getModel(providerID, modelID).catch(() => undefined) const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
if (!model) return false if (!model) return false
return model.info.modalities?.input?.includes("image") ?? false return model.capabilities.input.image
})() })()
if (isImage) { if (isImage) {
if (!supportsImages) { if (!supportsImages) {

View file

@ -942,6 +942,76 @@ export type AgentConfig = {
| undefined | undefined
} }
export type ProviderConfig = {
api?: string
name?: string
env?: Array<string>
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<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 = { export type McpLocalConfig = {
/** /**
* Type of MCP server connection * Type of MCP server connection
@ -1100,75 +1170,7 @@ export type Config = {
* Custom provider configurations and model overrides * Custom provider configurations and model overrides
*/ */
provider?: { provider?: {
[key: string]: { [key: string]: ProviderConfig
api?: string
name?: string
env?: Array<string>
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<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
}
}
} }
/** /**
* MCP (Model Context Protocol) server configurations * MCP (Model Context Protocol) server configurations
@ -1355,52 +1357,71 @@ export type Command = {
export type Model = { export type Model = {
id: string id: string
providerID: string
api: {
id: string
url: string
npm: string
}
name: string name: string
target: string capabilities: {
release_date: string temperature: boolean
attachment: boolean reasoning: boolean
reasoning: boolean attachment: boolean
temperature: boolean toolcall: boolean
tool_call: boolean input: {
text: boolean
audio: boolean
image: boolean
video: boolean
pdf: boolean
}
output: {
text: boolean
audio: boolean
image: boolean
video: boolean
pdf: boolean
}
}
cost: { cost: {
input: number input: number
output: number output: number
cache_read?: number cache: {
cache_write?: number read: number
context_over_200k?: { write: number
}
experimentalOver200K?: {
input: number input: number
output: number output: number
cache_read?: number cache: {
cache_write?: number read: number
write: number
}
} }
} }
limit: { limit: {
context: number context: number
output: number output: number
} }
modalities?: { status: "alpha" | "beta" | "deprecated" | "active"
input: Array<"text" | "audio" | "image" | "video" | "pdf">
output: Array<"text" | "audio" | "image" | "video" | "pdf">
}
experimental?: boolean
status?: "alpha" | "beta" | "deprecated"
options: { options: {
[key: string]: unknown [key: string]: unknown
} }
headers?: { headers: {
[key: string]: string [key: string]: string
} }
provider?: {
npm: string
}
} }
export type Provider = { export type Provider = {
api?: string
name: string
env: Array<string>
id: string id: string
npm?: string name: string
source: "env" | "config" | "custom" | "api"
env: Array<string>
key?: string
options: {
[key: string]: unknown
}
models: { models: {
[key: string]: Model [key: string]: Model
} }
@ -2667,7 +2688,56 @@ export type ProviderListResponses = {
* List of providers * List of providers
*/ */
200: { 200: {
all: Array<Provider> all: Array<{
api?: string
name: string
env: Array<string>
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: { default: {
[key: string]: string [key: string]: string
} }