mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-27 13:41:34 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8efd8783d0 | |||
| 5436b55a7e | |||
| 3a09383d33 | |||
| beefda1f8f | |||
| 2f85f4744d | |||
| 1a9bd8c1fd | |||
| 315ba3720e | |||
| d1446a9e72 | |||
| 928510608d | |||
| 2b0b573474 | |||
| 62e44a80e7 | |||
| 9848587052 | |||
| 547496656c | |||
| 733bb06134 |
@@ -1,6 +1,8 @@
|
||||
import { Types } from "effect"
|
||||
import { Schema } from "effect"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { withStatics } from "./schema"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Model.ID
|
||||
export type ID = typeof ID.Type
|
||||
@@ -8,7 +10,6 @@ export type ID = typeof ID.Type
|
||||
export const VariantID = Model.VariantID
|
||||
export type VariantID = typeof VariantID.Type
|
||||
|
||||
// Grouping of models, eg claude opus, claude sonnet
|
||||
export const Family = Model.Family
|
||||
export type Family = Model.Family
|
||||
|
||||
@@ -20,14 +21,89 @@ export const Cost = Model.Cost
|
||||
export const Ref = Model.Ref
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
export const Api = Model.Api
|
||||
export type Api = Model.Api
|
||||
// Temporary runtime schema until core catalog consumers migrate to the flat
|
||||
// package identity in @opencode-ai/schema.
|
||||
export const Api = Schema.Union([
|
||||
Schema.Struct({ id: ID, ...ProviderV2.AISDK.fields }),
|
||||
Schema.Struct({ id: ID, ...ProviderV2.Native.fields }),
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
|
||||
export const Info = Model.Info
|
||||
export type Info = Model.Info
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
family: Family.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
api: Api,
|
||||
capabilities: Capabilities,
|
||||
request: Schema.Struct({
|
||||
...ProviderV2.Request.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
variants: Schema.Struct({
|
||||
id: VariantID,
|
||||
...ProviderV2.Request.fields,
|
||||
}).pipe(Schema.Array, Schema.mutable),
|
||||
time: Schema.Struct({ released: Schema.Finite }),
|
||||
cost: Cost.pipe(Schema.Array, Schema.mutable),
|
||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||
enabled: Schema.Boolean,
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Int,
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
output: Schema.Int,
|
||||
}),
|
||||
})
|
||||
.annotate({ identifier: "ModelV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (providerID: ProviderV2.ID, modelID: ID) =>
|
||||
schema.make({
|
||||
id: modelID,
|
||||
providerID,
|
||||
name: modelID,
|
||||
api: { id: modelID, type: "native", settings: {} },
|
||||
capabilities: { tools: false, input: [], output: [] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 0, output: 0 },
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
|
||||
api: ProviderV2.MutableApi<Api>
|
||||
export type MutableInfo = Omit<DeepMutable<Info>, "api"> & { api: ProviderV2.MutableApi<Api> }
|
||||
|
||||
export function flatten(info: Info): Model.Info {
|
||||
return Model.Info.make({
|
||||
id: info.id,
|
||||
modelID: info.api.id === info.id ? undefined : info.api.id,
|
||||
providerID: info.providerID,
|
||||
family: info.family,
|
||||
name: info.name,
|
||||
package: info.api.type === "aisdk" ? `aisdk:${info.api.package}` : undefined,
|
||||
settings: {
|
||||
...info.api.settings,
|
||||
...(info.api.url === undefined ? {} : { baseURL: info.api.url }),
|
||||
},
|
||||
headers: info.request.headers,
|
||||
body: info.request.body,
|
||||
capabilities: info.capabilities,
|
||||
variants: info.variants.map((variant) => ({
|
||||
id: variant.id,
|
||||
headers: variant.headers,
|
||||
body: variant.body,
|
||||
})),
|
||||
time: info.time,
|
||||
cost: info.cost,
|
||||
status: info.status,
|
||||
enabled: info.enabled,
|
||||
limit: info.limit,
|
||||
})
|
||||
}
|
||||
|
||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||
|
||||
@@ -1,25 +1,77 @@
|
||||
export * as ProviderV2 from "./provider"
|
||||
|
||||
import { Types } from "effect"
|
||||
import { Schema } from "effect"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Integration } from "./integration"
|
||||
import { withStatics } from "./schema"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const AISDK = Provider.AISDK
|
||||
// Temporary runtime schema until core catalog consumers migrate to the flat
|
||||
// package identity in @opencode-ai/schema.
|
||||
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
|
||||
export const AISDK = Schema.Struct({
|
||||
type: Schema.Literal("aisdk"),
|
||||
package: Schema.String,
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export const Native = Provider.Native
|
||||
export interface Native extends Schema.Schema.Type<typeof Native> {}
|
||||
export const Native = Schema.Struct({
|
||||
type: Schema.Literal("native"),
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown),
|
||||
})
|
||||
|
||||
export const Api = Provider.Api
|
||||
export type Api = Provider.Api
|
||||
export type MutableApi<T extends Api = Api> = T extends Api
|
||||
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
|
||||
: never
|
||||
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
|
||||
export const Request = Provider.Request
|
||||
export type Request = Provider.Request
|
||||
|
||||
export const Info = Provider.Info
|
||||
export type Info = Provider.Info
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
integrationID: Integration.ID.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
api: Api,
|
||||
request: Request,
|
||||
})
|
||||
.annotate({ identifier: "ProviderV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({
|
||||
id,
|
||||
name: id,
|
||||
api: { type: "native", settings: {} },
|
||||
request: { headers: {}, body: {} },
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
|
||||
export type MutableApi<T extends Api = Api> = T extends Api
|
||||
? Omit<DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
|
||||
: never
|
||||
|
||||
export type MutableInfo = Omit<DeepMutable<Info>, "api"> & { api: MutableApi }
|
||||
|
||||
export function flatten(info: Info): Provider.Info {
|
||||
return Provider.Info.make({
|
||||
id: info.id,
|
||||
integrationID: info.integrationID,
|
||||
name: info.name,
|
||||
disabled: info.disabled,
|
||||
package: info.api.type === "aisdk" ? `aisdk:${info.api.package}` : "",
|
||||
settings: {
|
||||
...info.api.settings,
|
||||
...(info.api.url === undefined ? {} : { baseURL: info.api.url }),
|
||||
},
|
||||
headers: info.request.headers,
|
||||
body: info.request.body,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,14 +100,8 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[ModelV2.Family, Model.Family],
|
||||
[ModelV2.Capabilities, Model.Capabilities],
|
||||
[ModelV2.Cost, Model.Cost],
|
||||
[ModelV2.Api, Model.Api],
|
||||
[ModelV2.Info, Model.Info],
|
||||
[ProviderV2.ID, Provider.ID],
|
||||
[ProviderV2.AISDK, Provider.AISDK],
|
||||
[ProviderV2.Native, Provider.Native],
|
||||
[ProviderV2.Api, Provider.Api],
|
||||
[ProviderV2.Request, Provider.Request],
|
||||
[ProviderV2.Info, Provider.Info],
|
||||
[corePermission.Effect, Permission.Effect],
|
||||
[corePermission.Rule, Permission.Rule],
|
||||
[corePermission.Ruleset, Permission.Ruleset],
|
||||
@@ -155,15 +149,50 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
for (const [core, shared] of schemas) expect(core).toBe(shared)
|
||||
|
||||
expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test")))
|
||||
expect(Model.Info.empty(Provider.ID.make("test"), Model.ID.make("model"))).toEqual(
|
||||
ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")),
|
||||
)
|
||||
expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test")))
|
||||
expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe(
|
||||
"directory:/tmp",
|
||||
)
|
||||
})
|
||||
|
||||
test("shared provider schemas use flat package identity", () => {
|
||||
expect(
|
||||
Schema.decodeUnknownSync(Provider.Info)({
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
package: "@opencode-ai/llm/providers/openai",
|
||||
settings: { baseURL: "https://api.openai.com/v1" },
|
||||
headers: { "x-application": "opencode" },
|
||||
body: { service_tier: "priority" },
|
||||
}),
|
||||
).toMatchObject({ package: "@opencode-ai/llm/providers/openai" })
|
||||
expect(
|
||||
Schema.decodeUnknownSync(Provider.Info)({
|
||||
id: "legacy",
|
||||
name: "Legacy",
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
}),
|
||||
).toMatchObject({ package: "aisdk:@ai-sdk/openai" })
|
||||
expect(
|
||||
Schema.decodeUnknownSync(Model.Info)({
|
||||
id: "claude-sonnet",
|
||||
modelID: "us.anthropic.claude-sonnet-4-6",
|
||||
providerID: "amazon-bedrock",
|
||||
name: "Claude Sonnet",
|
||||
package: "@opencode-ai/llm/providers/amazon-bedrock",
|
||||
settings: { region: "us-west-2" },
|
||||
headers: {},
|
||||
body: { additionalModelRequestFields: { reasoning_config: { type: "enabled" } } },
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
variants: [{ id: "max", settings: {}, body: { budget_tokens: 31999 } }],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 200000, output: 64000 },
|
||||
}),
|
||||
).toMatchObject({ id: "claude-sonnet", modelID: "us.anthropic.claude-sonnet-4-6" })
|
||||
})
|
||||
|
||||
test("shared record schemas construct and decode plain objects", () => {
|
||||
const made = Prompt.make({ text: "hello" })
|
||||
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"./route": "./src/route/index.ts",
|
||||
"./provider": "./src/provider.ts",
|
||||
"./providers": "./src/providers/index.ts",
|
||||
"./provider-package": "./src/provider-package.ts",
|
||||
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
||||
"./providers/anthropic": "./src/providers/anthropic.ts",
|
||||
"./providers/azure": "./src/providers/azure.ts",
|
||||
@@ -22,6 +23,8 @@
|
||||
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
||||
"./providers/google": "./src/providers/google.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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 { 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.<region>.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<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 { 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<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 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<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 cerebras = define(profiles.cerebras)
|
||||
export const deepinfra = define(profiles.deepinfra)
|
||||
|
||||
@@ -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<Record<string, string>>
|
||||
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<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 responsesWebSocket = provider.responsesWebSocket
|
||||
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", () => {
|
||||
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()
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -41,34 +41,23 @@ export const Cost = Schema.Struct({
|
||||
}),
|
||||
})
|
||||
|
||||
export const Api = Schema.Union([
|
||||
Schema.Struct({
|
||||
id: ID,
|
||||
...Provider.AISDK.fields,
|
||||
}),
|
||||
Schema.Struct({
|
||||
id: ID,
|
||||
...Provider.Native.fields,
|
||||
}),
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
export interface Variant extends Schema.Schema.Type<typeof Variant> {}
|
||||
export const Variant = Schema.Struct({
|
||||
id: VariantID,
|
||||
...Provider.Overlays,
|
||||
})
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
modelID: ID.pipe(Schema.optional),
|
||||
providerID: Provider.ID,
|
||||
family: Family.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
api: Api,
|
||||
package: Provider.Package.pipe(Schema.optional),
|
||||
...Provider.Overlays,
|
||||
capabilities: Capabilities,
|
||||
request: Schema.Struct({
|
||||
...Provider.Request.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
variants: Schema.Struct({
|
||||
id: VariantID,
|
||||
...Provider.Request.fields,
|
||||
}).pipe(Schema.Array, Schema.mutable),
|
||||
variants: Variant.pipe(Schema.Array, Schema.mutable, Schema.optional),
|
||||
time: Schema.Struct({
|
||||
released: Schema.Finite,
|
||||
}),
|
||||
@@ -84,15 +73,12 @@ export const Info = Schema.Struct({
|
||||
.annotate({ identifier: "ModelV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (providerID: Provider.ID, modelID: ID) =>
|
||||
empty: (providerID: Provider.ID, id: ID) =>
|
||||
schema.make({
|
||||
id: modelID,
|
||||
id,
|
||||
providerID,
|
||||
name: modelID,
|
||||
api: { id: modelID, type: "native", settings: {} },
|
||||
name: id,
|
||||
capabilities: { tools: false, input: [], output: [] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "active",
|
||||
|
||||
@@ -22,23 +22,14 @@ export const ID = Schema.String.pipe(
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
|
||||
export const AISDK = Schema.Struct({
|
||||
type: Schema.Literal("aisdk"),
|
||||
package: Schema.String,
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
export const Package = Schema.String
|
||||
export type Package = typeof Package.Type
|
||||
|
||||
export const Overlays = {
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export interface Native extends Schema.Schema.Type<typeof Native> {}
|
||||
export const Native = Schema.Struct({
|
||||
type: Schema.Literal("native"),
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown),
|
||||
})
|
||||
|
||||
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}
|
||||
|
||||
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
||||
export const Request = Schema.Struct({
|
||||
@@ -52,18 +43,12 @@ export const Info = Schema.Struct({
|
||||
integrationID: Integration.ID.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
api: Api,
|
||||
request: Request,
|
||||
package: Package,
|
||||
...Overlays,
|
||||
})
|
||||
.annotate({ identifier: "ProviderV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({
|
||||
id,
|
||||
name: id,
|
||||
api: { type: "native", settings: {} },
|
||||
request: { headers: {}, body: {} },
|
||||
}),
|
||||
empty: (id: ID) => schema.make({ id, name: id, package: "" }),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -10,7 +11,7 @@ export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers)
|
||||
"model.list",
|
||||
Effect.fn(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
return yield* response(catalog.model.available())
|
||||
return yield* response(catalog.model.available().pipe(Effect.map((models) => models.map(ModelV2.flatten))))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { ProviderNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { response } from "../location"
|
||||
|
||||
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
||||
@@ -12,7 +13,9 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han
|
||||
"provider.list",
|
||||
Effect.fn(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
return yield* response(catalog.provider.available())
|
||||
return yield* response(
|
||||
catalog.provider.available().pipe(Effect.map((providers) => providers.map(ProviderV2.flatten))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
@@ -25,7 +28,7 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han
|
||||
providerID: ctx.params.providerID,
|
||||
message: `Provider not found: ${ctx.params.providerID}`,
|
||||
})
|
||||
return yield* response(Effect.succeed(provider))
|
||||
return yield* response(Effect.succeed(ProviderV2.flatten(provider)))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user