diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 8c98b8487d..3a9a7d9569 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -5,12 +5,16 @@ import { LLMResponse, Message, ModelCapabilities, + ModelID, ModelLimits, ModelRef, + ProviderID, ToolChoice, ToolDefinition, type ContentPart, type Protocol, + type ModelID as ModelIDType, + type ProviderID as ProviderIDType, type ReasoningEffort, type SystemPart, type ToolCallPart, @@ -28,7 +32,9 @@ export type CapabilitiesInput = { } } -export type ModelInput = Omit[0], "capabilities" | "limits"> & { +export type ModelInput = Omit[0], "id" | "provider" | "capabilities" | "limits"> & { + readonly id: string | ModelIDType + readonly provider: string | ProviderIDType readonly capabilities?: ModelCapabilities | CapabilitiesInput readonly limits?: ModelLimits | ConstructorParameters[0] } @@ -98,6 +104,8 @@ export const model = (input: ModelInput) => { const { capabilities: modelCapabilities, limits: modelLimits, ...rest } = input return new ModelRef({ ...rest, + id: ModelID.make(input.id), + provider: ProviderID.make(input.provider), protocol: input.protocol as Protocol, capabilities: modelCapabilities instanceof ModelCapabilities ? modelCapabilities : capabilities(modelCapabilities), limits: modelLimits instanceof ModelLimits ? modelLimits : limits(modelLimits), diff --git a/packages/llm/src/provider-route.ts b/packages/llm/src/provider-route.ts index c4b757391e..6875a86a57 100644 --- a/packages/llm/src/provider-route.ts +++ b/packages/llm/src/provider-route.ts @@ -1,28 +1,42 @@ -import type { Protocol } from "./schema" +import { ModelID, ProviderID, type Protocol } from "./schema" +import type { ModelID as ModelIDType, ProviderID as ProviderIDType } from "./schema" export interface ProviderRoute { - readonly provider: string + readonly provider: ProviderIDType readonly protocol: Protocol } export interface ProviderRouteInput { - readonly modelID: string - readonly providerID: string + readonly modelID: ModelIDType + readonly providerID: ProviderIDType readonly options: Record } export interface ProviderDefinition { - readonly id: string + readonly id: ProviderIDType readonly route: (input: ProviderRouteInput) => ProviderRoute | undefined } -export const make = (provider: string, protocol: Protocol): ProviderRoute => ({ provider, protocol }) +export const make = (provider: string | ProviderIDType, protocol: Protocol): ProviderRoute => ({ + provider: ProviderID.make(provider), + protocol, +}) export const define = (input: ProviderDefinition): ProviderDefinition => input -export const fixed = (provider: string, protocol: Protocol): ProviderDefinition => { +export const fixed = (provider: string | ProviderIDType, protocol: Protocol): ProviderDefinition => { const route = make(provider, protocol) - return define({ id: provider, route: () => route }) + return define({ id: route.provider, route: () => route }) } +export const input = ( + modelID: string | ModelIDType, + providerID: string | ProviderIDType, + options: Record, +): ProviderRouteInput => ({ + modelID: ModelID.make(modelID), + providerID: ProviderID.make(providerID), + options, +}) + export * as ProviderRoute from "./provider-route" diff --git a/packages/llm/src/provider/azure.ts b/packages/llm/src/provider/azure.ts index c1f30b8cbc..981c0d16f3 100644 --- a/packages/llm/src/provider/azure.ts +++ b/packages/llm/src/provider/azure.ts @@ -1,6 +1,7 @@ import { ProviderRoute } from "../provider-route" +import { ProviderID } from "../schema" -export const id = "azure" +export const id = ProviderID.make("azure") export const provider = ProviderRoute.define({ id, diff --git a/packages/llm/src/provider/github-copilot.ts b/packages/llm/src/provider/github-copilot.ts index 5e5992b9d3..d1aee79769 100644 --- a/packages/llm/src/provider/github-copilot.ts +++ b/packages/llm/src/provider/github-copilot.ts @@ -1,6 +1,7 @@ import { ProviderRoute } from "../provider-route" +import { ProviderID } from "../schema" -export const id = "github-copilot" +export const id = ProviderID.make("github-copilot") export const shouldUseResponsesApi = (modelID: string) => { const match = /^gpt-(\d+)/.exec(modelID) diff --git a/packages/llm/src/provider/openai-compatible-family.ts b/packages/llm/src/provider/openai-compatible-family.ts index c06116970a..52e0bb95d0 100644 --- a/packages/llm/src/provider/openai-compatible-family.ts +++ b/packages/llm/src/provider/openai-compatible-family.ts @@ -21,7 +21,7 @@ export const byProvider: Record = Object.fromEntries( export const route = (provider: string) => ProviderRoute.make(provider, "openai-compatible-chat") export const provider = ProviderRoute.define({ - id: "openai-compatible", + id: ProviderRoute.make("openai-compatible", "openai-compatible-chat").provider, route: (input) => route(input.providerID), }) diff --git a/packages/llm/src/schema.ts b/packages/llm/src/schema.ts index 4e9d142df4..24fb83fa3b 100644 --- a/packages/llm/src/schema.ts +++ b/packages/llm/src/schema.ts @@ -10,6 +10,12 @@ export const Protocol = Schema.Literals([ ]) export type Protocol = Schema.Schema.Type +export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID")) +export type ModelID = typeof ModelID.Type + +export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID")) +export type ProviderID = typeof ProviderID.Type + export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const export const ReasoningEffort = Schema.Literals(ReasoningEfforts) export type ReasoningEffort = Schema.Schema.Type @@ -61,8 +67,8 @@ export class ModelLimits extends Schema.Class("LLM.ModelLimits")({ }) {} export class ModelRef extends Schema.Class("LLM.ModelRef")({ - id: Schema.String, - provider: Schema.String, + id: ModelID, + provider: ProviderID, protocol: Protocol, baseURL: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), @@ -351,8 +357,8 @@ export class InvalidRequestError extends Schema.TaggedErrorClass()("LLM.NoAdapterError", { protocol: Protocol, - provider: Schema.String, - model: Schema.String, + provider: ProviderID, + model: ModelID, }) { override get message() { return `No LLM adapter for ${this.provider}/${this.model} using ${this.protocol}` diff --git a/packages/llm/test/provider/openai-compatible-chat.test.ts b/packages/llm/test/provider/openai-compatible-chat.test.ts index 8e802bbc68..bd3fb44be4 100644 --- a/packages/llm/test/provider/openai-compatible-chat.test.ts +++ b/packages/llm/test/provider/openai-compatible-chat.test.ts @@ -91,8 +91,8 @@ describe("OpenAI-compatible Chat adapter", () => { providerFamilies.map(([provider, makeModel, baseURL]) => { const model = makeModel({ id: `${provider}-model`, apiKey: "test-key" }) return { - id: model.id, - provider: model.provider, + id: String(model.id), + provider: String(model.provider), protocol: model.protocol, baseURL: model.baseURL, headers: model.headers, diff --git a/packages/llm/test/schema.test.ts b/packages/llm/test/schema.test.ts index 30e12c4ba8..d80acfa5e9 100644 --- a/packages/llm/test/schema.test.ts +++ b/packages/llm/test/schema.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { ContentPart, LLMEvent, LLMRequest, ModelCapabilities, ModelLimits, ModelRef } from "../src/schema" +import { ContentPart, LLMEvent, LLMRequest, ModelCapabilities, ModelID, ModelLimits, ModelRef, ProviderID } from "../src/schema" const capabilities = new ModelCapabilities({ input: { text: true, image: false, audio: false, video: false, pdf: false }, @@ -11,8 +11,8 @@ const capabilities = new ModelCapabilities({ }) const model = new ModelRef({ - id: "fake-model", - provider: "fake-provider", + id: ModelID.make("fake-model"), + provider: ProviderID.make("fake-provider"), protocol: "openai-chat", capabilities, limits: new ModelLimits({}), diff --git a/packages/opencode/src/provider/llm-bridge.ts b/packages/opencode/src/provider/llm-bridge.ts index a666f92bed..b756494143 100644 --- a/packages/opencode/src/provider/llm-bridge.ts +++ b/packages/opencode/src/provider/llm-bridge.ts @@ -6,7 +6,8 @@ import { Google } from "@opencode-ai/llm/provider/google" import { OpenAI } from "@opencode-ai/llm/provider/openai" import { OpenAICompatibleFamily } from "@opencode-ai/llm/provider/openai-compatible-family" import { XAI } from "@opencode-ai/llm/provider/xai" -import type { ProviderDefinition, ProviderRoute } from "@opencode-ai/llm/provider-route" +import { ProviderRoute } from "@opencode-ai/llm/provider-route" +import type { ProviderDefinition, ProviderRoute as ProviderRouteType } from "@opencode-ai/llm/provider-route" import { ReasoningEfforts, type ModelRef, type Protocol, type ReasoningEffort } from "@opencode-ai/llm/schema" import { isRecord } from "@/util/record" import type * as Provider from "./provider" @@ -48,12 +49,8 @@ const recordOption = (options: Record, key: string): Record = { ...input.provider.options, ...input.model.options }, -): ProviderRoute | undefined => - PROVIDERS[input.model.api.npm]?.route({ - modelID: input.model.api.id, - providerID: input.model.providerID, - options, - }) +): ProviderRouteType | undefined => + PROVIDERS[input.model.api.npm]?.route(ProviderRoute.input(input.model.api.id, input.model.providerID, options)) const baseURL = (input: Input, selected: Protocol, options: Record) => { const configured = stringOption(options, "baseURL") ?? input.model.api.url