mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -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 { Model } from "@opencode-ai/schema/model"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
|
import { withStatics } from "./schema"
|
||||||
|
import type { DeepMutable } from "./schema"
|
||||||
|
|
||||||
export const ID = Model.ID
|
export const ID = Model.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
@@ -8,7 +10,6 @@ export type ID = typeof ID.Type
|
|||||||
export const VariantID = Model.VariantID
|
export const VariantID = Model.VariantID
|
||||||
export type VariantID = typeof VariantID.Type
|
export type VariantID = typeof VariantID.Type
|
||||||
|
|
||||||
// Grouping of models, eg claude opus, claude sonnet
|
|
||||||
export const Family = Model.Family
|
export const Family = Model.Family
|
||||||
export type Family = Model.Family
|
export type Family = Model.Family
|
||||||
|
|
||||||
@@ -20,14 +21,89 @@ export const Cost = Model.Cost
|
|||||||
export const Ref = Model.Ref
|
export const Ref = Model.Ref
|
||||||
export type Ref = typeof Ref.Type
|
export type Ref = typeof Ref.Type
|
||||||
|
|
||||||
export const Api = Model.Api
|
// Temporary runtime schema until core catalog consumers migrate to the flat
|
||||||
export type Api = Model.Api
|
// 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 interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||||
export type Info = Model.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"> & {
|
export type MutableInfo = Omit<DeepMutable<Info>, "api"> & { api: ProviderV2.MutableApi<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 } {
|
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||||
|
|||||||
@@ -1,25 +1,77 @@
|
|||||||
export * as ProviderV2 from "./provider"
|
export * as ProviderV2 from "./provider"
|
||||||
|
|
||||||
import { Types } from "effect"
|
import { Schema } from "effect"
|
||||||
import { Provider } from "@opencode-ai/schema/provider"
|
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 const ID = Provider.ID
|
||||||
export type ID = typeof ID.Type
|
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 const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
||||||
export type Api = Provider.Api
|
export type Api = typeof Api.Type
|
||||||
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 Request = Provider.Request
|
export const Request = Provider.Request
|
||||||
export type Request = Provider.Request
|
export type Request = Provider.Request
|
||||||
|
|
||||||
export const Info = Provider.Info
|
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||||
export type Info = Provider.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.Family, Model.Family],
|
||||||
[ModelV2.Capabilities, Model.Capabilities],
|
[ModelV2.Capabilities, Model.Capabilities],
|
||||||
[ModelV2.Cost, Model.Cost],
|
[ModelV2.Cost, Model.Cost],
|
||||||
[ModelV2.Api, Model.Api],
|
|
||||||
[ModelV2.Info, Model.Info],
|
|
||||||
[ProviderV2.ID, Provider.ID],
|
[ProviderV2.ID, Provider.ID],
|
||||||
[ProviderV2.AISDK, Provider.AISDK],
|
|
||||||
[ProviderV2.Native, Provider.Native],
|
|
||||||
[ProviderV2.Api, Provider.Api],
|
|
||||||
[ProviderV2.Request, Provider.Request],
|
[ProviderV2.Request, Provider.Request],
|
||||||
[ProviderV2.Info, Provider.Info],
|
|
||||||
[corePermission.Effect, Permission.Effect],
|
[corePermission.Effect, Permission.Effect],
|
||||||
[corePermission.Rule, Permission.Rule],
|
[corePermission.Rule, Permission.Rule],
|
||||||
[corePermission.Ruleset, Permission.Ruleset],
|
[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)
|
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(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(
|
expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe(
|
||||||
"directory:/tmp",
|
"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", () => {
|
test("shared record schemas construct and decode plain objects", () => {
|
||||||
const made = Prompt.make({ text: "hello" })
|
const made = Prompt.make({ text: "hello" })
|
||||||
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
|
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
|
||||||
|
|||||||
@@ -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",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -41,34 +41,23 @@ export const Cost = Schema.Struct({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Api = Schema.Union([
|
export interface Variant extends Schema.Schema.Type<typeof Variant> {}
|
||||||
Schema.Struct({
|
export const Variant = Schema.Struct({
|
||||||
id: ID,
|
id: VariantID,
|
||||||
...Provider.AISDK.fields,
|
...Provider.Overlays,
|
||||||
}),
|
})
|
||||||
Schema.Struct({
|
|
||||||
id: ID,
|
|
||||||
...Provider.Native.fields,
|
|
||||||
}),
|
|
||||||
]).pipe(Schema.toTaggedUnion("type"))
|
|
||||||
export type Api = typeof Api.Type
|
|
||||||
|
|
||||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||||
export const Info = Schema.Struct({
|
export const Info = Schema.Struct({
|
||||||
id: ID,
|
id: ID,
|
||||||
|
modelID: ID.pipe(Schema.optional),
|
||||||
providerID: Provider.ID,
|
providerID: Provider.ID,
|
||||||
family: Family.pipe(Schema.optional),
|
family: Family.pipe(Schema.optional),
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
api: Api,
|
package: Provider.Package.pipe(Schema.optional),
|
||||||
|
...Provider.Overlays,
|
||||||
capabilities: Capabilities,
|
capabilities: Capabilities,
|
||||||
request: Schema.Struct({
|
variants: Variant.pipe(Schema.Array, Schema.mutable, Schema.optional),
|
||||||
...Provider.Request.fields,
|
|
||||||
variant: Schema.String.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
variants: Schema.Struct({
|
|
||||||
id: VariantID,
|
|
||||||
...Provider.Request.fields,
|
|
||||||
}).pipe(Schema.Array, Schema.mutable),
|
|
||||||
time: Schema.Struct({
|
time: Schema.Struct({
|
||||||
released: Schema.Finite,
|
released: Schema.Finite,
|
||||||
}),
|
}),
|
||||||
@@ -84,15 +73,12 @@ export const Info = Schema.Struct({
|
|||||||
.annotate({ identifier: "ModelV2.Info" })
|
.annotate({ identifier: "ModelV2.Info" })
|
||||||
.pipe(
|
.pipe(
|
||||||
withStatics((schema) => ({
|
withStatics((schema) => ({
|
||||||
empty: (providerID: Provider.ID, modelID: ID) =>
|
empty: (providerID: Provider.ID, id: ID) =>
|
||||||
schema.make({
|
schema.make({
|
||||||
id: modelID,
|
id,
|
||||||
providerID,
|
providerID,
|
||||||
name: modelID,
|
name: id,
|
||||||
api: { id: modelID, type: "native", settings: {} },
|
|
||||||
capabilities: { tools: false, input: [], output: [] },
|
capabilities: { tools: false, input: [], output: [] },
|
||||||
request: { headers: {}, body: {} },
|
|
||||||
variants: [],
|
|
||||||
time: { released: 0 },
|
time: { released: 0 },
|
||||||
cost: [],
|
cost: [],
|
||||||
status: "active",
|
status: "active",
|
||||||
|
|||||||
@@ -22,23 +22,14 @@ export const ID = Schema.String.pipe(
|
|||||||
)
|
)
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
|
export const Package = Schema.String
|
||||||
export const AISDK = Schema.Struct({
|
export type Package = typeof Package.Type
|
||||||
type: Schema.Literal("aisdk"),
|
|
||||||
package: Schema.String,
|
export const Overlays = {
|
||||||
url: Schema.String.pipe(Schema.optional),
|
|
||||||
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||||
})
|
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||||
|
body: 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
|
|
||||||
|
|
||||||
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
||||||
export const Request = Schema.Struct({
|
export const Request = Schema.Struct({
|
||||||
@@ -52,18 +43,12 @@ export const Info = Schema.Struct({
|
|||||||
integrationID: Integration.ID.pipe(Schema.optional),
|
integrationID: Integration.ID.pipe(Schema.optional),
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||||
api: Api,
|
package: Package,
|
||||||
request: Request,
|
...Overlays,
|
||||||
})
|
})
|
||||||
.annotate({ identifier: "ProviderV2.Info" })
|
.annotate({ identifier: "ProviderV2.Info" })
|
||||||
.pipe(
|
.pipe(
|
||||||
withStatics((schema) => ({
|
withStatics((schema) => ({
|
||||||
empty: (id: ID) =>
|
empty: (id: ID) => schema.make({ id, name: id, package: "" }),
|
||||||
schema.make({
|
|
||||||
id,
|
|
||||||
name: id,
|
|
||||||
api: { type: "native", settings: {} },
|
|
||||||
request: { headers: {}, body: {} },
|
|
||||||
}),
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Effect } from "effect"
|
|||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../location"
|
import { response } from "../location"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
||||||
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -10,7 +11,7 @@ export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers)
|
|||||||
"model.list",
|
"model.list",
|
||||||
Effect.fn(function* () {
|
Effect.fn(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
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 { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { ProviderNotFoundError } from "@opencode-ai/protocol/errors"
|
import { ProviderNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { response } from "../location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
||||||
@@ -12,7 +13,9 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han
|
|||||||
"provider.list",
|
"provider.list",
|
||||||
Effect.fn(function* () {
|
Effect.fn(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
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(
|
.handle(
|
||||||
@@ -25,7 +28,7 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han
|
|||||||
providerID: ctx.params.providerID,
|
providerID: ctx.params.providerID,
|
||||||
message: `Provider not found: ${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