feat(llm): add native provider packages
This commit is contained in:
parent
116ac93ddb
commit
733bb06134
10 changed files with 188 additions and 4 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
16
packages/llm/src/provider-package.ts
Normal file
16
packages/llm/src/provider-package.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { Model } from "./schema"
|
||||
|
||||
export interface Settings extends Readonly<Record<string, unknown>> {
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
readonly body?: Readonly<Record<string, unknown>>
|
||||
readonly limits?: {
|
||||
readonly context: number
|
||||
readonly output: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Definition<ProviderSettings extends Settings = Settings> {
|
||||
readonly model: (id: string, settings: ProviderSettings) => Model
|
||||
}
|
||||
|
||||
export * as ProviderPackage from "./provider-package"
|
||||
28
packages/llm/src/provider-packages/amazon-bedrock.ts
Normal file
28
packages/llm/src/provider-packages/amazon-bedrock.ts
Normal file
|
|
@ -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<Settings>["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)
|
||||
}
|
||||
15
packages/llm/src/provider-packages/anthropic-messages.ts
Normal file
15
packages/llm/src/provider-packages/anthropic-messages.ts
Normal file
|
|
@ -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<Settings>["model"] = (id, settings) =>
|
||||
Anthropic.configure({
|
||||
...defaults(settings),
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
}).model(id)
|
||||
20
packages/llm/src/provider-packages/openai-chat.ts
Normal file
20
packages/llm/src/provider-packages/openai-chat.ts
Normal file
|
|
@ -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<Record<string, string>>
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings>["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)
|
||||
17
packages/llm/src/provider-packages/openai-compatible-chat.ts
Normal file
17
packages/llm/src/provider-packages/openai-compatible-chat.ts
Normal file
|
|
@ -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<Settings>["model"] = (id, settings) =>
|
||||
OpenAICompatible.configure({
|
||||
...defaults(settings),
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
provider: settings.provider,
|
||||
}).model(id)
|
||||
25
packages/llm/src/provider-packages/openai-responses.ts
Normal file
25
packages/llm/src/provider-packages/openai-responses.ts
Normal file
|
|
@ -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<Record<string, string>>
|
||||
readonly transport?: "http" | "websocket"
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings>["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)}`)
|
||||
}
|
||||
8
packages/llm/src/provider-packages/shared.ts
Normal file
8
packages/llm/src/provider-packages/shared.ts
Normal file
|
|
@ -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 } },
|
||||
})
|
||||
45
packages/llm/test/provider-package.test.ts
Normal file
45
packages/llm/test/provider-package.test.ts
Normal file
|
|
@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue