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:
parent
759db0d172
commit
5e5294c036
12 changed files with 117 additions and 221 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>) =>
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ requiredAuthModel("custom-model", {})
|
|||
// @ts-expect-error auth is an override, so apiKey cannot be supplied with it.
|
||||
requiredAuthModel("custom-model", { apiKey: "key", auth })
|
||||
|
||||
OpenAI.responses("gpt-4.1-mini")
|
||||
OpenAI.configure({}).responses("gpt-4.1-mini")
|
||||
OpenAI.configure({ apiKey: "sk-test" }).responses("gpt-4.1-mini")
|
||||
OpenAI.configure({ apiKey: configApiKey }).responses("gpt-4.1-mini")
|
||||
|
|
@ -99,7 +98,6 @@ OpenAI.configure({ providerOptions: { openai: { store: "false" } } })
|
|||
// @ts-expect-error auth is an override, so OpenAI rejects apiKey with auth.
|
||||
OpenAI.configure({ apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") })
|
||||
|
||||
OpenAI.chat("gpt-4.1-mini")
|
||||
OpenAI.configure({ apiKey: "sk-test" }).chat("gpt-4.1-mini")
|
||||
OpenAI.configure({ apiKey: configApiKey }).chat("gpt-4.1-mini")
|
||||
OpenAI.configure({ auth: RuntimeAuth.bearer("oauth-token") }).chat("gpt-4.1-mini")
|
||||
|
|
@ -155,9 +153,14 @@ XAI.configure({ apiKey: "xai-key" }).responses("grok-4", {})
|
|||
// @ts-expect-error xAI Chat selectors only accept model ids.
|
||||
XAI.configure({ apiKey: "xai-key" }).chat("grok-4", {})
|
||||
|
||||
OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat")
|
||||
// @ts-expect-error OpenAI-compatible family selectors only accept model ids.
|
||||
OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat", {})
|
||||
OpenAICompatible.configure({
|
||||
apiKey: "deepseek-key",
|
||||
provider: "deepseek",
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
}).model("deepseek-chat")
|
||||
OpenAICompatible.configure({ apiKey: "deepseek-key", provider: "deepseek", baseURL: "https://api.deepseek.com/v1" })
|
||||
// @ts-expect-error OpenAI-compatible model selectors only accept model ids.
|
||||
.model("deepseek-chat", {})
|
||||
|
||||
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
|
||||
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ describe("public exports", () => {
|
|||
|
||||
test("provider barrels expose user-facing facades", () => {
|
||||
expect(OpenAI.model).toBeFunction()
|
||||
expect(OpenAI.provider.model).toBeFunction()
|
||||
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
||||
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
||||
expect(OpenAICodex.model).toBeFunction()
|
||||
expect(OpenAICompatible.deepseek.model).toBeFunction()
|
||||
expect(OpenAICompatible.model).toBeFunction()
|
||||
expect(
|
||||
OpenAICompatible.configure({ baseURL: "https://api.compatible.test/v1", apiKey: "fixture" }).model,
|
||||
).toBeFunction()
|
||||
expect(CloudflareAIGateway.configure).toBeFunction()
|
||||
expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction()
|
||||
expect(CloudflareWorkersAI.configure).toBeFunction()
|
||||
|
|
|
|||
|
|
@ -39,17 +39,21 @@ const cloudflareAIGatewayWorkers = cloudflareAIGateway.model("workers-ai/@cf/met
|
|||
const cloudflareAIGatewayWorkersTools = cloudflareAIGateway.model("workers-ai/@cf/openai/gpt-oss-20b")
|
||||
const cloudflareWorkersAI = cloudflareWorkers.model("@cf/meta/llama-3.1-8b-instruct")
|
||||
const cloudflareWorkersAITools = cloudflareWorkers.model("@cf/openai/gpt-oss-20b")
|
||||
const deepseek = OpenAICompatible.deepseek
|
||||
.configure({ apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture" })
|
||||
.model("deepseek-chat")
|
||||
const together = OpenAICompatible.togetherai
|
||||
.configure({
|
||||
apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
||||
})
|
||||
.model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
||||
const groq = OpenAICompatible.groq
|
||||
.configure({ apiKey: process.env.GROQ_API_KEY ?? "fixture" })
|
||||
.model("llama-3.3-70b-versatile")
|
||||
const deepseek = OpenAICompatible.configure({
|
||||
apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture",
|
||||
provider: "deepseek",
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
}).model("deepseek-chat")
|
||||
const together = OpenAICompatible.configure({
|
||||
apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
||||
provider: "togetherai",
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
}).model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
||||
const groq = OpenAICompatible.configure({
|
||||
apiKey: process.env.GROQ_API_KEY ?? "fixture",
|
||||
provider: "groq",
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
}).model("llama-3.3-70b-versatile")
|
||||
const openRouter = OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" })
|
||||
const openrouter = openRouter.model("openai/gpt-4o-mini")
|
||||
const openrouterGpt55 = openRouter.model("openai/gpt-5.5")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Effect, Schema } from "effect"
|
|||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, Message, ToolCallPart } from "../../src"
|
||||
import { Auth, LLMClient } from "../../src/route"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse } from "../lib/http"
|
||||
|
|
@ -40,15 +39,6 @@ const usageChunk = (usage: object) => ({
|
|||
usage,
|
||||
})
|
||||
|
||||
const providerFamilies = [
|
||||
["baseten", OpenAICompatible.baseten, "https://inference.baseten.co/v1"],
|
||||
["cerebras", OpenAICompatible.cerebras, "https://api.cerebras.ai/v1"],
|
||||
["deepinfra", OpenAICompatible.deepinfra, "https://api.deepinfra.com/v1/openai"],
|
||||
["deepseek", OpenAICompatible.deepseek, "https://api.deepseek.com/v1"],
|
||||
["fireworks", OpenAICompatible.fireworks, "https://api.fireworks.ai/inference/v1"],
|
||||
["togetherai", OpenAICompatible.togetherai, "https://api.together.xyz/v1"],
|
||||
] as const
|
||||
|
||||
describe("OpenAI-compatible Chat route", () => {
|
||||
it.effect("prepares generic Chat target", () =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -90,41 +80,6 @@ describe("OpenAI-compatible Chat route", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("provides model helpers for compatible provider families", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
providerFamilies.map(([provider, family]) => {
|
||||
const model = family.configure({ apiKey: "test-key" }).model(`${provider}-model`)
|
||||
return {
|
||||
id: String(model.id),
|
||||
provider: String(model.provider),
|
||||
route: model.route.id,
|
||||
baseURL: model.route.endpoint.baseURL,
|
||||
}
|
||||
}),
|
||||
).toEqual(
|
||||
providerFamilies.map(([provider, _, baseURL]) => ({
|
||||
id: `${provider}-model`,
|
||||
provider,
|
||||
route: "openai-compatible-chat",
|
||||
baseURL,
|
||||
})),
|
||||
)
|
||||
|
||||
const custom = OpenAICompatible.deepseek
|
||||
.configure({
|
||||
apiKey: "test-key",
|
||||
baseURL: "https://custom.deepseek.test/v1",
|
||||
})
|
||||
.model("deepseek-chat")
|
||||
expect(custom).toMatchObject({
|
||||
provider: "deepseek",
|
||||
route: { id: "openai-compatible-chat" },
|
||||
})
|
||||
expect(custom.route.endpoint.baseURL).toBe("https://custom.deepseek.test/v1")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
|||
import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMClient } from "../../src"
|
||||
import { Anthropic, OpenAI, OpenAICodex } from "../../src/providers"
|
||||
import { Anthropic, OpenAI, OpenAICodex, OpenAICompatible } from "../../src/providers"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse } from "../lib/http"
|
||||
import { sseEvents } from "../lib/sse"
|
||||
|
|
@ -117,4 +117,40 @@ describe("provider package contract", () => {
|
|||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("builds OpenAI-compatible Chat models from flat settings", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* LLMClient.generate(
|
||||
requestFor(
|
||||
OpenAICompatible.model("compatible-x", {
|
||||
apiKey: "compatible-key",
|
||||
baseURL: "https://api.compatible.test/v1",
|
||||
body: { user: "provider-package" },
|
||||
providerOptions: { serviceTier: "priority" },
|
||||
}),
|
||||
),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
||||
const body = decodeJsonRecord(input.text)
|
||||
|
||||
expect(web.url).toBe("https://api.compatible.test/v1/chat/completions")
|
||||
expect(web.headers.get("authorization")).toBe("Bearer compatible-key")
|
||||
expect(body).toMatchObject({
|
||||
model: "compatible-x",
|
||||
user: "provider-package",
|
||||
stream: true,
|
||||
})
|
||||
expect(body).not.toHaveProperty("apiKey")
|
||||
return input.respond(sseEvents({ choices: [{ delta: {}, finish_reason: "stop" }], usage: null }), {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue