mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a09383d33 | |||
| beefda1f8f | |||
| 2f85f4744d |
@@ -15,6 +15,7 @@
|
|||||||
"./route": "./src/route/index.ts",
|
"./route": "./src/route/index.ts",
|
||||||
"./provider": "./src/provider.ts",
|
"./provider": "./src/provider.ts",
|
||||||
"./providers": "./src/providers/index.ts",
|
"./providers": "./src/providers/index.ts",
|
||||||
|
"./provider-package": "./src/provider-package.ts",
|
||||||
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
||||||
"./providers/anthropic": "./src/providers/anthropic.ts",
|
"./providers/anthropic": "./src/providers/anthropic.ts",
|
||||||
"./providers/azure": "./src/providers/azure.ts",
|
"./providers/azure": "./src/providers/azure.ts",
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
||||||
"./providers/google": "./src/providers/google.ts",
|
"./providers/google": "./src/providers/google.ts",
|
||||||
"./providers/openai": "./src/providers/openai.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": "./src/providers/openai-compatible.ts",
|
||||||
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
||||||
"./providers/openrouter": "./src/providers/openrouter.ts",
|
"./providers/openrouter": "./src/providers/openrouter.ts",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { LLMClient } from "./route/client"
|
export { LLMClient } from "./route/client"
|
||||||
export { Auth } from "./route/auth"
|
export { Auth } from "./route/auth"
|
||||||
export { Provider } from "./provider"
|
export { Provider } from "./provider"
|
||||||
|
export { ProviderPackage } from "./provider-package"
|
||||||
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
||||||
export type {
|
export type {
|
||||||
RouteModelInput,
|
RouteModelInput,
|
||||||
@@ -31,3 +32,7 @@ export type {
|
|||||||
ModelFactory as ProviderModelFactory,
|
ModelFactory as ProviderModelFactory,
|
||||||
ModelOptions as ProviderModelOptions,
|
ModelOptions as ProviderModelOptions,
|
||||||
} from "./provider"
|
} from "./provider"
|
||||||
|
export type {
|
||||||
|
Definition as ProviderPackageDefinition,
|
||||||
|
Settings as ProviderPackageSettings,
|
||||||
|
} from "./provider-package"
|
||||||
|
|||||||
@@ -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: (modelID: string, settings: ProviderSettings) => Model
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as ProviderPackage from "./provider-package"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { Auth } from "../route/auth"
|
import { Auth } from "../route/auth"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as BedrockConverse from "../protocols/bedrock-converse"
|
import * as BedrockConverse from "../protocols/bedrock-converse"
|
||||||
import type { BedrockCredentials } 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.<region>.amazonaws.com` URL. */
|
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
|
||||||
readonly baseURL?: string
|
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]
|
export const routes = [BedrockConverse.route]
|
||||||
|
|
||||||
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
|
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 provider = configure()
|
||||||
export const model = provider.model
|
export const model: ProviderPackage.Definition<Settings>["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)
|
||||||
|
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(modelID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { Auth } from "../route/auth"
|
import { Auth } from "../route/auth"
|
||||||
import type { ProviderAuthOption } from "../route/auth-options"
|
import type { ProviderAuthOption } from "../route/auth-options"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as AnthropicMessages from "../protocols/anthropic-messages"
|
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 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">) => {
|
const auth = (options: ProviderAuthOption<"optional">) => {
|
||||||
if ("auth" in options && options.auth) return options.auth
|
if ("auth" in options && options.auth) return options.auth
|
||||||
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
|
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 provider = configure()
|
||||||
export const model = provider.model
|
export const model: ProviderPackage.Definition<Settings>["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(modelID)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema"
|
|||||||
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
||||||
|
|
||||||
export const id = ProviderID.make("openai-compatible")
|
export const id = ProviderID.make("openai-compatible")
|
||||||
@@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput &
|
|||||||
readonly baseURL: string
|
readonly baseURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Settings extends ProviderPackage.Settings {
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly baseURL: string
|
||||||
|
readonly provider?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type FamilyModelOptions = RouteDefaultsInput &
|
export type FamilyModelOptions = RouteDefaultsInput &
|
||||||
ProviderAuthOption<"optional"> & {
|
ProviderAuthOption<"optional"> & {
|
||||||
readonly baseURL?: string
|
readonly baseURL?: string
|
||||||
@@ -56,6 +63,16 @@ export const provider = {
|
|||||||
configure,
|
configure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const model: ProviderPackage.Definition<Settings>["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,
|
||||||
|
provider: settings.provider,
|
||||||
|
}).model(modelID)
|
||||||
|
|
||||||
export const baseten = define(profiles.baseten)
|
export const baseten = define(profiles.baseten)
|
||||||
export const cerebras = define(profiles.cerebras)
|
export const cerebras = define(profiles.cerebras)
|
||||||
export const deepinfra = define(profiles.deepinfra)
|
export const deepinfra = define(profiles.deepinfra)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
import type { Route, RouteDefaultsInput } from "../route/client"
|
import type { Route, RouteDefaultsInput } from "../route/client"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as OpenAIChat from "../protocols/openai-chat"
|
import * as OpenAIChat from "../protocols/openai-chat"
|
||||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||||
@@ -21,6 +22,14 @@ export type Config = RouteDefaultsInput &
|
|||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
||||||
|
|
||||||
const defaults = (input: Config) => {
|
const defaults = (input: Config) => {
|
||||||
@@ -57,7 +66,25 @@ export const configure = (input: Config = {}) => {
|
|||||||
|
|
||||||
export const provider = configure()
|
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<Settings>["model"] = (modelID, settings) => {
|
||||||
|
const configured = configure(config(settings))
|
||||||
|
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<Settings>["model"] = (modelID, settings) =>
|
||||||
|
configure(config(settings)).chat(modelID)
|
||||||
export const responses = provider.responses
|
export const responses = provider.responses
|
||||||
export const responsesWebSocket = provider.responsesWebSocket
|
export const responsesWebSocket = provider.responsesWebSocket
|
||||||
export const chat = provider.chat
|
export const chat = provider.chat
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { chatModel as model } from "../openai"
|
||||||
|
export type { Settings } from "../openai"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { model } from "../openai"
|
||||||
|
export type { Settings } from "../openai"
|
||||||
@@ -30,7 +30,6 @@ describe("public exports", () => {
|
|||||||
|
|
||||||
test("provider barrels expose user-facing facades", () => {
|
test("provider barrels expose user-facing facades", () => {
|
||||||
expect(OpenAI.model).toBeFunction()
|
expect(OpenAI.model).toBeFunction()
|
||||||
expect(OpenAI.provider.model).toBe(OpenAI.model)
|
|
||||||
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||||
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
||||||
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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/openai-compatible"),
|
||||||
|
import("@opencode-ai/llm/providers/amazon-bedrock"),
|
||||||
|
])
|
||||||
|
|
||||||
|
for (const module of modules) expect(module.model).toBeFunction()
|
||||||
|
expect(modules[0].model).toBe(modules[1].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",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user