refactor(llm): collapse model construction to provider packages and routes

Provider packages are now the only module-level model constructors; the
protocol-level model wrappers added earlier on this branch had one call
site each and duplicated the Settings vocabulary, so packages call
route.with(...).model({id}) directly. Deletes provider facades with no
importers outside llm tests; configure stays for V1 native-request.
This commit is contained in:
Kit Langton 2026-07-03 14:25:29 -04:00
commit 5e5294c036
12 changed files with 117 additions and 221 deletions

View file

@ -1,13 +1,11 @@
import { Effect, Schema } from "effect"
import { Route, type RoutePatch } from "../route/client"
import { Auth, type Auth as AuthDef } from "../route/auth"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol"
import {
LLMEvent,
type Model,
type ProviderOptions,
Usage,
type CacheHint,
type FinishReason,
@ -854,25 +852,4 @@ export const route = Route.make({
headers: () => ({ "anthropic-version": "2023-06-01" }),
})
export interface ModelConfig {
readonly auth: AuthDef
readonly baseURL?: string
readonly headers?: Readonly<Record<string, string>>
readonly providerOptions?: ProviderOptions
readonly body?: Readonly<Record<string, unknown>>
readonly limits?: { readonly context: number; readonly output: number }
}
export const model = (id: string, config: ModelConfig): Model =>
route
.with({
auth: config.auth,
endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
headers: config.headers,
providerOptions: config.providerOptions,
http: config.body === undefined ? undefined : { body: config.body },
limits: config.limits,
} satisfies RoutePatch<AnthropicMessagesBody, unknown>)
.model({ id })
export * as AnthropicMessages from "./anthropic-messages"

View file

@ -1,8 +1,6 @@
import { Route, type RoutePatch, type RouteRoutedModelInput } from "../route/client"
import type { Auth as AuthDef } from "../route/auth"
import { Route, type RouteRoutedModelInput } from "../route/client"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import type { Model, ProviderOptions } from "../schema"
import * as OpenAIChat from "./openai-chat"
const ADAPTER = "openai-compatible-chat"
@ -23,25 +21,4 @@ export const route = Route.make({
framing: Framing.sse,
})
export interface ModelConfig {
readonly auth: AuthDef
readonly baseURL?: string
readonly headers?: Readonly<Record<string, string>>
readonly providerOptions?: ProviderOptions
readonly body?: Readonly<Record<string, unknown>>
readonly limits?: { readonly context: number; readonly output: number }
}
export const model = (id: string, config: ModelConfig): Model =>
route
.with({
auth: config.auth,
endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
headers: config.headers,
providerOptions: config.providerOptions,
http: config.body === undefined ? undefined : { body: config.body },
limits: config.limits,
} satisfies RoutePatch<OpenAIChat.OpenAIChatBody, unknown>)
.model({ id, provider: "openai-compatible" })
export * as OpenAICompatibleChat from "./openai-compatible-chat"

View file

@ -1,13 +1,11 @@
import { Effect, Schema } from "effect"
import { Route, type RoutePatch } from "../route/client"
import { Auth, type Auth as AuthDef } from "../route/auth"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
import { HttpTransport, WebSocketTransport } from "../route/transport"
import { Protocol } from "../route/protocol"
import {
LLMEvent,
type Model,
type ProviderOptions,
Usage,
type FinishReason,
type JsonSchema,
@ -993,27 +991,6 @@ export const route = Route.make({
defaults: { providerOptions: { openai: { store: false } } },
})
export interface ModelConfig {
readonly auth: AuthDef
readonly baseURL?: string
readonly headers?: Readonly<Record<string, string>>
readonly providerOptions?: ProviderOptions
readonly body?: Readonly<Record<string, unknown>>
readonly limits?: { readonly context: number; readonly output: number }
}
export const model = (id: string, config: ModelConfig): Model =>
route
.with({
auth: config.auth,
endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
headers: config.headers,
providerOptions: config.providerOptions,
http: config.body === undefined ? undefined : { body: config.body },
limits: config.limits,
} satisfies RoutePatch<OpenAIResponsesBody, unknown>)
.model({ id })
const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =>

View file

@ -3,13 +3,13 @@ import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options"
import { ProviderID, type ModelID } from "../schema"
import { ProviderPackage } from "../provider-package"
import * as AnthropicMessages from "../protocols/anthropic-messages"
import { AnthropicMessages } from "../protocols/anthropic-messages"
export const id = ProviderID.make("anthropic")
export const routes = [AnthropicMessages.route]
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
export interface AnthropicSettings extends ProviderPackage.Settings {}
@ -34,14 +34,15 @@ export const configure = (input: Config = {}) => {
}
}
export const provider = configure()
export const model = ProviderPackage.define((modelID, settings: AnthropicSettings) =>
AnthropicMessages.model(modelID, {
auth: settings.apiKey === undefined ? Auth.none : Auth.header("x-api-key", settings.apiKey),
baseURL: settings.baseURL,
headers: settings.headers,
providerOptions: settings.providerOptions === undefined ? undefined : { anthropic: settings.providerOptions },
body: settings.body,
limits: settings.limits,
}),
AnthropicMessages.route
.with({
auth: settings.apiKey === undefined ? Auth.none : Auth.header("x-api-key", settings.apiKey),
endpoint: { baseURL: settings.baseURL },
headers: settings.headers,
providerOptions: settings.providerOptions && { anthropic: settings.providerOptions },
http: { body: settings.body },
limits: settings.limits,
})
.model({ id: modelID }),
)

View file

@ -1,10 +1,9 @@
import { ProviderID, type ModelID } from "../schema"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { Auth } from "../route/auth"
import { ProviderPackage } from "../provider-package"
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
export const id = ProviderID.make("openai-compatible")
@ -14,11 +13,6 @@ type GenericModelOptions = RouteDefaultsInput &
readonly baseURL: string
}
export type FamilyModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
}
export interface OpenAICompatibleSettings extends ProviderPackage.Settings {}
export const routes = [OpenAICompatibleChat.route]
@ -39,42 +33,15 @@ export const configure = (input: GenericModelOptions) => {
}
}
const define = (profile: OpenAICompatibleProfile) => {
const configureProfile = (input: FamilyModelOptions = {}) => {
const facade = configure({
...input,
baseURL: input.baseURL ?? profile.baseURL,
provider: profile.provider,
})
return {
id: ProviderID.make(profile.provider),
model: facade.model,
configure: configureProfile,
}
}
return configureProfile()
}
export const provider = {
id,
configure,
}
export const model = ProviderPackage.define((modelID, settings: OpenAICompatibleSettings) =>
OpenAICompatibleChat.model(modelID, {
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
baseURL: settings.baseURL,
headers: settings.headers,
providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
body: settings.body,
limits: settings.limits,
}),
OpenAICompatibleChat.route
.with({
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
endpoint: { baseURL: settings.baseURL },
headers: settings.headers,
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
http: { body: settings.body },
limits: settings.limits,
})
.model({ id: modelID, provider: "openai-compatible" }),
)
export const baseten = define(profiles.baseten)
export const cerebras = define(profiles.cerebras)
export const deepinfra = define(profiles.deepinfra)
export const deepseek = define(profiles.deepseek)
export const fireworks = define(profiles.fireworks)
export const groq = define(profiles.groq)
export const togetherai = define(profiles.togetherai)

