From 5e5294c0368f72e284803d66c7babc659b198978 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 3 Jul 2026 14:25:29 -0400 Subject: [PATCH] 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. --- .../llm/src/protocols/anthropic-messages.ts | 27 +-------- .../src/protocols/openai-compatible-chat.ts | 25 +-------- .../llm/src/protocols/openai-responses.ts | 27 +-------- packages/llm/src/providers/anthropic.ts | 23 ++++---- .../llm/src/providers/openai-compatible.ts | 55 ++++--------------- packages/llm/src/providers/openai.ts | 29 +++++----- packages/llm/src/providers/openai/codex.ts | 22 ++++---- packages/llm/test/auth-options.types.ts | 13 +++-- packages/llm/test/exports.test.ts | 8 +-- .../llm/test/provider/golden.recorded.test.ts | 26 +++++---- .../provider/openai-compatible-chat.test.ts | 45 --------------- .../test/provider/provider-package.test.ts | 38 ++++++++++++- 12 files changed, 117 insertions(+), 221 deletions(-) diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/llm/src/protocols/anthropic-messages.ts index 22d8f3375b..1c0dcd32a4 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/llm/src/protocols/anthropic-messages.ts @@ -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> - readonly providerOptions?: ProviderOptions - readonly body?: Readonly> - 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) - .model({ id }) - export * as AnthropicMessages from "./anthropic-messages" diff --git a/packages/llm/src/protocols/openai-compatible-chat.ts b/packages/llm/src/protocols/openai-compatible-chat.ts index 07bdf2f531..ce3f0a83d7 100644 --- a/packages/llm/src/protocols/openai-compatible-chat.ts +++ b/packages/llm/src/protocols/openai-compatible-chat.ts @@ -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> - readonly providerOptions?: ProviderOptions - readonly body?: Readonly> - 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) - .model({ id, provider: "openai-compatible" }) - export * as OpenAICompatibleChat from "./openai-compatible-chat" diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index d5d37d96c4..4936d31c92 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -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> - readonly providerOptions?: ProviderOptions - readonly body?: Readonly> - 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) - .model({ id }) - const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage)) const webSocketMessage = (body: OpenAIResponsesBody | Record) => diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index 0975ad40b0..63094704b6 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -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 }), ) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index fb497e0889..fbb53a851d 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -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) diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index a5420d1d0e..51e7b47daf 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -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 @@ -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 diff --git a/packages/llm/src/providers/openai/codex.ts b/packages/llm/src/providers/openai/codex.ts index 62c113f277..d99defa6fa 100644 --- a/packages/llm/src/providers/openai/codex.ts +++ b/packages/llm/src/providers/openai/codex.ts @@ -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 }), ) diff --git a/packages/llm/test/auth-options.types.ts b/packages/llm/test/auth-options.types.ts index 18f9508c3c..5c41a3df2a 100644 --- a/packages/llm/test/auth-options.types.ts +++ b/packages/llm/test/auth-options.types.ts @@ -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. diff --git a/packages/llm/test/exports.test.ts b/packages/llm/test/exports.test.ts index d52108c787..89b5db842f 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/llm/test/exports.test.ts @@ -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() diff --git a/packages/llm/test/provider/golden.recorded.test.ts b/packages/llm/test/provider/golden.recorded.test.ts index ef67c866d8..e5b3be214b 100644 --- a/packages/llm/test/provider/golden.recorded.test.ts +++ b/packages/llm/test/provider/golden.recorded.test.ts @@ -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") diff --git a/packages/llm/test/provider/openai-compatible-chat.test.ts b/packages/llm/test/provider/openai-compatible-chat.test.ts index 43ae283e9f..457a11de28 100644 --- a/packages/llm/test/provider/openai-compatible-chat.test.ts +++ b/packages/llm/test/provider/openai-compatible-chat.test.ts @@ -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) diff --git a/packages/llm/test/provider/provider-package.test.ts b/packages/llm/test/provider/provider-package.test.ts index e028dabd82..bf7935fc35 100644 --- a/packages/llm/test/provider/provider-package.test.ts +++ b/packages/llm/test/provider/provider-package.test.ts @@ -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" }, + }) + }), + ), + ), + ) + }), + ) })