provider-cleanup
Dax Raad 2025-12-03 15:14:30 -05:00
parent 8b1c55f9fa
commit 2dbb029472
16 changed files with 365 additions and 333 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" />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
<Show when={i.release_date}>
<Show when={false}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
{DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>

View File

@ -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(),

View File

@ -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)

View File

@ -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<typeof Model>
export const Provider = z
.object({
api: z.string().optional(),
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(z.string(), Model),
})
.meta({
ref: "Provider",
})
export const Provider = z.object({
api: z.string().optional(),
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(z.string(), Model),
})
export type Provider = z.infer<typeof Provider>

View File

@ -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<string, any>) => Promise<any>
type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
getModel?: CustomModelLoader
options?: Record<string, any>
}>
type Source = "env" | "config" | "custom" | "api"
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic() {
return {
@ -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<string, ModelWithStuff>()
const languages = new Map<string, LanguageModelV2>()
const modelLoaders: {
[providerID: string]: CustomModelLoader
} = {}
const sdk = new Map<number, SDK>()
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,
}
}

View File

@ -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<string, any>,
): Record<string, any> {
const result: Record<string, any> = {}
// switch to providerID later, for now use this
if (npm === "@openrouter/ai-sdk-provider") {
if (model.api.npm === "@openrouter/ai-sdk-provider") {
result["usage"] = {
include: true,
}
}
if (providerID === "openai" || providerOptions?.setCacheKey) {
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
result["promptCacheKey"] = sessionID
}
if (providerID === "google" || (providerID.startsWith("opencode") && 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<string, any> = {}
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

View File

@ -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),

View File

@ -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) {

View File

@ -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<ModelsDev.Model>(),
model: Provider.Model,
usage: z.custom<LanguageModelUsage>(),
metadata: z.custom<ProviderMetadata>().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))

View File

@ -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<string, MessageV2.ToolPart> = {}
@ -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++

View File

@ -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<string, boolean>
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 })
})
}
}

View File

@ -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
}

View File

@ -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]
}

View File

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

View File

@ -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) {

View File

@ -942,6 +942,76 @@ export type AgentConfig = {
| undefined
}
export type ProviderConfig = {
api?: string
name?: string
env?: Array<string>
id?: string
npm?: string
models?: {
[key: string]: {
id?: string
name?: string
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 = {
/**
* 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<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
}
}
[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<string>
id: string
npm?: string
name: string
source: "env" | "config" | "custom" | "api"
env: Array<string>
key?: string
options: {
[key: string]: unknown
}
models: {
[key: string]: Model
}
@ -2667,7 +2688,56 @@ export type ProviderListResponses = {
* List of providers
*/
200: {
all: Array<Provider>
all: Array<{
api?: string
name: string
env: Array<string>
id: string
npm?: string
models: {
[key: string]: {
id: string
name: string
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
}