chore: sync native provider stack
This commit is contained in:
commit
315ba3720e
29 changed files with 249 additions and 787 deletions
|
|
@ -234,16 +234,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" })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -597,8 +597,8 @@ describe("Config", () => {
|
|||
headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" },
|
||||
models: {
|
||||
model: {
|
||||
body: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
|
||||
variants: [{ id: "high", body: { reasoningEffort: "high", reasoningSummary: "auto" } }],
|
||||
body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" },
|
||||
variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ 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
|
||||
|
|
@ -172,8 +172,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
expect(model.variants).toMatchObject([
|
||||
{
|
||||
id: "high",
|
||||
body: {},
|
||||
options: {
|
||||
body: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
|
|
@ -183,7 +182,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
|
||||
|
|
@ -225,8 +224,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" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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 } })),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
79
packages/core/test/plugin/variant.test.ts
Normal file
79
packages/core/test/plugin/variant.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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" } }),
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -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,17 +129,15 @@ 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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -158,8 +151,6 @@ describe("SessionRunnerModel", () => {
|
|||
settings: { baseURL: "https://regional.example/v1" },
|
||||
headers: {},
|
||||
body: {},
|
||||
generation: {},
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
|
@ -173,16 +164,13 @@ describe("SessionRunnerModel", () => {
|
|||
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
|
||||
location: { directory: AbsolutePath.make("/project") },
|
||||
})
|
||||
const resolved = yield* SessionRunnerModel.resolve(
|
||||
session,
|
||||
base,
|
||||
)
|
||||
const resolved = yield* SessionRunnerModel.resolve(session, base)
|
||||
|
||||
expect(resolved.route.endpoint.baseURL).toBe("https://regional.example/v1")
|
||||
}),
|
||||
)
|
||||
|
||||
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" },
|
||||
|
|
@ -190,9 +178,7 @@ describe("SessionRunnerModel", () => {
|
|||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
headers: {},
|
||||
body: {},
|
||||
generation: {},
|
||||
options: { reasoningEffort: "high" },
|
||||
body: { store: false, reasoning_effort: "high" },
|
||||
},
|
||||
],
|
||||
)
|
||||
|
|
@ -208,14 +194,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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -249,15 +233,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({
|
||||
|
|
@ -272,13 +254,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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -300,7 +280,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" }),
|
||||
)
|
||||
|
|
@ -323,7 +303,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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
@ -113,8 +110,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],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue