refactor(llm): brand provider and model identifiers

This commit is contained in:
Kit Langton
2026-04-26 19:40:20 -04:00
parent 4e3f678b24
commit 6c887b0faa
9 changed files with 55 additions and 28 deletions
+9 -1
View File
@@ -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<ConstructorParameters<typeof ModelRef>[0], "capabilities" | "limits"> & {
export type ModelInput = Omit<ConstructorParameters<typeof ModelRef>[0], "id" | "provider" | "capabilities" | "limits"> & {
readonly id: string | ModelIDType
readonly provider: string | ProviderIDType
readonly capabilities?: ModelCapabilities | CapabilitiesInput
readonly limits?: ModelLimits | ConstructorParameters<typeof ModelLimits>[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),
+22 -8
View File
@@ -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<string, unknown>
}
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<string, unknown>,
): ProviderRouteInput => ({
modelID: ModelID.make(modelID),
providerID: ProviderID.make(providerID),
options,
})
export * as ProviderRoute from "./provider-route"
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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)
@@ -21,7 +21,7 @@ export const byProvider: Record<string, ProviderFamily> = 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),
})
+10 -4
View File
@@ -10,6 +10,12 @@ export const Protocol = Schema.Literals([
])
export type Protocol = Schema.Schema.Type<typeof Protocol>
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<typeof ReasoningEffort>
@@ -61,8 +67,8 @@ export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
}) {}
export class ModelRef extends Schema.Class<ModelRef>("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<InvalidRequestE
export class NoAdapterError extends Schema.TaggedErrorClass<NoAdapterError>()("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}`
@@ -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,
+3 -3
View File
@@ -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({}),
+4 -7
View File
@@ -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<string, unknown>, key: string): Record<str
export const route = (
input: Input,
options: Record<string, unknown> = { ...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<string, unknown>) => {
const configured = stringOption(options, "baseURL") ?? input.model.api.url