Compare commits

...

14 Commits

Author SHA1 Message Date
Shoubhit Dash 8efd8783d0 fix(schema): adapt legacy catalog responses 2026-06-25 20:36:02 +05:30
Shoubhit Dash 5436b55a7e chore: sync native provider schema stack 2026-06-25 20:32:36 +05:30
Shoubhit Dash 3a09383d33 refactor(llm): name provider model identity 2026-06-25 20:27:22 +05:30
Shoubhit Dash beefda1f8f refactor(llm): colocate provider package logic 2026-06-25 20:27:22 +05:30
Shoubhit Dash 2f85f4744d feat(llm): add native provider packages 2026-06-25 20:27:22 +05:30
Shoubhit Dash 1a9bd8c1fd refactor(schema): flatten provider identity 2026-06-25 19:49:14 +05:30
Shoubhit Dash 315ba3720e chore: sync native provider stack 2026-06-25 00:24:57 +05:30
Shoubhit Dash d1446a9e72 refactor(llm): name provider model identity 2026-06-25 00:20:03 +05:30
Shoubhit Dash 928510608d refactor(llm): colocate provider package logic 2026-06-25 00:20:02 +05:30
Shoubhit Dash 2b0b573474 feat(llm): add native provider packages 2026-06-25 00:20:02 +05:30
Shoubhit Dash 62e44a80e7 feat(core): migrate native provider config 2026-06-25 00:17:32 +05:30
Shoubhit Dash 9848587052 refactor(llm): name provider model identity 2026-06-24 23:29:06 +05:30
Shoubhit Dash 547496656c refactor(llm): colocate provider package logic 2026-06-24 23:10:45 +05:30
Shoubhit Dash 733bb06134 feat(llm): add native provider packages 2026-06-24 23:05:02 +05:30
18 changed files with 369 additions and 87 deletions
+84 -8
View File
@@ -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 } {
+63 -11
View File
@@ -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,
})
}
+39 -10
View File
@@ -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" })
+3
View File
@@ -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",
+5
View File
@@ -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
View 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: (modelID: string, settings: ProviderSettings) => Model
}
export * as ProviderPackage from "./provider-package"
+26 -1
View File
@@ -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)
}
+14 -1
View File
@@ -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)
+28 -1
View File
@@ -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"
-1
View File
@@ -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",
)
})
})
+12 -26
View File
@@ -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",
+10 -25
View File
@@ -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: "" }),
})),
)
+2 -1
View File
@@ -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))))
}),
)
}),
+5 -2
View File
@@ -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)))
}),
)
}),