refactor(core): simplify model requests

This commit is contained in:
Dax Raad 2026-06-24 10:51:41 -04:00
commit 17f312d537
21 changed files with 56 additions and 685 deletions

View file

@ -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" })
}),
)

View file

@ -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" } }],
},
},
})

View file

@ -55,7 +55,7 @@ function request(headers: Record<string, string>, 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" },
})
}),
)

View file

@ -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: {},
})
})
})

View file

@ -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 } })),

View file

@ -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(

View file

@ -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,
)

View file

@ -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],