View file

@ -3,8 +3,8 @@ import { Auth } from "../route/auth"
import type { Route, RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema"
import { ProviderPackage } from "../provider-package"
import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses"
import { OpenAIChat } from "../protocols/openai-chat"
import { OpenAIResponses } from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
@ -16,7 +16,7 @@ export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, Op
// This provider facade wraps the lower-level Responses and Chat model factories
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
// and default option normalization.
export type Config = RouteDefaultsInput &
type Config = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly queryParams?: Record<string, string>
@ -59,18 +59,15 @@ export const configure = (input: Config = {}) => {
}
}
export const provider = configure()
export const model = ProviderPackage.define((modelID, settings: OpenAISettings) =>
OpenAIResponses.model(modelID, {
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
baseURL: settings.baseURL,
headers: settings.headers,
providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
body: settings.body,
limits: settings.limits,
}),
OpenAIResponses.route
.with({
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
endpoint: { baseURL: settings.baseURL },
headers: settings.headers,
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
http: { body: settings.body },
limits: settings.limits,
})
.model({ id: modelID }),
)
export const responses = provider.responses
export const responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat

View file

@ -7,14 +7,16 @@ export interface OpenAICodexSettings extends ProviderPackage.Settings {
}
export const model = ProviderPackage.define((modelID, settings: OpenAICodexSettings) =>
OpenAIResponses.model(modelID, {
auth: (settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey)).andThen(
settings.accountID === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": settings.accountID }),
),
baseURL: "https://chatgpt.com/backend-api/codex",
headers: settings.headers,
providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
body: settings.body,
limits: settings.limits,
}),
OpenAIResponses.route
.with({
auth: (settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey)).andThen(
settings.accountID === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": settings.accountID }),
),
endpoint: { baseURL: "https://chatgpt.com/backend-api/codex" },
headers: settings.headers,
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
http: { body: settings.body },
limits: settings.limits,
})
.model({ id: modelID }),
)