diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6ca8fe55be..1f355b5036 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -470,6 +470,50 @@ export namespace Config { }) export type Layout = z.infer + export const Provider = ModelsDev.Provider.partial() + .extend({ + whitelist: z.array(z.string()).optional(), + blacklist: z.array(z.string()).optional(), + models: z + .record( + z.string(), + ModelsDev.Model.partial().refine( + (input) => input.id === undefined, + "The model.id field can no longer be specified. Use model.target to specify an alternate model id to use when calling the provider.", + ), + ) + .optional(), + options: z + .object({ + apiKey: z.string().optional(), + baseURL: z.string().optional(), + enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), + setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), + timeout: z + .union([ + z + .number() + .int() + .positive() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + z.literal(false).describe("Disable timeout for this provider entirely."), + ]) + .optional() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + }) + .catchall(z.any()) + .optional(), + }) + .strict() + .meta({ + ref: "ProviderConfig", + }) + export type Provider = z.infer + export const Info = z .object({ $schema: z.string().optional().describe("JSON schema reference for configuration validation"), @@ -536,51 +580,7 @@ export namespace Config { .optional() .describe("Agent configuration, see https://opencode.ai/docs/agent"), provider: z - .record( - z.string(), - ModelsDev.Provider.partial() - .extend({ - whitelist: z.array(z.string()).optional(), - blacklist: z.array(z.string()).optional(), - models: z - .record( - z.string(), - ModelsDev.Model.partial().refine( - (input) => input.id === undefined, - "The model.id field can no longer be specified. Use model.target to specify an alternate model id to use when calling the provider.", - ), - ) - .optional(), - options: z - .object({ - apiKey: z.string().optional(), - baseURL: z.string().optional(), - enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), - setCacheKey: z - .boolean() - .optional() - .describe("Enable promptCacheKey for this provider (default false)"), - timeout: z - .union([ - z - .number() - .int() - .positive() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - z.literal(false).describe("Disable timeout for this provider entirely."), - ]) - .optional() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - }) - .catchall(z.any()) - .optional(), - }) - .strict(), - ) + .record(z.string(), Provider) .optional() .describe("Custom provider configurations and model overrides"), mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c7443b2cd0..8341faf3d3 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,7 +1,7 @@ import z from "zod" import fuzzysort from "fuzzysort" import { Config } from "../config/config" -import { entries, mapValues, mergeDeep, pipe, sortBy } from "remeda" +import { mapValues, mergeDeep, sortBy } from "remeda" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" import { Log } from "../util/log" import { BunProc } from "../bun" @@ -360,20 +360,25 @@ export namespace Provider { }) export type Model = z.infer - export const Info = z.object({ - id: z.string(), - name: z.string(), - source: z.enum(["env", "config", "custom", "api"]), - env: z.string().array(), - key: z.string().optional(), - options: z.record(z.string(), z.any()), - models: z.record(z.string(), Model), - }) + export const Info = z + .object({ + id: z.string(), + name: z.string(), + source: z.enum(["env", "config", "custom", "api"]), + env: z.string().array(), + key: z.string().optional(), + options: z.record(z.string(), z.any()), + models: z.record(z.string(), Model), + }) + .meta({ + ref: "Provider", + }) export type Info = z.infer function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { return { id: model.id, + providerID: provider.id, name: model.name, api: { id: model.id, @@ -483,16 +488,16 @@ export namespace Provider { providers[providerID] = mergeDeep(match, provider) } - // TODO: load config + // extend database from config for (const [providerID, provider] of configProviders) { const existing = database[providerID] - const parsed: ModelsDev.Provider = { + const parsed: Info = { id: providerID, - npm: provider.npm ?? existing?.npm, name: provider.name ?? existing?.name ?? providerID, env: provider.env ?? existing?.env ?? [], - api: provider.api ?? existing?.api, - models: existing?.models ?? {}, + options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), + source: "config", + models: {}, } for (const [modelID, model] of Object.entries(provider.models ?? {})) { @@ -504,44 +509,51 @@ export namespace Provider { }) const parsedModel: Model = { id: modelID, - apiID: model.target ?? existing?.target ?? modelID, - status: model.status ?? existing?.status ?? "alpha", + api: { + id: model.id ?? existing?.api.id ?? modelID, + npm: model.provider?.npm ?? provider.npm ?? existing?.api.npm ?? providerID, + url: provider?.api ?? existing?.api.url, + }, + status: model.status ?? existing?.status ?? "active", name, providerID, - npm: model.provider?.npm ?? existing?.provider?.npm ?? provider.npm ?? providerID, - support: { - temperature: model.temperature ?? existing?.temperature ?? false, - reasoning: model.reasoning ?? existing?.reasoning ?? false, - attachment: model.attachment ?? existing?.attachment ?? false, - toolcall: model.tool_call ?? existing?.tool_call ?? true, + capabilities: { + temperature: model.temperature ?? existing?.capabilities.temperature ?? false, + reasoning: model.reasoning ?? existing?.capabilities.reasoning ?? false, + attachment: model.attachment ?? existing?.capabilities.attachment ?? false, + toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true, + input: { + text: model.modalities?.input?.includes("text") ?? false, + audio: model.modalities?.input?.includes("audio") ?? false, + image: model.modalities?.input?.includes("image") ?? false, + video: model.modalities?.input?.includes("video") ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? false, + }, + output: { + text: model.modalities?.output?.includes("text") ?? false, + audio: model.modalities?.output?.includes("audio") ?? false, + image: model.modalities?.output?.includes("image") ?? false, + video: model.modalities?.output?.includes("video") ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? false, + }, }, cost: { input: model?.cost?.input ?? existing?.cost?.input ?? 0, output: model?.cost?.output ?? existing?.cost?.output ?? 0, cache: { - read: model?.cost?.cache_read ?? existing?.cost?.cache_read ?? 0, - write: model?.cost?.cache_write ?? existing?.cost?.cache_write ?? 0, + read: model?.cost?.cache_read ?? existing?.cost?.cache.read ?? 0, + write: model?.cost?.cache_write ?? existing?.cost?.cache.write ?? 0, }, }, - options: { - ...existing?.options, - ...model.options, + options: mergeDeep(existing?.options ?? {}, model.options ?? {}), + limit: { + context: model.limit?.context ?? existing?.limit?.context ?? 0, + output: model.limit?.output ?? existing?.limit?.output ?? 0, }, - limit: model.limit ?? - existing?.limit ?? { - context: 0, - output: 0, - }, - modalities: model.modalities ?? - existing?.modalities ?? { - input: ["text"], - output: ["text"], - }, - headers: model.headers ?? {}, + headers: mergeDeep(existing?.headers ?? {}, model.headers ?? {}), } parsed.models[modelID] = parsedModel } - database[providerID] = parsed }