From 17f312d537c4950404cb686f7f82d7f78bfe0ef5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 24 Jun 2026 10:51:41 -0400 Subject: [PATCH 1/7] refactor(core): simplify model requests --- CONTEXT.md | 9 - packages/core/src/catalog.ts | 4 +- packages/core/src/config/plugin/provider.ts | 19 +- packages/core/src/model-request.ts | 102 ----- packages/core/src/plugin/models-dev.ts | 18 +- packages/core/src/plugin/provider/opencode.ts | 11 +- packages/core/src/session/runner/model.ts | 8 +- packages/core/src/v1/config/migrate.ts | 9 +- packages/core/test/catalog.test.ts | 7 +- packages/core/test/config/config.test.ts | 4 +- packages/core/test/config/provider.test.ts | 10 +- packages/core/test/model-request.test.ts | 44 --- packages/core/test/plugin/host.ts | 10 - .../test/plugin/provider-opencode.test.ts | 7 +- .../core/test/session-runner-model.test.ts | 57 ++- packages/core/test/shared-schema.test.ts | 5 - packages/schema/src/index.ts | 1 - packages/schema/src/model-request.ts | 31 -- packages/schema/src/model.ts | 7 +- packages/sdk/js/src/v2/gen/types.gen.ts | 26 -- packages/sdk/openapi.json | 352 ------------------ 21 files changed, 56 insertions(+), 685 deletions(-) delete mode 100644 packages/core/src/model-request.ts delete mode 100644 packages/core/test/model-request.test.ts delete mode 100644 packages/schema/src/model-request.ts diff --git a/CONTEXT.md b/CONTEXT.md index 7fe7f3fc8d..4b5fabd93c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -57,13 +57,6 @@ The bounded projection of a Core-executed tool result persisted in Session histo **Managed Tool Output File**: A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history. -**Model Request Options**: -Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request. -_Avoid_: Request body, wire options - -**Generation Controls**: -Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog. - **PTY Environment**: The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory. @@ -114,8 +107,6 @@ The host-supplied environment overlay applied by the server when creating a PTY, - A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix. - Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history. - A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn. -- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding. -- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing. - The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`. - A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter. - A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 0b274299e5..431aa004b3 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -2,7 +2,6 @@ export * as Catalog from "./catalog" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" import { ModelV2 } from "./model" -import { ModelRequest } from "./model-request" import { ProviderV2 } from "./provider" import { EventV2 } from "./event" import { Policy } from "./policy" @@ -86,7 +85,8 @@ export const layer = Layer.effect( ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } : model.api const request = { - ...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request), + headers: { ...provider.request.headers, ...model.request.headers }, + body: { ...provider.request.body, ...model.request.body }, variant: model.request.variant, } return ModelV2.Info.make({ diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 6fb13f1c63..6f6e0528da 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -4,7 +4,6 @@ import { define } from "../../plugin/internal" import { Effect } from "effect" import { Config } from "../../config" import { ModelV2 } from "../../model" -import { ModelRequest } from "../../model-request" import { ProviderV2 } from "../../provider" export const Plugin = define({ @@ -59,15 +58,11 @@ export const Plugin = define({ Object.assign(provider.request.body, item.request.body) } }) - const providerApi = catalog.provider.get(providerID)?.provider.api - const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined - for (const [id, config] of Object.entries(item.models ?? {})) { catalog.model.update(providerID, id, (model) => { if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name if (config.api !== undefined) model.api = { ...model.api, ...config.api } - const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, @@ -76,10 +71,8 @@ export const Plugin = define({ } } if (config.request !== undefined) { - ModelRequest.assign(model.request, { - headers: config.request.headers, - ...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}), - }) + Object.assign(model.request.headers, config.request.headers) + Object.assign(model.request.body, config.request.body) if (config.request.variant !== undefined) model.request.variant = config.request.variant } if (config.variants !== undefined) { @@ -90,15 +83,11 @@ export const Plugin = define({ id: variant.id, headers: {}, body: {}, - generation: {}, - options: {}, } model.variants.push(existing) } - ModelRequest.assign(existing, { - headers: variant.headers, - ...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}), - }) + Object.assign(existing.headers, variant.headers) + Object.assign(existing.body, variant.body) } } if (config.cost !== undefined) { diff --git a/packages/core/src/model-request.ts b/packages/core/src/model-request.ts deleted file mode 100644 index 06059a9d73..0000000000 --- a/packages/core/src/model-request.ts +++ /dev/null @@ -1,102 +0,0 @@ -export * as ModelRequest from "./model-request" - -import { ModelRequest } from "@opencode-ai/schema/model-request" - -export const Generation = ModelRequest.Generation -export type Generation = ModelRequest.Generation - -export const Request = ModelRequest.Request -export type Request = ModelRequest.Request - -interface MutableRequest { - headers: Record - body: Record - generation?: Record - options?: Record -} - -const generationKeys = new Map([ - ["maxOutputTokens", "maxTokens"], - ["maxTokens", "maxTokens"], - ["temperature", "temperature"], - ["topP", "topP"], - ["topK", "topK"], - ["frequencyPenalty", "frequencyPenalty"], - ["presencePenalty", "presencePenalty"], - ["seed", "seed"], - ["stopSequences", "stop"], - ["stop", "stop"], -]) - -interface Profile { - readonly namespace: string - readonly semantics: ReadonlyMap -} - -const profiles = new Map([ - [ - "@ai-sdk/openai", - { - namespace: "openai", - semantics: new Map([ - ["store", "store"], - ["promptCacheKey", "promptCacheKey"], - ["reasoningEffort", "reasoningEffort"], - ["reasoningSummary", "reasoningSummary"], - ["include", "include"], - ["textVerbosity", "textVerbosity"], - ["serviceTier", "serviceTier"], - ["service_tier", "serviceTier"], - ]), - }, - ], - [ - "@ai-sdk/openai-compatible", - { - namespace: "openai", - semantics: new Map([ - ["store", "store"], - ["promptCacheKey", "promptCacheKey"], - ["reasoningEffort", "reasoningEffort"], - ["reasoning_effort", "reasoningEffort"], - ]), - }, - ], - ["@ai-sdk/anthropic", { namespace: "anthropic", semantics: new Map([["thinking", "thinking"]]) }], -]) - -export const namespace = (packageName: string) => profiles.get(packageName)?.namespace - -export const merge = (base: Request, override: Partial) => ({ - headers: { ...base.headers, ...override.headers }, - body: { ...base.body, ...override.body }, - generation: { ...base.generation, ...override.generation }, - options: { ...base.options, ...override.options }, -}) - -export const assign = (target: MutableRequest, override: Partial) => { - Object.assign(target.headers, override.headers) - Object.assign(target.body, override.body) - Object.assign((target.generation ??= {}), override.generation) - Object.assign((target.options ??= {}), override.options) -} - -/** Partitions AI-SDK-shaped request options before they enter the Catalog. */ -export function normalizeAiSdkOptions(packageName: string | undefined, input: Readonly>) { - const generation: Record> = {} - const options: Record = {} - const body: Record = {} - const semantics = profiles.get(packageName ?? "")?.semantics - - for (const [key, value] of Object.entries(input)) { - const generationKey = generationKeys.get(key) - if (generationKey === "stop" && Array.isArray(value) && value.every((item) => typeof item === "string")) - generation[generationKey] = value - else if (generationKey !== undefined && generationKey !== "stop" && typeof value === "number") - generation[generationKey] = value - else if (semantics?.has(key)) options[semantics.get(key)!] = value - else body[key] = value - } - - return { generation, options, body } -} diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 04f1f092a0..1adc979d25 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -2,7 +2,6 @@ import { define } from "./internal" import { Effect, Stream } from "effect" import { EventV2 } from "../event" import { ModelV2 } from "../model" -import { ModelRequest } from "../model-request" import { ModelsDev } from "../models-dev" import { ProviderV2 } from "../provider" @@ -38,15 +37,12 @@ function cost(input: ModelsDev.Model["cost"]) { ] } -function variants(model: ModelsDev.Model, packageName?: string) { - return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => { - const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {}) - return { - id: ModelV2.VariantID.make(id), - headers: { ...(item.provider?.headers ?? {}) }, - ...request, - } - }) +function variants(model: ModelsDev.Model) { + return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({ + id: ModelV2.VariantID.make(id), + headers: { ...(item.provider?.headers ?? {}) }, + body: { ...(item.provider?.body ?? {}) }, + })) } export const ModelsDevPlugin = define({ @@ -115,7 +111,7 @@ export const ModelsDevPlugin = define({ input: [...(model.modalities?.input ?? [])], output: [...(model.modalities?.output ?? [])], } - draft.variants = variants(model, model.provider?.npm ?? item.npm) + draft.variants = variants(model) draft.time.released = released(model.release_date) draft.cost = cost(model.cost) draft.status = model.status ?? "active" diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 691a2b2a28..87a02d328a 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -8,9 +8,9 @@ import { EventV2 } from "../../event" import { Credential } from "../../credential" import { Integration } from "../../integration" import { ModelV2 } from "../../model" -import { ModelRequest } from "../../model-request" import { ProviderV2 } from "../../provider" import { ConfigProviderV1 } from "../../v1/config/provider" +import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" import { ConfigV1 } from "../../v1/config/config" const defaultServer = "https://console.opencode.ai" @@ -142,15 +142,14 @@ export const OpencodePlugin = define ({ id: ModelV2.VariantID.make(id), headers: { ...(options.headers ?? {}) }, - ...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(options)), + body: lowerer.request(withoutCredentials(options)), })) } if (config.release_date !== undefined) { diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 97b862e432..b864df8ba3 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -11,7 +11,6 @@ import { Catalog } from "../../catalog" import { Credential } from "../../credential" import { Integration } from "../../integration" import { ModelV2 } from "../../model" -import { ModelRequest } from "../../model-request" import { ProviderV2 } from "../../provider" import { SessionSchema } from "../schema" @@ -88,8 +87,6 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { } const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { - const options = model.request.options ?? {} - const namespace = model.api.type === "aisdk" ? ModelRequest.namespace(model.api.package) : undefined const body = model.request.body const httpBody = Object.hasOwn(body, "apiKey") ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey")) @@ -98,8 +95,6 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { provider: model.providerID, endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url }, headers: model.request.headers, - generation: model.request.generation, - providerOptions: namespace && Object.keys(options).length > 0 ? { [namespace]: options } : undefined, http: { body: httpBody }, limits: { context: model.limit.context, output: model.limit.output }, }) @@ -122,7 +117,8 @@ const withVariant = ( return Effect.succeed( variant ? produce(model, (draft) => { - ModelRequest.assign(draft.request, variant) + Object.assign(draft.request.headers, variant.headers) + Object.assign(draft.request.body, variant.body) }) : model, ) diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index c474cac51a..1583c6c1d5 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -6,7 +6,6 @@ import { ConfigMCPV1 } from "./mcp" import { ConfigPermissionV1 } from "./permission" import { ConfigProviderV1 } from "./provider" import { ConfigProviderOptionsV1 } from "./provider-options" -import { ModelRequest } from "../../model-request" const keys = new Set([ "logLevel", @@ -192,11 +191,7 @@ function migrateProvider(info: ConfigProviderV1.Info) { function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) { const packageID = info.provider?.npm ?? packageName const lowerer = ConfigProviderOptionsV1.get(packageID) - const ingest = (options: Readonly>) => { - const request = ModelRequest.normalizeAiSdkOptions(packageID, options) - return { ...lowerer.request(request.body), ...request.generation, ...request.options } - } - const request = info.options && ingest(info.options) + const request = info.options && lowerer.request(info.options) const costs = info.cost && [ { input: info.cost.input, @@ -241,7 +236,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st info.variants && Object.entries(info.variants).map(([id, options]) => ({ id, - body: ingest(options), + body: lowerer.request(options), })), cost: costs, disabled: info.status === "deprecated" ? true : undefined, diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 23a3a0f4aa..a3a1c4972d 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -233,16 +233,13 @@ describe("CatalogV2", () => { model.request.headers.shared = "model" model.request.body.model = true model.request.body.request = true - const options = (model.request.options ??= {}) - options.shared = "model" - options.model = true + model.request.body.shared = "model" }) }) const model = required(yield* catalog.model.get(providerID, modelID)) expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) - expect(model.request.body).toEqual({ provider: true, model: true, request: true }) - expect(model.request.options).toEqual({ shared: "model", model: true }) + expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" }) }), ) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 6275d8fed3..b092bb5582 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -599,9 +599,9 @@ describe("Config", () => { models: { model: { request: { - body: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" }, + body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" }, }, - variants: [{ id: "high", body: { reasoningEffort: "high", reasoningSummary: "auto" } }], + variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }], }, }, }) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 12f4a01c78..605bcd63cf 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -55,7 +55,7 @@ function request(headers: Record, variant?: string) { const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { - it.effect("partitions existing model variant bodies without changing config shape", () => + it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.opencode @@ -96,8 +96,7 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.variants).toMatchObject([ { id: "high", - body: {}, - options: { + body: { reasoningEffort: "high", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], @@ -107,7 +106,7 @@ describe("ConfigProviderPlugin.Plugin", () => { }), ) - it.effect("uses the effective provider package across layered config", () => + it.effect("keeps layered model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.opencode @@ -147,8 +146,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const model = required(yield* catalog.model.get(providerID, modelID)) expect(model.variants[0]).toMatchObject({ id: "high", - body: {}, - options: { reasoningEffort: "high" }, + body: { reasoningEffort: "high" }, }) }), ) diff --git a/packages/core/test/model-request.test.ts b/packages/core/test/model-request.test.ts deleted file mode 100644 index c8ac12d470..0000000000 --- a/packages/core/test/model-request.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { ModelRequest } from "@opencode-ai/core/model-request" - -describe("ModelRequest", () => { - test("partitions AI SDK model and models.dev mode options", () => { - expect( - ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", { - maxOutputTokens: 4096, - temperature: 0.2, - reasoningEffort: "high", - serviceTier: "priority", - custom_extension: { enabled: true }, - }), - ).toEqual({ - generation: { maxTokens: 4096, temperature: 0.2 }, - options: { reasoningEffort: "high", serviceTier: "priority" }, - body: { custom_extension: { enabled: true } }, - }) - }) - - test("keeps unknown-provider options as compatibility fields", () => { - expect(ModelRequest.normalizeAiSdkOptions(undefined, { temperature: 0.2, reasoningEffort: "high" })).toEqual({ - generation: { temperature: 0.2 }, - options: {}, - body: { reasoningEffort: "high" }, - }) - }) - - test("does not consult inherited package-name properties", () => { - expect(ModelRequest.normalizeAiSdkOptions("__proto__", { reasoningEffort: "high" })).toEqual({ - generation: {}, - options: {}, - body: { reasoningEffort: "high" }, - }) - }) - - test("normalizes models.dev wire aliases owned by native protocols", () => { - expect(ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", { service_tier: "priority" })).toEqual({ - generation: {}, - options: { serviceTier: "priority" }, - body: {}, - }) - }) -}) diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 5ec8c16bd0..4561366d4c 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -290,21 +290,11 @@ function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) { ...value.request, headers: { ...value.request.headers }, body: { ...value.request.body }, - generation: value.request.generation && { - ...value.request.generation, - stop: value.request.generation.stop && [...value.request.generation.stop], - }, - options: value.request.options && { ...value.request.options }, }, variants: value.variants.map((variant) => ({ ...variant, headers: { ...variant.headers }, body: { ...variant.body }, - generation: variant.generation && { - ...variant.generation, - stop: variant.generation.stop && [...variant.generation.stop], - }, - options: variant.options && { ...variant.options }, })), time: { ...value.time }, cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index a2d450d31b..93177c283e 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -150,15 +150,12 @@ describe("OpencodePlugin", () => { cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], limit: { context: 1000, output: 100 }, }) - expect(model.request).toMatchObject({ body: { custom: "value" }, generation: { temperature: 0.5 } }) - expect(model.request.body).toEqual({ custom: "value" }) + expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 }) expect(model.variants).toEqual([ { id: ModelV2.VariantID.make("high"), headers: {}, - body: {}, - generation: { temperature: 0.2 }, - options: {}, + body: { temperature: 0.2 }, }, ]) expect( diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index efebe60d04..94669b24be 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -31,8 +31,6 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) => request: { headers: { "x-test": "header" }, body: { apiKey: "secret", custom_extension: { enabled: true } }, - generation: { temperature: 0.7 }, - options: { store: false, serviceTier: "priority" }, }, variants, time: { released: 0 }, @@ -56,8 +54,6 @@ describe("SessionRunnerModel", () => { defaults: { headers: { "x-test": "header" }, limits: { context: 100, output: 20 }, - generation: { temperature: 0.7 }, - providerOptions: { openai: { store: false, serviceTier: "priority" } }, http: { body: { custom_extension: { enabled: true } } }, }, }) @@ -86,7 +82,7 @@ describe("SessionRunnerModel", () => { url: "https://compatible.example/v1", settings: { apiKey: "settings-secret", compatibility: "strict" }, }), - request: { headers: {}, body: {}, generation: {}, options: {} }, + request: { headers: {}, body: {} }, }), ) const request = LLM.request({ model: resolved, prompt: "Hello" }) @@ -103,21 +99,20 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("lowers selected OpenAI Session variants into Responses options", () => + it.effect("overlays selected OpenAI Session variant bodies", () => Effect.gen(function* () { - const base = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [ + const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [ { id: ModelV2.VariantID.make("high"), headers: { "x-variant": "high" }, - body: {}, - generation: { temperature: 0.2 }, - options: { reasoningEffort: "high" }, + body: { + store: false, + service_tier: "priority", + temperature: 0.2, + reasoning: { effort: "high" }, + }, }, ]) - const catalog = ModelV2.Info.make({ - ...base, - request: { ...base.request, options: { ...base.request.options, reasoningEffort: "medium" } }, - }) const session = SessionV2.Info.make({ id: SessionV2.ID.make("ses_model_variant"), projectID: ProjectV2.ID.global, @@ -134,21 +129,19 @@ describe("SessionRunnerModel", () => { }) const resolved = yield* SessionRunnerModel.resolve(session, catalog) - const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" })) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) - expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } }) - expect(prepared.body).toMatchObject({ + expect(resolved.route.defaults.http?.body).toEqual({ + custom_extension: { enabled: true }, store: false, service_tier: "priority", temperature: 0.2, reasoning: { effort: "high" }, }) - expect(prepared.body).not.toHaveProperty("reasoningEffort") }), ) - it.effect("lowers selected OpenAI-compatible Session variants into Chat options", () => + it.effect("overlays selected OpenAI-compatible Session variant bodies", () => Effect.gen(function* () { const catalog = model( { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" }, @@ -156,9 +149,7 @@ describe("SessionRunnerModel", () => { { id: ModelV2.VariantID.make("high"), headers: {}, - body: {}, - generation: {}, - options: { reasoningEffort: "high" }, + body: { store: false, reasoning_effort: "high" }, }, ], ) @@ -174,14 +165,12 @@ describe("SessionRunnerModel", () => { }) const resolved = yield* SessionRunnerModel.resolve(session, catalog) - const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" })) - expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } }) - expect(prepared.body).toMatchObject({ + expect(resolved.route.defaults.http?.body).toEqual({ + custom_extension: { enabled: true }, store: false, reasoning_effort: "high", }) - expect(prepared.body).not.toHaveProperty("reasoningEffort") }), ) @@ -215,15 +204,13 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("lowers selected Anthropic Session variants into Messages options", () => + it.effect("overlays selected Anthropic Session variant bodies", () => Effect.gen(function* () { const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [ { id: ModelV2.VariantID.make("high"), headers: {}, - body: {}, - generation: {}, - options: { thinking: { type: "enabled", budgetTokens: 12000 } }, + body: { thinking: { type: "enabled", budget_tokens: 12000 } }, }, ]) const session = SessionV2.Info.make({ @@ -238,13 +225,11 @@ describe("SessionRunnerModel", () => { }) const resolved = yield* SessionRunnerModel.resolve(session, catalog) - const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" })) - expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } }) - expect(prepared.body).toMatchObject({ + expect(resolved.route.defaults.http?.body).toEqual({ + custom_extension: { enabled: true }, thinking: { type: "enabled", budget_tokens: 12000 }, }) - expect(JSON.stringify(prepared.body)).not.toContain("budgetTokens") }), ) @@ -266,7 +251,7 @@ describe("SessionRunnerModel", () => { const resolved = yield* SessionRunnerModel.fromCatalogModel( ModelV2.Info.make({ ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), - request: { headers: {}, body: {}, generation: {}, options: {} }, + request: { headers: {}, body: {} }, }), Credential.Key.make({ type: "key", key: "secret" }), ) @@ -289,7 +274,7 @@ describe("SessionRunnerModel", () => { const resolved = yield* SessionRunnerModel.fromCatalogModel( ModelV2.Info.make({ ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), - request: { headers: {}, body: { apiKey: "configured-secret" }, generation: {}, options: {} }, + request: { headers: {}, body: { apiKey: "configured-secret" } }, }), credential, ) diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index a1794b9263..81b813a878 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -19,7 +19,6 @@ import { Credential } from "@opencode-ai/schema/credential" import { FileSystem } from "@opencode-ai/schema/filesystem" import { Integration } from "@opencode-ai/schema/integration" import { LLM } from "@opencode-ai/schema/llm" -import { ModelRequest } from "@opencode-ai/schema/model-request" import { Permission } from "@opencode-ai/schema/permission" import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" @@ -35,7 +34,6 @@ test("Core reuses the canonical shared schemas", async () => { coreIntegration, coreLocation, coreLLM, - coreModelRequest, corePermission, coreProject, coreReference, @@ -53,7 +51,6 @@ test("Core reuses the canonical shared schemas", async () => { import("@opencode-ai/core/integration"), import("@opencode-ai/core/location"), import("@opencode-ai/llm"), - import("@opencode-ai/core/model-request"), import("@opencode-ai/core/permission"), import("@opencode-ai/core/project/schema"), import("@opencode-ai/core/reference"), @@ -111,8 +108,6 @@ test("Core reuses the canonical shared schemas", async () => { [ProviderV2.Api, Provider.Api], [ProviderV2.Request, Provider.Request], [ProviderV2.Info, Provider.Info], - [coreModelRequest.Generation, ModelRequest.Generation], - [coreModelRequest.Request, ModelRequest.Request], [corePermission.Effect, Permission.Effect], [corePermission.Rule, Permission.Rule], [corePermission.Ruleset, Permission.Ruleset], diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 9b30f76f72..e68140d9ee 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -7,7 +7,6 @@ export { Integration } from "./integration" export { LLM } from "./llm" export { Location } from "./location" export { Model } from "./model" -export { ModelRequest } from "./model-request" export { Permission } from "./permission" export { Project } from "./project" export { Provider } from "./provider" diff --git a/packages/schema/src/model-request.ts b/packages/schema/src/model-request.ts deleted file mode 100644 index d829acca66..0000000000 --- a/packages/schema/src/model-request.ts +++ /dev/null @@ -1,31 +0,0 @@ -export * as ModelRequest from "./model-request" - -import { Effect, Schema } from "effect" -import { Provider } from "./provider" - -export interface Generation extends Schema.Schema.Type {} -export const Generation = Schema.Struct({ - maxTokens: Schema.Number.pipe(Schema.optional), - temperature: Schema.Number.pipe(Schema.optional), - topP: Schema.Number.pipe(Schema.optional), - topK: Schema.Number.pipe(Schema.optional), - frequencyPenalty: Schema.Number.pipe(Schema.optional), - presencePenalty: Schema.Number.pipe(Schema.optional), - seed: Schema.Number.pipe(Schema.optional), - stop: Schema.String.pipe(Schema.Array, Schema.mutable, Schema.optional), -}) - -export interface Request extends Schema.Schema.Type {} -export const Request = Schema.Struct({ - ...Provider.Request.fields, - generation: Generation.pipe( - Schema.optionalKey, - Schema.withConstructorDefault(Effect.succeed({})), - Schema.withDecodingDefaultKey(Effect.succeed({})), - ), - options: Schema.Record(Schema.String, Schema.Any).pipe( - Schema.optionalKey, - Schema.withConstructorDefault(Effect.succeed({})), - Schema.withDecodingDefaultKey(Effect.succeed({})), - ), -}) diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 62b76df0fc..a41b540b4d 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -1,7 +1,6 @@ export * as Model from "./model" import { Schema } from "effect" -import { ModelRequest } from "./model-request" import { Provider } from "./provider" import { withStatics } from "./schema" @@ -63,12 +62,12 @@ export const Info = Schema.Struct({ api: Api, capabilities: Capabilities, request: Schema.Struct({ - ...ModelRequest.Request.fields, + ...Provider.Request.fields, variant: Schema.String.pipe(Schema.optional), }), variants: Schema.Struct({ id: VariantID, - ...ModelRequest.Request.fields, + ...Provider.Request.fields, }).pipe(Schema.Array, Schema.mutable), time: Schema.Struct({ released: Schema.Finite, @@ -92,7 +91,7 @@ export const Info = Schema.Struct({ name: modelID, api: { id: modelID, type: "native", settings: {} }, capabilities: { tools: false, input: [], output: [] }, - request: { headers: {}, body: {}, generation: {}, options: {} }, + request: { headers: {}, body: {} }, variants: [], time: { released: 0 }, cost: [], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9f9ddb14c2..3116c295f1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4019,19 +4019,6 @@ export type ModelV2Info = { body: { [key: string]: unknown } - generation?: { - maxTokens?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - temperature?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - topP?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - topK?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - frequencyPenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - presencePenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - seed?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - stop?: Array - } - options?: { - [key: string]: unknown - } variant?: string } variants: Array<{ @@ -4042,19 +4029,6 @@ export type ModelV2Info = { body: { [key: string]: unknown } - generation?: { - maxTokens?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - temperature?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - topP?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - topK?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - frequencyPenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - presencePenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - seed?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - stop?: Array - } - options?: { - [key: string]: unknown - } }> time: { released: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 90f872b2f0..59a01f8a18 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -26833,182 +26833,6 @@ "body": { "type": "object" }, - "generation": { - "type": "object", - "properties": { - "maxTokens": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "temperature": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "topP": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "topK": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "frequencyPenalty": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "presencePenalty": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "seed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "stop": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "options": { - "type": "object" - }, "variant": { "type": "string" } @@ -27032,182 +26856,6 @@ }, "body": { "type": "object" - }, - "generation": { - "type": "object", - "properties": { - "maxTokens": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "temperature": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "topP": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "topK": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "frequencyPenalty": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "presencePenalty": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "seed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "stop": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "options": { - "type": "object" } }, "required": ["id", "headers", "body"], From 42bb79357401cb92d1725032a657c2c74a483712 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 24 Jun 2026 12:18:43 -0400 Subject: [PATCH 2/7] feat(core): generate model variants --- packages/core/src/plugin/internal.ts | 4 +- packages/core/src/plugin/variant.ts | 39 ++++++++++ packages/core/test/plugin/variant.test.ts | 87 +++++++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/plugin/variant.ts create mode 100644 packages/core/test/plugin/variant.test.ts diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 4e80e60337..51c6e279b7 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -29,6 +29,7 @@ import { CommandPlugin } from "./command" import { ModelsDevPlugin } from "./models-dev" import { ProviderPlugins } from "./provider" import { SkillPlugin } from "./skill" +import { VariantPlugin } from "./variant" export type Requirements = | AgentV2.Service @@ -107,12 +108,13 @@ export const locationLayer = Layer.effectDiscard( yield* add(CommandPlugin.Plugin) yield* add(SkillPlugin.Plugin) yield* add(ModelsDevPlugin) - yield* add(ConfigProviderPlugin.Plugin) yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin) for (const item of ProviderPlugins) yield* add(item) yield* add(ConfigExternalPlugin.Plugin) + yield* add(ConfigProviderPlugin.Plugin) + yield* add(VariantPlugin.Plugin) }).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true })) }), ).pipe( diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts new file mode 100644 index 0000000000..05e1653b6e --- /dev/null +++ b/packages/core/src/plugin/variant.ts @@ -0,0 +1,39 @@ +export * as VariantPlugin from "./variant" + +import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import { Effect } from "effect" +import { define } from "./internal" + +export const Plugin = define({ + id: "variant", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform((catalog) => { + for (const record of catalog.provider.list()) { + for (const model of record.models.values()) { + catalog.model.update(model.providerID, model.id, (draft) => { + const generated = generate(draft) + if (generated.length === 0) return + + const explicit = new Map(draft.variants.map((variant) => [variant.id, variant])) + const generatedIDs = new Set(generated.map((variant) => variant.id)) + draft.variants = [ + ...generated.map((variant) => explicit.get(variant.id) ?? variant), + ...draft.variants.filter((variant) => !generatedIDs.has(variant.id)), + ] + }) + } + } + }) + }), +}) + +export function generate(model: ModelV2Info): ModelV2Info["variants"] { + if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return [] + const ids = `${model.id} ${model.api.id}`.toLowerCase() + if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] + return ["high", "max"].map((id) => ({ + id, + headers: {}, + body: { reasoning_effort: id }, + })) +} diff --git a/packages/core/test/plugin/variant.test.ts b/packages/core/test/plugin/variant.test.ts new file mode 100644 index 0000000000..c3075975fa --- /dev/null +++ b/packages/core/test/plugin/variant.test.ts @@ -0,0 +1,87 @@ +import { describe, expect } from "bun:test" +import { Catalog } from "@opencode-ai/core/catalog" +import { Credential } from "@opencode-ai/core/credential" +import { EventV2 } from "@opencode-ai/core/event" +import { Integration } from "@opencode-ai/core/integration" +import { Location } from "@opencode-ai/core/location" +import { ModelV2 } from "@opencode-ai/core/model" +import { Policy } from "@opencode-ai/core/policy" +import { VariantPlugin } from "@opencode-ai/core/plugin/variant" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { Effect, Layer } from "effect" +import { location } from "../fixture/location" +import { testEffect } from "../lib/effect" +import { catalogHost, host } from "./host" + +const events = EventV2.defaultLayer +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), +) +const connections = Credential.defaultLayer.pipe(Layer.fresh) +const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections)) +const catalog = Catalog.layer.pipe( + Layer.provide( + Layer.mergeAll( + events, + locationLayer, + Policy.layer.pipe(Layer.provide(locationLayer)), + connections, + integrations, + ), + ), +) +const it = testEffect( + Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer), +) + +describe("VariantPlugin", () => { + it.effect("adds GLM 5.2 variants after catalog sources", () => + Effect.gen(function* () { + const service = yield* Catalog.Service + yield* service.transform((catalog) => { + catalog.provider.update(ProviderV2.ID.opencode, (provider) => { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } + }) + catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { + model.api = { + id: ModelV2.ID.make("glm-5.2"), + type: "aisdk", + package: "@ai-sdk/openai-compatible", + } + }) + }) + yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) + + expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([ + expect.objectContaining({ id: "high", body: { reasoning_effort: "high" } }), + expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }), + ]) + }), + ) + + it.effect("keeps explicit variants over generated defaults", () => + Effect.gen(function* () { + const service = yield* Catalog.Service + yield* service.transform((catalog) => { + catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { + model.api = { + id: ModelV2.ID.make("glm-5.2"), + type: "aisdk", + package: "@ai-sdk/openai-compatible", + } + model.variants = [ + { id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }, + ] + }) + }) + yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) + + expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([ + expect.objectContaining({ id: "high", headers: { custom: "true" } }), + expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }), + ]) + }), + ) +}) From 31b58b470465977f9b9b6bd9a17bfe3d76f1a229 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 24 Jun 2026 17:21:26 +0000 Subject: [PATCH 3/7] chore: generate --- packages/core/test/plugin/variant.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/test/plugin/variant.test.ts b/packages/core/test/plugin/variant.test.ts index c3075975fa..45476f366c 100644 --- a/packages/core/test/plugin/variant.test.ts +++ b/packages/core/test/plugin/variant.test.ts @@ -23,13 +23,7 @@ const connections = Credential.defaultLayer.pipe(Layer.fresh) const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections)) const catalog = Catalog.layer.pipe( Layer.provide( - Layer.mergeAll( - events, - locationLayer, - Policy.layer.pipe(Layer.provide(locationLayer)), - connections, - integrations, - ), + Layer.mergeAll(events, locationLayer, Policy.layer.pipe(Layer.provide(locationLayer)), connections, integrations), ), ) const it = testEffect( @@ -71,9 +65,7 @@ describe("VariantPlugin", () => { type: "aisdk", package: "@ai-sdk/openai-compatible", } - model.variants = [ - { id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }, - ] + model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }] }) }) yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) From ea5dd9d9408c73e034598e7dc314c9b864ebf649 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 24 Jun 2026 14:34:58 -0400 Subject: [PATCH 4/7] zen: track model tier in stats --- infra/stats.ts | 3 +- .../console/function/src/log-processor.ts | 3 +- packages/session-ui/sst-env.d.ts | 10 +++ sst-env.d.ts | 64 ++++--------------- 4 files changed, 26 insertions(+), 54 deletions(-) create mode 100644 packages/session-ui/sst-env.d.ts diff --git a/infra/stats.ts b/infra/stats.ts index bbf5a3ec33..b5b0e1c600 100644 --- a/infra/stats.ts +++ b/infra/stats.ts @@ -42,11 +42,12 @@ const inferenceEventTable = new aws.s3tables.Table( { name: "request", type: "string", required: false }, { name: "client", type: "string", required: false }, { name: "user_agent", type: "string", required: false }, + { name: "model", type: "string", required: false }, + { name: "model_tier", type: "string", required: false }, { name: "model_variant", type: "string", required: false }, { name: "source", type: "string", required: false }, { name: "provider", type: "string", required: false }, { name: "provider_model", type: "string", required: false }, - { name: "model", type: "string", required: false }, { name: "llm_error_code", type: "int", required: false }, { name: "llm_error_message", type: "string", required: false }, { name: "error_response", type: "string", required: false }, diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 27db46af51..ee8743ef15 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -118,11 +118,12 @@ function toLakeEvent(time: string, data: Record) { request: string(data, "request"), client: string(data, "client"), user_agent: string(data, "user_agent"), + model: string(data, "model"), + model_tier: string(data, "model.tier"), model_variant: string(data, "model.variant"), source: string(data, "source"), provider: string(data, "provider"), provider_model: string(data, "provider.model"), - model: string(data, "model"), llm_error_code: integer(data, "llm.error.code"), llm_error_message: string(data, "llm.error.message"), error_response: string(data, "error.response"), diff --git a/packages/session-ui/sst-env.d.ts b/packages/session-ui/sst-env.d.ts new file mode 100644 index 0000000000..64441936d7 --- /dev/null +++ b/packages/session-ui/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/sst-env.d.ts b/sst-env.d.ts index 6920fe0781..109cb6d81b 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -26,6 +26,14 @@ declare module "sst" { "AuthApi": import("@cloudflare/workers-types").Service "AuthStorage": import("@cloudflare/workers-types").KVNamespace "Bucket": import("@cloudflare/workers-types").R2Bucket + "CLOUDFLARE_API_TOKEN": { + "type": "sst.sst.Secret" + "value": string + } + "CLOUDFLARE_DEFAULT_ACCOUNT_ID": { + "type": "sst.sst.Secret" + "value": string + } "Console": { "type": "sst.cloudflare.SolidStart" "url": string @@ -91,37 +99,6 @@ declare module "sst" { "type": "random.index/randomPassword.RandomPassword" "value": string } - "InferenceEvent": { - "catalog": string - "database": string - "region": string - "table": string - "tableBucket": string - "type": "sst.sst.Linkable" - "workgroup": string - } - "LakeIngest": { - "secret": string - "type": "sst.sst.Linkable" - "url": string - } - "LakeIngestConfig": { - "secret": string - "streamName": string - "type": "sst.sst.Linkable" - } - "LakeIngestSecret": { - "type": "random.index/randomPassword.RandomPassword" - "value": string - } - "LakeIngestService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "LakeVpc": { - "type": "sst.aws.Vpc" - } "LogProcessor": import("@cloudflare/workers-types").Service "R2AccessKey": { "type": "sst.sst.Secret" @@ -155,28 +132,11 @@ declare module "sst" { "type": "sst.sst.Linkable" "value": string } + "SUPPORT_API_KEY": { + "type": "sst.sst.Secret" + "value": string + } "Stat": import("@cloudflare/workers-types").Service - "Stats": { - "type": "sst.cloudflare.SolidStart" - "url": string - } - "StatsDatabase": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.sst.Linkable" - "url": string - "username": string - } - "StatsSyncConfig": { - "dataset": string - "type": "sst.sst.Linkable" - } - "StatsSyncService": { - "service": string - "type": "sst.aws.Service" - } "Teams": { "type": "sst.cloudflare.SolidStart" "url": string From 2b0b57347495d450fa14e693b7a0ad7871e1eae5 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:05:02 +0530 Subject: [PATCH 5/7] feat(llm): add native provider packages --- packages/llm/package.json | 13 ++++-- packages/llm/src/index.ts | 5 +++ packages/llm/src/provider-package.ts | 16 +++++++ .../src/provider-packages/amazon-bedrock.ts | 28 ++++++++++++ .../provider-packages/anthropic-messages.ts | 15 +++++++ .../llm/src/provider-packages/openai-chat.ts | 20 +++++++++ .../openai-compatible-chat.ts | 17 +++++++ .../src/provider-packages/openai-responses.ts | 25 +++++++++++ packages/llm/src/provider-packages/shared.ts | 8 ++++ packages/llm/test/provider-package.test.ts | 45 +++++++++++++++++++ 10 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 packages/llm/src/provider-package.ts create mode 100644 packages/llm/src/provider-packages/amazon-bedrock.ts create mode 100644 packages/llm/src/provider-packages/anthropic-messages.ts create mode 100644 packages/llm/src/provider-packages/openai-chat.ts create mode 100644 packages/llm/src/provider-packages/openai-compatible-chat.ts create mode 100644 packages/llm/src/provider-packages/openai-responses.ts create mode 100644 packages/llm/src/provider-packages/shared.ts create mode 100644 packages/llm/test/provider-package.test.ts diff --git a/packages/llm/package.json b/packages/llm/package.json index faccd3dd47..4c9d62fdcd 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -15,14 +15,19 @@ "./route": "./src/route/index.ts", "./provider": "./src/provider.ts", "./providers": "./src/providers/index.ts", - "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts", - "./providers/anthropic": "./src/providers/anthropic.ts", + "./provider-package": "./src/provider-package.ts", + "./providers/amazon-bedrock": "./src/provider-packages/amazon-bedrock.ts", + "./providers/anthropic": "./src/provider-packages/anthropic-messages.ts", + "./providers/anthropic/messages": "./src/provider-packages/anthropic-messages.ts", "./providers/azure": "./src/providers/azure.ts", "./providers/cloudflare": "./src/providers/cloudflare.ts", "./providers/github-copilot": "./src/providers/github-copilot.ts", "./providers/google": "./src/providers/google.ts", - "./providers/openai": "./src/providers/openai.ts", - "./providers/openai-compatible": "./src/providers/openai-compatible.ts", + "./providers/openai": "./src/provider-packages/openai-responses.ts", + "./providers/openai/responses": "./src/provider-packages/openai-responses.ts", + "./providers/openai/chat": "./src/provider-packages/openai-chat.ts", + "./providers/openai-compatible": "./src/provider-packages/openai-compatible-chat.ts", + "./providers/openai-compatible/chat": "./src/provider-packages/openai-compatible-chat.ts", "./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts", "./providers/openrouter": "./src/providers/openrouter.ts", "./providers/xai": "./src/providers/xai.ts", diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 735520ff77..843b4536a6 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.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" diff --git a/packages/llm/src/provider-package.ts b/packages/llm/src/provider-package.ts new file mode 100644 index 0000000000..977a211864 --- /dev/null +++ b/packages/llm/src/provider-package.ts @@ -0,0 +1,16 @@ +import type { Model } from "./schema" + +export interface Settings extends Readonly> { + readonly headers?: Readonly> + readonly body?: Readonly> + readonly limits?: { + readonly context: number + readonly output: number + } +} + +export interface Definition { + readonly model: (id: string, settings: ProviderSettings) => Model +} + +export * as ProviderPackage from "./provider-package" diff --git a/packages/llm/src/provider-packages/amazon-bedrock.ts b/packages/llm/src/provider-packages/amazon-bedrock.ts new file mode 100644 index 0000000000..f00b2056ae --- /dev/null +++ b/packages/llm/src/provider-packages/amazon-bedrock.ts @@ -0,0 +1,28 @@ +import type { ProviderPackage } from "../provider-package" +import * as AmazonBedrock from "../providers/amazon-bedrock" +import type { BedrockCredentials } from "../protocols/bedrock-converse" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly auth?: "bearer" | "sigv4" + readonly baseURL?: string + readonly credentials?: BedrockCredentials + readonly region?: string + readonly topP?: number +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + if (settings.auth === "bearer" && settings.apiKey === undefined) + throw new Error("Amazon Bedrock bearer auth requires apiKey") + if (settings.auth === "sigv4" && settings.apiKey !== undefined) + throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") + return AmazonBedrock.configure({ + ...defaults(settings), + apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, + baseURL: settings.baseURL, + credentials: settings.credentials, + generation: settings.topP === undefined ? undefined : { topP: settings.topP }, + region: settings.region, + }).model(id) +} diff --git a/packages/llm/src/provider-packages/anthropic-messages.ts b/packages/llm/src/provider-packages/anthropic-messages.ts new file mode 100644 index 0000000000..be6def4081 --- /dev/null +++ b/packages/llm/src/provider-packages/anthropic-messages.ts @@ -0,0 +1,15 @@ +import type { ProviderPackage } from "../provider-package" +import * as Anthropic from "../providers/anthropic" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + Anthropic.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + }).model(id) diff --git a/packages/llm/src/provider-packages/openai-chat.ts b/packages/llm/src/provider-packages/openai-chat.ts new file mode 100644 index 0000000000..6a6d04c704 --- /dev/null +++ b/packages/llm/src/provider-packages/openai-chat.ts @@ -0,0 +1,20 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAI from "../providers/openai" +import type { OpenAIProviderOptionsInput } from "../providers/openai-options" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly queryParams?: Readonly> + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + OpenAI.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + providerOptions: settings.providerOptions, + }).chat(id) diff --git a/packages/llm/src/provider-packages/openai-compatible-chat.ts b/packages/llm/src/provider-packages/openai-compatible-chat.ts new file mode 100644 index 0000000000..ba7d569b9c --- /dev/null +++ b/packages/llm/src/provider-packages/openai-compatible-chat.ts @@ -0,0 +1,17 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAICompatible from "../providers/openai-compatible" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL: string + readonly provider?: string +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => + OpenAICompatible.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + provider: settings.provider, + }).model(id) diff --git a/packages/llm/src/provider-packages/openai-responses.ts b/packages/llm/src/provider-packages/openai-responses.ts new file mode 100644 index 0000000000..4851a96a34 --- /dev/null +++ b/packages/llm/src/provider-packages/openai-responses.ts @@ -0,0 +1,25 @@ +import type { ProviderPackage } from "../provider-package" +import * as OpenAI from "../providers/openai" +import type { OpenAIProviderOptionsInput } from "../providers/openai-options" +import { defaults } from "./shared" + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly queryParams?: Readonly> + readonly transport?: "http" | "websocket" + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const model: ProviderPackage.Definition["model"] = (id, settings) => { + const provider = OpenAI.configure({ + ...defaults(settings), + apiKey: settings.apiKey, + baseURL: settings.baseURL, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + providerOptions: settings.providerOptions, + }) + if (settings.transport === undefined || settings.transport === "http") return provider.responses(id) + if (settings.transport === "websocket") return provider.responsesWebSocket(id) + throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) +} diff --git a/packages/llm/src/provider-packages/shared.ts b/packages/llm/src/provider-packages/shared.ts new file mode 100644 index 0000000000..e3c7ec2444 --- /dev/null +++ b/packages/llm/src/provider-packages/shared.ts @@ -0,0 +1,8 @@ +import type { RouteDefaultsInput } from "../route/client" +import type { ProviderPackage } from "../provider-package" + +export const defaults = (settings: ProviderPackage.Settings): RouteDefaultsInput => ({ + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + limits: settings.limits, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, +}) diff --git a/packages/llm/test/provider-package.test.ts b/packages/llm/test/provider-package.test.ts new file mode 100644 index 0000000000..a4f1627c67 --- /dev/null +++ b/packages/llm/test/provider-package.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { model } from "@opencode-ai/llm/providers/openai" + +describe("provider package entrypoints", () => { + test("semantic API aliases expose the same contract", async () => { + const modules = await Promise.all([ + import("@opencode-ai/llm/providers/openai"), + import("@opencode-ai/llm/providers/openai/responses"), + import("@opencode-ai/llm/providers/openai/chat"), + import("@opencode-ai/llm/providers/anthropic"), + import("@opencode-ai/llm/providers/anthropic/messages"), + import("@opencode-ai/llm/providers/openai-compatible"), + import("@opencode-ai/llm/providers/openai-compatible/chat"), + import("@opencode-ai/llm/providers/amazon-bedrock"), + ]) + + for (const module of modules) expect(module.model).toBeFunction() + expect(modules[0].model).toBe(modules[1].model) + expect(modules[3].model).toBe(modules[4].model) + expect(modules[5].model).toBe(modules[6].model) + }) + + test("maps package settings onto the executable model", () => { + const selected = model("gpt-5", { + apiKey: "fixture", + baseURL: "https://api.openai.test/v1", + headers: { "x-application": "opencode" }, + body: { service_tier: "priority" }, + limits: { context: 200_000, output: 64_000 }, + unrelatedInheritedSetting: true, + }) + + expect(selected.route.id).toBe("openai-responses") + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) + expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + }) + + test("selects transport without changing the semantic API", () => { + expect(model("gpt-5", { apiKey: "fixture" }).route.id).toBe("openai-responses") + expect(model("gpt-5", { apiKey: "fixture", transport: "websocket" }).route.id).toBe( + "openai-responses-websocket", + ) + }) +}) From 928510608d2d297aa4117debb7eb381a7bd7aa17 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:10:45 +0530 Subject: [PATCH 6/7] refactor(llm): colocate provider package logic --- packages/llm/package.json | 14 ++++----- .../src/provider-packages/amazon-bedrock.ts | 28 ------------------ .../provider-packages/anthropic-messages.ts | 15 ---------- .../llm/src/provider-packages/openai-chat.ts | 20 ------------- .../openai-compatible-chat.ts | 17 ----------- .../src/provider-packages/openai-responses.ts | 25 ---------------- packages/llm/src/provider-packages/shared.ts | 8 ----- packages/llm/src/providers/amazon-bedrock.ts | 27 ++++++++++++++++- packages/llm/src/providers/anthropic.ts | 15 +++++++++- .../llm/src/providers/openai-compatible.ts | 17 +++++++++++ packages/llm/src/providers/openai.ts | 29 ++++++++++++++++++- packages/llm/src/providers/openai/chat.ts | 2 ++ .../llm/src/providers/openai/responses.ts | 2 ++ packages/llm/test/exports.test.ts | 1 - packages/llm/test/provider-package.test.ts | 4 --- 15 files changed, 95 insertions(+), 129 deletions(-) delete mode 100644 packages/llm/src/provider-packages/amazon-bedrock.ts delete mode 100644 packages/llm/src/provider-packages/anthropic-messages.ts delete mode 100644 packages/llm/src/provider-packages/openai-chat.ts delete mode 100644 packages/llm/src/provider-packages/openai-compatible-chat.ts delete mode 100644 packages/llm/src/provider-packages/openai-responses.ts delete mode 100644 packages/llm/src/provider-packages/shared.ts create mode 100644 packages/llm/src/providers/openai/chat.ts create mode 100644 packages/llm/src/providers/openai/responses.ts diff --git a/packages/llm/package.json b/packages/llm/package.json index 4c9d62fdcd..2a5d97d5bf 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -16,18 +16,16 @@ "./provider": "./src/provider.ts", "./providers": "./src/providers/index.ts", "./provider-package": "./src/provider-package.ts", - "./providers/amazon-bedrock": "./src/provider-packages/amazon-bedrock.ts", - "./providers/anthropic": "./src/provider-packages/anthropic-messages.ts", - "./providers/anthropic/messages": "./src/provider-packages/anthropic-messages.ts", + "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts", + "./providers/anthropic": "./src/providers/anthropic.ts", "./providers/azure": "./src/providers/azure.ts", "./providers/cloudflare": "./src/providers/cloudflare.ts", "./providers/github-copilot": "./src/providers/github-copilot.ts", "./providers/google": "./src/providers/google.ts", - "./providers/openai": "./src/provider-packages/openai-responses.ts", - "./providers/openai/responses": "./src/provider-packages/openai-responses.ts", - "./providers/openai/chat": "./src/provider-packages/openai-chat.ts", - "./providers/openai-compatible": "./src/provider-packages/openai-compatible-chat.ts", - "./providers/openai-compatible/chat": "./src/provider-packages/openai-compatible-chat.ts", + "./providers/openai": "./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", "./providers/xai": "./src/providers/xai.ts", diff --git a/packages/llm/src/provider-packages/amazon-bedrock.ts b/packages/llm/src/provider-packages/amazon-bedrock.ts deleted file mode 100644 index f00b2056ae..0000000000 --- a/packages/llm/src/provider-packages/amazon-bedrock.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as AmazonBedrock from "../providers/amazon-bedrock" -import type { BedrockCredentials } from "../protocols/bedrock-converse" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly auth?: "bearer" | "sigv4" - readonly baseURL?: string - readonly credentials?: BedrockCredentials - readonly region?: string - readonly topP?: number -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => { - if (settings.auth === "bearer" && settings.apiKey === undefined) - throw new Error("Amazon Bedrock bearer auth requires apiKey") - if (settings.auth === "sigv4" && settings.apiKey !== undefined) - throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") - return AmazonBedrock.configure({ - ...defaults(settings), - apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, - baseURL: settings.baseURL, - credentials: settings.credentials, - generation: settings.topP === undefined ? undefined : { topP: settings.topP }, - region: settings.region, - }).model(id) -} diff --git a/packages/llm/src/provider-packages/anthropic-messages.ts b/packages/llm/src/provider-packages/anthropic-messages.ts deleted file mode 100644 index be6def4081..0000000000 --- a/packages/llm/src/provider-packages/anthropic-messages.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as Anthropic from "../providers/anthropic" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - Anthropic.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - }).model(id) diff --git a/packages/llm/src/provider-packages/openai-chat.ts b/packages/llm/src/provider-packages/openai-chat.ts deleted file mode 100644 index 6a6d04c704..0000000000 --- a/packages/llm/src/provider-packages/openai-chat.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAI from "../providers/openai" -import type { OpenAIProviderOptionsInput } from "../providers/openai-options" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string - readonly queryParams?: Readonly> - readonly providerOptions?: OpenAIProviderOptionsInput -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - OpenAI.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, - providerOptions: settings.providerOptions, - }).chat(id) diff --git a/packages/llm/src/provider-packages/openai-compatible-chat.ts b/packages/llm/src/provider-packages/openai-compatible-chat.ts deleted file mode 100644 index ba7d569b9c..0000000000 --- a/packages/llm/src/provider-packages/openai-compatible-chat.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAICompatible from "../providers/openai-compatible" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL: string - readonly provider?: string -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => - OpenAICompatible.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - provider: settings.provider, - }).model(id) diff --git a/packages/llm/src/provider-packages/openai-responses.ts b/packages/llm/src/provider-packages/openai-responses.ts deleted file mode 100644 index 4851a96a34..0000000000 --- a/packages/llm/src/provider-packages/openai-responses.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ProviderPackage } from "../provider-package" -import * as OpenAI from "../providers/openai" -import type { OpenAIProviderOptionsInput } from "../providers/openai-options" -import { defaults } from "./shared" - -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly baseURL?: string - readonly queryParams?: Readonly> - readonly transport?: "http" | "websocket" - readonly providerOptions?: OpenAIProviderOptionsInput -} - -export const model: ProviderPackage.Definition["model"] = (id, settings) => { - const provider = OpenAI.configure({ - ...defaults(settings), - apiKey: settings.apiKey, - baseURL: settings.baseURL, - queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, - providerOptions: settings.providerOptions, - }) - if (settings.transport === undefined || settings.transport === "http") return provider.responses(id) - if (settings.transport === "websocket") return provider.responsesWebSocket(id) - throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) -} diff --git a/packages/llm/src/provider-packages/shared.ts b/packages/llm/src/provider-packages/shared.ts deleted file mode 100644 index e3c7ec2444..0000000000 --- a/packages/llm/src/provider-packages/shared.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RouteDefaultsInput } from "../route/client" -import type { ProviderPackage } from "../provider-package" - -export const defaults = (settings: ProviderPackage.Settings): RouteDefaultsInput => ({ - headers: settings.headers === undefined ? undefined : { ...settings.headers }, - limits: settings.limits, - http: settings.body === undefined ? undefined : { body: { ...settings.body } }, -}) diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/llm/src/providers/amazon-bedrock.ts index 2f1791e0d6..e578d614c6 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/llm/src/providers/amazon-bedrock.ts @@ -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..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["model"] = (id, settings) => { + if (settings.auth === "bearer" && settings.apiKey === undefined) + throw new Error("Amazon Bedrock bearer auth requires apiKey") + if (settings.auth === "sigv4" && settings.apiKey !== undefined) + throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") + return 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(id) +} diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index 0c9640af5e..8ef0293eeb 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -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["model"] = (id, 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(id) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index a79f65f6df..36d9b3fd5a 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -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["model"] = (id, 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(id) + export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) export const deepinfra = define(profiles.deepinfra) diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index 098cad8493..b40a94514b 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -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> + 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["model"] = (id, settings) => { + const configured = configure(config(settings)) + if (settings.transport === undefined || settings.transport === "http") return configured.responses(id) + if (settings.transport === "websocket") return configured.responsesWebSocket(id) + throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) +} + +export const chatModel: ProviderPackage.Definition["model"] = (id, settings) => + configure(config(settings)).chat(id) export const responses = provider.responses export const responsesWebSocket = provider.responsesWebSocket export const chat = provider.chat diff --git a/packages/llm/src/providers/openai/chat.ts b/packages/llm/src/providers/openai/chat.ts new file mode 100644 index 0000000000..eb92db8ac5 --- /dev/null +++ b/packages/llm/src/providers/openai/chat.ts @@ -0,0 +1,2 @@ +export { chatModel as model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/llm/src/providers/openai/responses.ts b/packages/llm/src/providers/openai/responses.ts new file mode 100644 index 0000000000..4db3a232c7 --- /dev/null +++ b/packages/llm/src/providers/openai/responses.ts @@ -0,0 +1,2 @@ +export { model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/llm/test/exports.test.ts b/packages/llm/test/exports.test.ts index 693a21638b..6ed66a0d8e 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/llm/test/exports.test.ts @@ -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() diff --git a/packages/llm/test/provider-package.test.ts b/packages/llm/test/provider-package.test.ts index a4f1627c67..c8cba78eb1 100644 --- a/packages/llm/test/provider-package.test.ts +++ b/packages/llm/test/provider-package.test.ts @@ -8,16 +8,12 @@ describe("provider package entrypoints", () => { import("@opencode-ai/llm/providers/openai/responses"), import("@opencode-ai/llm/providers/openai/chat"), import("@opencode-ai/llm/providers/anthropic"), - import("@opencode-ai/llm/providers/anthropic/messages"), import("@opencode-ai/llm/providers/openai-compatible"), - import("@opencode-ai/llm/providers/openai-compatible/chat"), import("@opencode-ai/llm/providers/amazon-bedrock"), ]) for (const module of modules) expect(module.model).toBeFunction() expect(modules[0].model).toBe(modules[1].model) - expect(modules[3].model).toBe(modules[4].model) - expect(modules[5].model).toBe(modules[6].model) }) test("maps package settings onto the executable model", () => { From d1446a9e72a3aaef900f480f783ed981c9511a50 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 24 Jun 2026 23:29:06 +0530 Subject: [PATCH 7/7] refactor(llm): name provider model identity --- packages/llm/src/provider-package.ts | 2 +- packages/llm/src/providers/amazon-bedrock.ts | 4 ++-- packages/llm/src/providers/anthropic.ts | 4 ++-- packages/llm/src/providers/openai-compatible.ts | 4 ++-- packages/llm/src/providers/openai.ts | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/llm/src/provider-package.ts b/packages/llm/src/provider-package.ts index 977a211864..fd878a9d06 100644 --- a/packages/llm/src/provider-package.ts +++ b/packages/llm/src/provider-package.ts @@ -10,7 +10,7 @@ export interface Settings extends Readonly> { } export interface Definition { - readonly model: (id: string, settings: ProviderSettings) => Model + readonly model: (modelID: string, settings: ProviderSettings) => Model } export * as ProviderPackage from "./provider-package" diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/llm/src/providers/amazon-bedrock.ts index e578d614c6..4e5040c9ec 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/llm/src/providers/amazon-bedrock.ts @@ -50,7 +50,7 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model: ProviderPackage.Definition["model"] = (id, settings) => { +export const model: ProviderPackage.Definition["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) @@ -64,5 +64,5 @@ export const model: ProviderPackage.Definition["model"] = (id, setting http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, region: settings.region, - }).model(id) + }).model(modelID) } diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index 8ef0293eeb..1bf8bf569c 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -38,11 +38,11 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model: ProviderPackage.Definition["model"] = (id, settings) => +export const model: ProviderPackage.Definition["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(id) + }).model(modelID) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index 36d9b3fd5a..8b5d23eed7 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -63,7 +63,7 @@ export const provider = { configure, } -export const model: ProviderPackage.Definition["model"] = (id, settings) => +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => configure({ apiKey: settings.apiKey, baseURL: settings.baseURL, @@ -71,7 +71,7 @@ export const model: ProviderPackage.Definition["model"] = (id, setting http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, provider: settings.provider, - }).model(id) + }).model(modelID) export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index b40a94514b..6b49b9def2 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -76,15 +76,15 @@ const config = (settings: Settings): Config => ({ queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, }) -export const model: ProviderPackage.Definition["model"] = (id, settings) => { +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { const configured = configure(config(settings)) - if (settings.transport === undefined || settings.transport === "http") return configured.responses(id) - if (settings.transport === "websocket") return configured.responsesWebSocket(id) + 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["model"] = (id, settings) => - configure(config(settings)).chat(id) +export const chatModel: ProviderPackage.Definition["model"] = (modelID, settings) => + configure(config(settings)).chat(modelID) export const responses = provider.responses export const responsesWebSocket = provider.responsesWebSocket export const chat = provider.chat