From 2f85f4744d90dbb842e3d3459d7a5089b9a12ab4 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:05:02 +0530 Subject: [PATCH 1/3] feat(llm): add native provider packages --- packages/llm/package.json | 13 ++++-- packages/llm/src/index.ts | 5 +++ packages/llm/src/provider-package.ts | 16 +++++++ .../src/provider-packages/amazon-bedrock.ts | 28 ++++++++++++ .../provider-packages/anthropic-messages.ts | 15 +++++++ .../llm/src/provider-packages/openai-chat.ts | 20 +++++++++ .../openai-compatible-chat.ts | 17 +++++++ .../src/provider-packages/openai-responses.ts | 25 +++++++++++ packages/llm/src/provider-packages/shared.ts | 8 ++++ packages/llm/test/provider-package.test.ts | 45 +++++++++++++++++++ 10 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 packages/llm/src/provider-package.ts create mode 100644 packages/llm/src/provider-packages/amazon-bedrock.ts create mode 100644 packages/llm/src/provider-packages/anthropic-messages.ts create mode 100644 packages/llm/src/provider-packages/openai-chat.ts create mode 100644 packages/llm/src/provider-packages/openai-compatible-chat.ts create mode 100644 packages/llm/src/provider-packages/openai-responses.ts create mode 100644 packages/llm/src/provider-packages/shared.ts create mode 100644 packages/llm/test/provider-package.test.ts diff --git a/packages/llm/package.json b/packages/llm/package.json index d5b370c7d4..6b52dccfe9 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -15,14 +15,19 @@ "./route": "./src/route/index.ts", "./provider": "./src/provider.ts", "./providers": "./src/providers/index.ts", - "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts", - "./providers/anthropic": "./src/providers/anthropic.ts", + "./provider-package": "./src/provider-package.ts", + "./providers/amazon-bedrock": "./src/provider-packages/amazon-bedrock.ts", + "./providers/anthropic": "./src/provider-packages/anthropic-messages.ts", + "./providers/anthropic/messages": "./src/provider-packages/anthropic-messages.ts", "./providers/azure": "./src/providers/azure.ts", "./providers/cloudflare": "./src/providers/cloudflare.ts", "./providers/github-copilot": "./src/providers/github-copilot.ts", "./providers/google": "./src/providers/google.ts", - "./providers/openai": "./src/providers/openai.ts", - "./providers/openai-compatible": "./src/providers/openai-compatible.ts", + "./providers/openai": "./src/provider-packages/openai-responses.ts", + "./providers/openai/responses": "./src/provider-packages/openai-responses.ts", + "./providers/openai/chat": "./src/provider-packages/openai-chat.ts", + "./providers/openai-compatible": "./src/provider-packages/openai-compatible-chat.ts", + "./providers/openai-compatible/chat": "./src/provider-packages/openai-compatible-chat.ts", "./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts", "./providers/openrouter": "./src/providers/openrouter.ts", "./providers/xai": "./src/providers/xai.ts", diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 735520ff77..843b4536a6 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -1,6 +1,7 @@ export { LLMClient } from "./route/client" export { Auth } from "./route/auth" export { Provider } from "./provider" +export { ProviderPackage } from "./provider-package" export { isContextOverflow, isContextOverflowFailure } from "./provider-error" export type { RouteModelInput, @@ -31,3 +32,7 @@ export type { ModelFactory as ProviderModelFactory, ModelOptions as ProviderModelOptions, } from "./provider" +export type { + Definition as ProviderPackageDefinition, + Settings as ProviderPackageSettings, +} from "./provider-package" diff --git a/packages/llm/src/provider-package.ts b/packages/llm/src/provider-package.ts new file mode 100644 index 0000000000..977a211864 --- /dev/null +++ b/packages/llm/src/provider-package.ts @@ -0,0 +1,16 @@ +import type { Model } from "./schema" + +export interface Settings extends Readonly> { + readonly headers?: Readonly> + readonly body?: Readonly> + readonly limits?: { + readonly context: number + readonly output: number + } +} + +export interface Definition { + readonly model: (id: string, settings: ProviderSettings) => Model +} + +export * as ProviderPackage from "./provider-package" diff --git a/packages/llm/src/provider-packages/amazon-bedrock.ts b/packages/llm/src/provider-packages/amazon-bedrock.ts new file mode 100644 index 0000000000..f00b2056ae --- /dev/null +++ b/packages/llm/src/provider-packages/amazon-bedrock.ts @@ -0,0 +1,28 @@ +import type { ProviderPackage } from "../provider-package" +import * as AmazonBedrock from "../providers/amazon-bedrock" +import type { BedrockCredentials } from "../protocols/bedrock-converse" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly auth?: "bearer" | "sigv4" + readonly baseURL?: string + readonly credentials?: BedrockCredentials + readonly region?: string + readonly topP?: number +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + if (settings.auth === "bearer" && settings.apiKey === undefined) + throw new Error("Amazon Bedrock bearer auth requires apiKey") + if (settings.auth === "sigv4" && settings.apiKey !== undefined) + throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") + return AmazonBedrock.configure({ + ...defaults(settings), + apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, + baseURL: settings.baseURL, + credentials: settings.credentials, + generation: settings.topP === undefined ? undefined : { topP: settings.topP }, + region: settings.region, + }).model(id) +} diff --git a/packages/llm/src/provider-packages/anthropic-messages.ts b/packages/llm/src/provider-packages/anthropic-messages.ts new file mode 100644 index 0000000000..be6def4081 --- /dev/null +++ b/packages/llm/src/provider-packages/anthropic-messages.ts @@ -0,0 +1,15 @@ +import type { ProviderPackage } from "../provider-package" +import * as Anthropic from "../providers/anthropic" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + Anthropic.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + }).model(id) diff --git a/packages/llm/src/provider-packages/openai-chat.ts b/packages/llm/src/provider-packages/openai-chat.ts new file mode 100644 index 0000000000..6a6d04c704 --- /dev/null +++ b/packages/llm/src/provider-packages/openai-chat.ts @@ -0,0 +1,20 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAI from "../providers/openai" +import type { OpenAIProviderOptionsInput } from "../providers/openai-options" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly queryParams?: Readonly> + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + OpenAI.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + providerOptions: settings.providerOptions, + }).chat(id) diff --git a/packages/llm/src/provider-packages/openai-compatible-chat.ts b/packages/llm/src/provider-packages/openai-compatible-chat.ts new file mode 100644 index 0000000000..ba7d569b9c --- /dev/null +++ b/packages/llm/src/provider-packages/openai-compatible-chat.ts @@ -0,0 +1,17 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAICompatible from "../providers/openai-compatible" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL: string + readonly provider?: string +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + OpenAICompatible.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + provider: settings.provider, + }).model(id) diff --git a/packages/llm/src/provider-packages/openai-responses.ts b/packages/llm/src/provider-packages/openai-responses.ts new file mode 100644 index 0000000000..4851a96a34 --- /dev/null +++ b/packages/llm/src/provider-packages/openai-responses.ts @@ -0,0 +1,25 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAI from "../providers/openai" +import type { OpenAIProviderOptionsInput } from "../providers/openai-options" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly queryParams?: Readonly> + readonly transport?: "http" | "websocket" + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + const provider = OpenAI.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + providerOptions: settings.providerOptions, + }) + if (settings.transport === undefined || settings.transport === "http") return provider.responses(id) + if (settings.transport === "websocket") return provider.responsesWebSocket(id) + throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) +} diff --git a/packages/llm/src/provider-packages/shared.ts b/packages/llm/src/provider-packages/shared.ts new file mode 100644 index 0000000000..e3c7ec2444 --- /dev/null +++ b/packages/llm/src/provider-packages/shared.ts @@ -0,0 +1,8 @@ +import type { RouteDefaultsInput } from "../route/client" +import type { ProviderPackage } from "../provider-package" + +export const defaults = (settings: ProviderPackage.Settings): RouteDefaultsInput => ({ + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + limits: settings.limits, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, +}) diff --git a/packages/llm/test/provider-package.test.ts b/packages/llm/test/provider-package.test.ts new file mode 100644 index 0000000000..a4f1627c67 --- /dev/null +++ b/packages/llm/test/provider-package.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { model } from "@opencode-ai/llm/providers/openai" + +describe("provider package entrypoints", () => { + test("semantic API aliases expose the same contract", async () => { + const modules = await Promise.all([ + import("@opencode-ai/llm/providers/openai"), + import("@opencode-ai/llm/providers/openai/responses"), + import("@opencode-ai/llm/providers/openai/chat"), + import("@opencode-ai/llm/providers/anthropic"), + import("@opencode-ai/llm/providers/anthropic/messages"), + import("@opencode-ai/llm/providers/openai-compatible"), + import("@opencode-ai/llm/providers/openai-compatible/chat"), + import("@opencode-ai/llm/providers/amazon-bedrock"), + ]) + + for (const module of modules) expect(module.model).toBeFunction() + expect(modules[0].model).toBe(modules[1].model) + expect(modules[3].model).toBe(modules[4].model) + expect(modules[5].model).toBe(modules[6].model) + }) + + test("maps package settings onto the executable model", () => { + const selected = model("gpt-5", { + apiKey: "fixture", + baseURL: "https://api.openai.test/v1", + headers: { "x-application": "opencode" }, + body: { service_tier: "priority" }, + limits: { context: 200_000, output: 64_000 }, + unrelatedInheritedSetting: true, + }) + + expect(selected.route.id).toBe("openai-responses") + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) + expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + }) + + test("selects transport without changing the semantic API", () => { + expect(model("gpt-5", { apiKey: "fixture" }).route.id).toBe("openai-responses") + expect(model("gpt-5", { apiKey: "fixture", transport: "websocket" }).route.id).toBe( + "openai-responses-websocket", + ) + }) +}) From beefda1f8fe017852626baef83a467840604a6fd Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:10:45 +0530 Subject: [PATCH 2/3] refactor(llm): colocate provider package logic --- packages/llm/package.json | 14 ++++----- .../src/provider-packages/amazon-bedrock.ts | 28 ------------------ .../provider-packages/anthropic-messages.ts | 15 ---------- .../llm/src/provider-packages/openai-chat.ts | 20 ------------- .../openai-compatible-chat.ts | 17 ----------- .../src/provider-packages/openai-responses.ts | 25 ---------------- packages/llm/src/provider-packages/shared.ts | 8 ----- packages/llm/src/providers/amazon-bedrock.ts | 27 ++++++++++++++++- packages/llm/src/providers/anthropic.ts | 15 +++++++++- .../llm/src/providers/openai-compatible.ts | 17 +++++++++++ packages/llm/src/providers/openai.ts | 29 ++++++++++++++++++- packages/llm/src/providers/openai/chat.ts | 2 ++ .../llm/src/providers/openai/responses.ts | 2 ++ packages/llm/test/exports.test.ts | 1 - packages/llm/test/provider-package.test.ts | 4 --- 15 files changed, 95 insertions(+), 129 deletions(-) delete mode 100644 packages/llm/src/provider-packages/amazon-bedrock.ts delete mode 100644 packages/llm/src/provider-packages/anthropic-messages.ts delete mode 100644 packages/llm/src/provider-packages/openai-chat.ts delete mode 100644 packages/llm/src/provider-packages/openai-compatible-chat.ts delete mode 100644 packages/llm/src/provider-packages/openai-responses.ts delete mode 100644 packages/llm/src/provider-packages/shared.ts create mode 100644 packages/llm/src/providers/openai/chat.ts create mode 100644 packages/llm/src/providers/openai/responses.ts diff --git a/packages/llm/package.json b/packages/llm/package.json index 6b52dccfe9..f78509d247 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -16,18 +16,16 @@ "./provider": "./src/provider.ts", "./providers": "./src/providers/index.ts", "./provider-package": "./src/provider-package.ts", - "./providers/amazon-bedrock": "./src/provider-packages/amazon-bedrock.ts", - "./providers/anthropic": "./src/provider-packages/anthropic-messages.ts", - "./providers/anthropic/messages": "./src/provider-packages/anthropic-messages.ts", + "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts", + "./providers/anthropic": "./src/providers/anthropic.ts", "./providers/azure": "./src/providers/azure.ts", "./providers/cloudflare": "./src/providers/cloudflare.ts", "./providers/github-copilot": "./src/providers/github-copilot.ts", "./providers/google": "./src/providers/google.ts", - "./providers/openai": "./src/provider-packages/openai-responses.ts", - "./providers/openai/responses": "./src/provider-packages/openai-responses.ts", - "./providers/openai/chat": "./src/provider-packages/openai-chat.ts", - "./providers/openai-compatible": "./src/provider-packages/openai-compatible-chat.ts", - "./providers/openai-compatible/chat": "./src/provider-packages/openai-compatible-chat.ts", + "./providers/openai": "./src/providers/openai.ts", + "./providers/openai/responses": "./src/providers/openai/responses.ts", + "./providers/openai/chat": "./src/providers/openai/chat.ts", + "./providers/openai-compatible": "./src/providers/openai-compatible.ts", "./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts", "./providers/openrouter": "./src/providers/openrouter.ts", "./providers/xai": "./src/providers/xai.ts", diff --git a/packages/llm/src/provider-packages/amazon-bedrock.ts b/packages/llm/src/provider-packages/amazon-bedrock.ts deleted file mode 100644 index f00b2056ae..0000000000 --- a/packages/llm/src/provider-packages/amazon-bedrock.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as AmazonBedrock from "../providers/amazon-bedrock" -import type { BedrockCredentials } from "../protocols/bedrock-converse" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly auth?: "bearer" | "sigv4" - readonly baseURL?: string - readonly credentials?: BedrockCredentials - readonly region?: string - readonly topP?: number -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => { - if (settings.auth === "bearer" && settings.apiKey === undefined) - throw new Error("Amazon Bedrock bearer auth requires apiKey") - if (settings.auth === "sigv4" && settings.apiKey !== undefined) - throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") - return AmazonBedrock.configure({ - ...defaults(settings), - apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, - baseURL: settings.baseURL, - credentials: settings.credentials, - generation: settings.topP === undefined ? undefined : { topP: settings.topP }, - region: settings.region, - }).model(id) -} diff --git a/packages/llm/src/provider-packages/anthropic-messages.ts b/packages/llm/src/provider-packages/anthropic-messages.ts deleted file mode 100644 index be6def4081..0000000000 --- a/packages/llm/src/provider-packages/anthropic-messages.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as Anthropic from "../providers/anthropic" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - Anthropic.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - }).model(id) diff --git a/packages/llm/src/provider-packages/openai-chat.ts b/packages/llm/src/provider-packages/openai-chat.ts deleted file mode 100644 index 6a6d04c704..0000000000 --- a/packages/llm/src/provider-packages/openai-chat.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAI from "../providers/openai" -import type { OpenAIProviderOptionsInput } from "../providers/openai-options" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string - readonly queryParams?: Readonly> - readonly providerOptions?: OpenAIProviderOptionsInput -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - OpenAI.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, - providerOptions: settings.providerOptions, - }).chat(id) diff --git a/packages/llm/src/provider-packages/openai-compatible-chat.ts b/packages/llm/src/provider-packages/openai-compatible-chat.ts deleted file mode 100644 index ba7d569b9c..0000000000 --- a/packages/llm/src/provider-packages/openai-compatible-chat.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAICompatible from "../providers/openai-compatible" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL: string - readonly provider?: string -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - OpenAICompatible.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - provider: settings.provider, - }).model(id) diff --git a/packages/llm/src/provider-packages/openai-responses.ts b/packages/llm/src/provider-packages/openai-responses.ts deleted file mode 100644 index 4851a96a34..0000000000 --- a/packages/llm/src/provider-packages/openai-responses.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAI from "../providers/openai" -import type { OpenAIProviderOptionsInput } from "../providers/openai-options" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string - readonly queryParams?: Readonly> - readonly transport?: "http" | "websocket" - readonly providerOptions?: OpenAIProviderOptionsInput -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => { - const provider = OpenAI.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, - providerOptions: settings.providerOptions, - }) - if (settings.transport === undefined || settings.transport === "http") return provider.responses(id) - if (settings.transport === "websocket") return provider.responsesWebSocket(id) - throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) -} diff --git a/packages/llm/src/provider-packages/shared.ts b/packages/llm/src/provider-packages/shared.ts deleted file mode 100644 index e3c7ec2444..0000000000 --- a/packages/llm/src/provider-packages/shared.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RouteDefaultsInput } from "../route/client" -import type { ProviderPackage } from "../provider-package" - -export const defaults = (settings: ProviderPackage.Settings): RouteDefaultsInput => ({ - headers: settings.headers === undefined ? undefined : { ...settings.headers }, - limits: settings.limits, - http: settings.body === undefined ? undefined : { body: { ...settings.body } }, -}) diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/llm/src/providers/amazon-bedrock.ts index 2f1791e0d6..e578d614c6 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/llm/src/providers/amazon-bedrock.ts @@ -1,5 +1,6 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as BedrockConverse from "../protocols/bedrock-converse" import type { BedrockCredentials } from "../protocols/bedrock-converse" @@ -15,6 +16,15 @@ export type Config = RouteDefaultsInput & { /** Override the computed `https://bedrock-runtime..amazonaws.com` URL. */ readonly baseURL?: string } + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly auth?: "bearer" | "sigv4" + readonly baseURL?: string + readonly credentials?: BedrockCredentials + readonly region?: string + readonly topP?: number +} export const routes = [BedrockConverse.route] const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com` @@ -40,4 +50,19 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model = provider.model +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + if (settings.auth === "bearer" && settings.apiKey === undefined) + throw new Error("Amazon Bedrock bearer auth requires apiKey") + if (settings.auth === "sigv4" && settings.apiKey !== undefined) + throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") + return configure({ + apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, + baseURL: settings.baseURL, + credentials: settings.credentials, + generation: settings.topP === undefined ? undefined : { topP: settings.topP }, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + region: settings.region, + }).model(id) +} diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index 0c9640af5e..8ef0293eeb 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -1,6 +1,7 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as AnthropicMessages from "../protocols/anthropic-messages" @@ -10,6 +11,11 @@ export const routes = [AnthropicMessages.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string +} + const auth = (options: ProviderAuthOption<"optional">) => { if ("auth" in options && options.auth) return options.auth return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") @@ -32,4 +38,11 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model = provider.model +export const model: ProviderPackage.Definition["model"] = (id, settings) => + configure({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + }).model(id) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index a79f65f6df..36d9b3fd5a 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" import type { RouteDefaultsInput } from "../route/client" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" +import type { ProviderPackage } from "../provider-package" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile" export const id = ProviderID.make("openai-compatible") @@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput & readonly baseURL: string } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL: string + readonly provider?: string +} + export type FamilyModelOptions = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string @@ -56,6 +63,16 @@ export const provider = { configure, } +export const model: ProviderPackage.Definition["model"] = (id, settings) => + configure({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + provider: settings.provider, + }).model(id) + export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) export const deepinfra = define(profiles.deepinfra) diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index 098cad8493..b40a94514b 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -1,5 +1,6 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import type { Route, RouteDefaultsInput } from "../route/client" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" @@ -21,6 +22,14 @@ export type Config = RouteDefaultsInput & readonly providerOptions?: OpenAIProviderOptionsInput } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly queryParams?: Readonly> + readonly transport?: "http" | "websocket" + readonly providerOptions?: OpenAIProviderOptionsInput +} + const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY") const defaults = (input: Config) => { @@ -57,7 +66,25 @@ export const configure = (input: Config = {}) => { export const provider = configure() -export const model = provider.model +const config = (settings: Settings): Config => ({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + providerOptions: settings.providerOptions, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, +}) + +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + const configured = configure(config(settings)) + if (settings.transport === undefined || settings.transport === "http") return configured.responses(id) + if (settings.transport === "websocket") return configured.responsesWebSocket(id) + throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) +} + +export const chatModel: ProviderPackage.Definition["model"] = (id, settings) => + configure(config(settings)).chat(id) export const responses = provider.responses export const responsesWebSocket = provider.responsesWebSocket export const chat = provider.chat diff --git a/packages/llm/src/providers/openai/chat.ts b/packages/llm/src/providers/openai/chat.ts new file mode 100644 index 0000000000..eb92db8ac5 --- /dev/null +++ b/packages/llm/src/providers/openai/chat.ts @@ -0,0 +1,2 @@ +export { chatModel as model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/llm/src/providers/openai/responses.ts b/packages/llm/src/providers/openai/responses.ts new file mode 100644 index 0000000000..4db3a232c7 --- /dev/null +++ b/packages/llm/src/providers/openai/responses.ts @@ -0,0 +1,2 @@ +export { model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/llm/test/exports.test.ts b/packages/llm/test/exports.test.ts index 693a21638b..6ed66a0d8e 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/llm/test/exports.test.ts @@ -30,7 +30,6 @@ describe("public exports", () => { test("provider barrels expose user-facing facades", () => { expect(OpenAI.model).toBeFunction() - expect(OpenAI.provider.model).toBe(OpenAI.model) expect(OpenAI.provider.responses).toBe(OpenAI.responses) expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket) expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction() diff --git a/packages/llm/test/provider-package.test.ts b/packages/llm/test/provider-package.test.ts index a4f1627c67..c8cba78eb1 100644 --- a/packages/llm/test/provider-package.test.ts +++ b/packages/llm/test/provider-package.test.ts @@ -8,16 +8,12 @@ describe("provider package entrypoints", () => { import("@opencode-ai/llm/providers/openai/responses"), import("@opencode-ai/llm/providers/openai/chat"), import("@opencode-ai/llm/providers/anthropic"), - import("@opencode-ai/llm/providers/anthropic/messages"), import("@opencode-ai/llm/providers/openai-compatible"), - import("@opencode-ai/llm/providers/openai-compatible/chat"), import("@opencode-ai/llm/providers/amazon-bedrock"), ]) for (const module of modules) expect(module.model).toBeFunction() expect(modules[0].model).toBe(modules[1].model) - expect(modules[3].model).toBe(modules[4].model) - expect(modules[5].model).toBe(modules[6].model) }) test("maps package settings onto the executable model", () => { From 3a09383d331e3b3c8ead76a81f11b934f7d4b7d5 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:29:06 +0530 Subject: [PATCH 3/3] refactor(llm): name provider model identity --- packages/llm/src/provider-package.ts | 2 +- packages/llm/src/providers/amazon-bedrock.ts | 4 ++-- packages/llm/src/providers/anthropic.ts | 4 ++-- packages/llm/src/providers/openai-compatible.ts | 4 ++-- packages/llm/src/providers/openai.ts | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/llm/src/provider-package.ts b/packages/llm/src/provider-package.ts index 977a211864..fd878a9d06 100644 --- a/packages/llm/src/provider-package.ts +++ b/packages/llm/src/provider-package.ts @@ -10,7 +10,7 @@ export interface Settings extends Readonly> { } export interface Definition { - readonly model: (id: string, settings: ProviderSettings) => Model + readonly model: (modelID: string, settings: ProviderSettings) => Model } export * as ProviderPackage from "./provider-package" diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/llm/src/providers/amazon-bedrock.ts index e578d614c6..4e5040c9ec 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/llm/src/providers/amazon-bedrock.ts @@ -50,7 +50,7 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model: ProviderPackage.Definition["model"] = (id, settings) => { +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { if (settings.auth === "bearer" && settings.apiKey === undefined) throw new Error("Amazon Bedrock bearer auth requires apiKey") if (settings.auth === "sigv4" && settings.apiKey !== undefined) @@ -64,5 +64,5 @@ export const model: ProviderPackage.Definition["model"] = (id, setting http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, region: settings.region, - }).model(id) + }).model(modelID) } diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index 8ef0293eeb..1bf8bf569c 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -38,11 +38,11 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model: ProviderPackage.Definition["model"] = (id, settings) => +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => configure({ apiKey: settings.apiKey, baseURL: settings.baseURL, headers: settings.headers === undefined ? undefined : { ...settings.headers }, http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, - }).model(id) + }).model(modelID) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index 36d9b3fd5a..8b5d23eed7 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -63,7 +63,7 @@ export const provider = { configure, } -export const model: ProviderPackage.Definition["model"] = (id, settings) => +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => configure({ apiKey: settings.apiKey, baseURL: settings.baseURL, @@ -71,7 +71,7 @@ export const model: ProviderPackage.Definition["model"] = (id, setting http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, provider: settings.provider, - }).model(id) + }).model(modelID) export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index b40a94514b..6b49b9def2 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -76,15 +76,15 @@ const config = (settings: Settings): Config => ({ queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, }) -export const model: ProviderPackage.Definition["model"] = (id, settings) => { +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { const configured = configure(config(settings)) - if (settings.transport === undefined || settings.transport === "http") return configured.responses(id) - if (settings.transport === "websocket") return configured.responsesWebSocket(id) + if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID) + if (settings.transport === "websocket") return configured.responsesWebSocket(modelID) throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) } -export const chatModel: ProviderPackage.Definition["model"] = (id, settings) => - configure(config(settings)).chat(id) +export const chatModel: ProviderPackage.Definition["model"] = (modelID, settings) => + configure(config(settings)).chat(modelID) export const responses = provider.responses export const responsesWebSocket = provider.responsesWebSocket export const chat = provider.chat