refactor(core): simplify model requests

This commit is contained in:
Dax Raad
2026-06-24 10:51:41 -04:00
parent 116ac93ddb
commit 17f312d537
21 changed files with 56 additions and 685 deletions
-9
View File
@@ -57,13 +57,6 @@ The bounded projection of a Core-executed tool result persisted in Session histo
**Managed Tool Output File**: **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. 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**: **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. 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. - 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. - 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. - 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`. - 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 **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. - A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
+2 -2
View File
@@ -2,7 +2,6 @@ export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { ModelRequest } from "./model-request"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Policy } from "./policy" import { Policy } from "./policy"
@@ -86,7 +85,8 @@ export const layer = Layer.effect(
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api : model.api
const request = { 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, variant: model.request.variant,
} }
return ModelV2.Info.make({ return ModelV2.Info.make({
+4 -15
View File
@@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const Plugin = define({ export const Plugin = define({
@@ -59,15 +58,11 @@ export const Plugin = define({
Object.assign(provider.request.body, item.request.body) 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 ?? {})) { for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => { catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api } if (config.api !== undefined) model.api = { ...model.api, ...config.api }
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
if (config.capabilities !== undefined) { if (config.capabilities !== undefined) {
model.capabilities = { model.capabilities = {
tools: config.capabilities.tools, tools: config.capabilities.tools,
@@ -76,10 +71,8 @@ export const Plugin = define({
} }
} }
if (config.request !== undefined) { if (config.request !== undefined) {
ModelRequest.assign(model.request, { Object.assign(model.request.headers, config.request.headers)
headers: config.request.headers, Object.assign(model.request.body, config.request.body)
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
})
if (config.request.variant !== undefined) model.request.variant = config.request.variant if (config.request.variant !== undefined) model.request.variant = config.request.variant
} }
if (config.variants !== undefined) { if (config.variants !== undefined) {
@@ -90,15 +83,11 @@ export const Plugin = define({
id: variant.id, id: variant.id,
headers: {}, headers: {},
body: {}, body: {},
generation: {},
options: {},
} }
model.variants.push(existing) model.variants.push(existing)
} }
ModelRequest.assign(existing, { Object.assign(existing.headers, variant.headers)
headers: variant.headers, Object.assign(existing.body, variant.body)
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
})
} }
} }
if (config.cost !== undefined) { if (config.cost !== undefined) {
-102
View File
@@ -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<string, string>
body: Record<string, unknown>
generation?: Record<string, unknown>
options?: Record<string, unknown>
}
const generationKeys = new Map<string, keyof Generation>([
["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<string, string>
}
const profiles = new Map<string, Profile>([
[
"@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<Request>) => ({
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<Request>) => {
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<Record<string, unknown>>) {
const generation: Record<string, number | ReadonlyArray<string>> = {}
const options: Record<string, unknown> = {}
const body: Record<string, unknown> = {}
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 }
}
+5 -9
View File
@@ -2,7 +2,6 @@ import { define } from "./internal"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider" import { ProviderV2 } from "../provider"
@@ -38,15 +37,12 @@ function cost(input: ModelsDev.Model["cost"]) {
] ]
} }
function variants(model: ModelsDev.Model, packageName?: string) { function variants(model: ModelsDev.Model) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => { return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
return {
id: ModelV2.VariantID.make(id), id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) }, headers: { ...(item.provider?.headers ?? {}) },
...request, body: { ...(item.provider?.body ?? {}) },
} }))
})
} }
export const ModelsDevPlugin = define({ export const ModelsDevPlugin = define({
@@ -115,7 +111,7 @@ export const ModelsDevPlugin = define({
input: [...(model.modalities?.input ?? [])], input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])], 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.time.released = released(model.release_date)
draft.cost = cost(model.cost) draft.cost = cost(model.cost)
draft.status = model.status ?? "active" draft.status = model.status ?? "active"
@@ -8,9 +8,9 @@ import { EventV2 } from "../../event"
import { Credential } from "../../credential" import { Credential } from "../../credential"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider" import { ConfigProviderV1 } from "../../v1/config/provider"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
import { ConfigV1 } from "../../v1/config/config" import { ConfigV1 } from "../../v1/config/config"
const defaultServer = "https://console.opencode.ai" const defaultServer = "https://console.opencode.ai"
@@ -142,15 +142,14 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input] if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output] if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm const packageName = config.provider?.npm ?? item.npm
ModelRequest.assign(model.request, { const lowerer = ConfigProviderOptionsV1.get(packageName)
headers: config.headers, Object.assign(model.request.headers, config.headers)
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(config.options)), Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
})
if (config.variants !== undefined) { if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({ model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id), id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) }, headers: { ...(options.headers ?? {}) },
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(options)), body: lowerer.request(withoutCredentials(options)),
})) }))
} }
if (config.release_date !== undefined) { if (config.release_date !== undefined) {
+2 -6
View File
@@ -11,7 +11,6 @@ import { Catalog } from "../../catalog"
import { Credential } from "../../credential" import { Credential } from "../../credential"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema" import { SessionSchema } from "../schema"
@@ -88,8 +87,6 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
} }
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { 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 body = model.request.body
const httpBody = Object.hasOwn(body, "apiKey") const httpBody = Object.hasOwn(body, "apiKey")
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey")) ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
@@ -98,8 +95,6 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
provider: model.providerID, provider: model.providerID,
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url }, endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
headers: model.request.headers, headers: model.request.headers,
generation: model.request.generation,
providerOptions: namespace && Object.keys(options).length > 0 ? { [namespace]: options } : undefined,
http: { body: httpBody }, http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output }, limits: { context: model.limit.context, output: model.limit.output },
}) })
@@ -122,7 +117,8 @@ const withVariant = (
return Effect.succeed( return Effect.succeed(
variant variant
? produce(model, (draft) => { ? produce(model, (draft) => {
ModelRequest.assign(draft.request, variant) Object.assign(draft.request.headers, variant.headers)
Object.assign(draft.request.body, variant.body)
}) })
: model, : model,
) )
+2 -7
View File
@@ -6,7 +6,6 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission" import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider" import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options" import { ConfigProviderOptionsV1 } from "./provider-options"
import { ModelRequest } from "../../model-request"
const keys = new Set([ const keys = new Set([
"logLevel", "logLevel",
@@ -192,11 +191,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) { function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
const packageID = info.provider?.npm ?? packageName const packageID = info.provider?.npm ?? packageName
const lowerer = ConfigProviderOptionsV1.get(packageID) const lowerer = ConfigProviderOptionsV1.get(packageID)
const ingest = (options: Readonly<Record<string, unknown>>) => { const request = info.options && lowerer.request(info.options)
const request = ModelRequest.normalizeAiSdkOptions(packageID, options)
return { ...lowerer.request(request.body), ...request.generation, ...request.options }
}
const request = info.options && ingest(info.options)
const costs = info.cost && [ const costs = info.cost && [
{ {
input: info.cost.input, input: info.cost.input,
@@ -241,7 +236,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
info.variants && info.variants &&
Object.entries(info.variants).map(([id, options]) => ({ Object.entries(info.variants).map(([id, options]) => ({
id, id,
body: ingest(options), body: lowerer.request(options),
})), })),
cost: costs, cost: costs,
disabled: info.status === "deprecated" ? true : undefined, disabled: info.status === "deprecated" ? true : undefined,
+2 -5
View File
@@ -233,16 +233,13 @@ describe("CatalogV2", () => {
model.request.headers.shared = "model" model.request.headers.shared = "model"
model.request.body.model = true model.request.body.model = true
model.request.body.request = true model.request.body.request = true
const options = (model.request.options ??= {}) model.request.body.shared = "model"
options.shared = "model"
options.model = true
}) })
}) })
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) 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.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
expect(model.request.options).toEqual({ shared: "model", model: true })
}), }),
) )
+2 -2
View File
@@ -599,9 +599,9 @@ describe("Config", () => {
models: { models: {
model: { model: {
request: { 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" } }],
}, },
}, },
}) })
+4 -6
View File
@@ -55,7 +55,7 @@ function request(headers: Record<string, string>, variant?: string) {
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigProviderPlugin.Plugin", () => { 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* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode const providerID = ProviderV2.ID.opencode
@@ -96,8 +96,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
expect(model.variants).toMatchObject([ expect(model.variants).toMatchObject([
{ {
id: "high", id: "high",
body: {}, body: {
options: {
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], 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* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode const providerID = ProviderV2.ID.opencode
@@ -147,8 +146,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({ expect(model.variants[0]).toMatchObject({
id: "high", id: "high",
body: {}, body: { reasoningEffort: "high" },
options: { reasoningEffort: "high" },
}) })
}), }),
) )
-44
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: {},
})
})
})
-10
View File
@@ -290,21 +290,11 @@ function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
...value.request, ...value.request,
headers: { ...value.request.headers }, headers: { ...value.request.headers },
body: { ...value.request.body }, 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) => ({ variants: value.variants.map((variant) => ({
...variant, ...variant,
headers: { ...variant.headers }, headers: { ...variant.headers },
body: { ...variant.body }, body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
})), })),
time: { ...value.time }, time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })), 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 } }], cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 }, limit: { context: 1000, output: 100 },
}) })
expect(model.request).toMatchObject({ body: { custom: "value" }, generation: { temperature: 0.5 } }) expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
expect(model.request.body).toEqual({ custom: "value" })
expect(model.variants).toEqual([ expect(model.variants).toEqual([
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: {}, body: { temperature: 0.2 },
generation: { temperature: 0.2 },
options: {},
}, },
]) ])
expect( expect(
+21 -36
View File
@@ -31,8 +31,6 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
request: { request: {
headers: { "x-test": "header" }, headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } }, body: { apiKey: "secret", custom_extension: { enabled: true } },
generation: { temperature: 0.7 },
options: { store: false, serviceTier: "priority" },
}, },
variants, variants,
time: { released: 0 }, time: { released: 0 },
@@ -56,8 +54,6 @@ describe("SessionRunnerModel", () => {
defaults: { defaults: {
headers: { "x-test": "header" }, headers: { "x-test": "header" },
limits: { context: 100, output: 20 }, limits: { context: 100, output: 20 },
generation: { temperature: 0.7 },
providerOptions: { openai: { store: false, serviceTier: "priority" } },
http: { body: { custom_extension: { enabled: true } } }, http: { body: { custom_extension: { enabled: true } } },
}, },
}) })
@@ -86,7 +82,7 @@ describe("SessionRunnerModel", () => {
url: "https://compatible.example/v1", url: "https://compatible.example/v1",
settings: { apiKey: "settings-secret", compatibility: "strict" }, settings: { apiKey: "settings-secret", compatibility: "strict" },
}), }),
request: { headers: {}, body: {}, generation: {}, options: {} }, request: { headers: {}, body: {} },
}), }),
) )
const request = LLM.request({ model: resolved, prompt: "Hello" }) 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* () { 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"), id: ModelV2.VariantID.make("high"),
headers: { "x-variant": "high" }, headers: { "x-variant": "high" },
body: {}, body: {
generation: { temperature: 0.2 }, store: false,
options: { reasoningEffort: "high" }, 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({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant"), id: SessionV2.ID.make("ses_model_variant"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@@ -134,21 +129,19 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) 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.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } }) expect(resolved.route.defaults.http?.body).toEqual({
expect(prepared.body).toMatchObject({ custom_extension: { enabled: true },
store: false, store: false,
service_tier: "priority", service_tier: "priority",
temperature: 0.2, temperature: 0.2,
reasoning: { effort: "high" }, 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* () { Effect.gen(function* () {
const catalog = model( const catalog = model(
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" }, { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
@@ -156,9 +149,7 @@ describe("SessionRunnerModel", () => {
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: {}, body: { store: false, reasoning_effort: "high" },
generation: {},
options: { reasoningEffort: "high" },
}, },
], ],
) )
@@ -174,14 +165,12 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) 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(resolved.route.defaults.http?.body).toEqual({
expect(prepared.body).toMatchObject({ custom_extension: { enabled: true },
store: false, store: false,
reasoning_effort: "high", 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* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [ const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: {}, body: { thinking: { type: "enabled", budget_tokens: 12000 } },
generation: {},
options: { thinking: { type: "enabled", budgetTokens: 12000 } },
}, },
]) ])
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
@@ -238,13 +225,11 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) 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(resolved.route.defaults.http?.body).toEqual({
expect(prepared.body).toMatchObject({ custom_extension: { enabled: true },
thinking: { type: "enabled", budget_tokens: 12000 }, 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( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), ...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" }), Credential.Key.make({ type: "key", key: "secret" }),
) )
@@ -289,7 +274,7 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), ...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, credential,
) )
-5
View File
@@ -19,7 +19,6 @@ import { Credential } from "@opencode-ai/schema/credential"
import { FileSystem } from "@opencode-ai/schema/filesystem" import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { LLM } from "@opencode-ai/schema/llm" import { LLM } from "@opencode-ai/schema/llm"
import { ModelRequest } from "@opencode-ai/schema/model-request"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { Reference } from "@opencode-ai/schema/reference" import { Reference } from "@opencode-ai/schema/reference"
import { Skill } from "@opencode-ai/schema/skill" import { Skill } from "@opencode-ai/schema/skill"
@@ -35,7 +34,6 @@ test("Core reuses the canonical shared schemas", async () => {
coreIntegration, coreIntegration,
coreLocation, coreLocation,
coreLLM, coreLLM,
coreModelRequest,
corePermission, corePermission,
coreProject, coreProject,
coreReference, coreReference,
@@ -53,7 +51,6 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/integration"), import("@opencode-ai/core/integration"),
import("@opencode-ai/core/location"), import("@opencode-ai/core/location"),
import("@opencode-ai/llm"), import("@opencode-ai/llm"),
import("@opencode-ai/core/model-request"),
import("@opencode-ai/core/permission"), import("@opencode-ai/core/permission"),
import("@opencode-ai/core/project/schema"), import("@opencode-ai/core/project/schema"),
import("@opencode-ai/core/reference"), import("@opencode-ai/core/reference"),
@@ -111,8 +108,6 @@ test("Core reuses the canonical shared schemas", async () => {
[ProviderV2.Api, Provider.Api], [ProviderV2.Api, Provider.Api],
[ProviderV2.Request, Provider.Request], [ProviderV2.Request, Provider.Request],
[ProviderV2.Info, Provider.Info], [ProviderV2.Info, Provider.Info],
[coreModelRequest.Generation, ModelRequest.Generation],
[coreModelRequest.Request, ModelRequest.Request],
[corePermission.Effect, Permission.Effect], [corePermission.Effect, Permission.Effect],
[corePermission.Rule, Permission.Rule], [corePermission.Rule, Permission.Rule],
[corePermission.Ruleset, Permission.Ruleset], [corePermission.Ruleset, Permission.Ruleset],
-1
View File
@@ -7,7 +7,6 @@ export { Integration } from "./integration"
export { LLM } from "./llm" export { LLM } from "./llm"
export { Location } from "./location" export { Location } from "./location"
export { Model } from "./model" export { Model } from "./model"
export { ModelRequest } from "./model-request"
export { Permission } from "./permission" export { Permission } from "./permission"
export { Project } from "./project" export { Project } from "./project"
export { Provider } from "./provider" export { Provider } from "./provider"
-31
View File
@@ -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<typeof Generation> {}
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<typeof Request> {}
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({})),
),
})
+3 -4
View File
@@ -1,7 +1,6 @@
export * as Model from "./model" export * as Model from "./model"
import { Schema } from "effect" import { Schema } from "effect"
import { ModelRequest } from "./model-request"
import { Provider } from "./provider" import { Provider } from "./provider"
import { withStatics } from "./schema" import { withStatics } from "./schema"
@@ -63,12 +62,12 @@ export const Info = Schema.Struct({
api: Api, api: Api,
capabilities: Capabilities, capabilities: Capabilities,
request: Schema.Struct({ request: Schema.Struct({
...ModelRequest.Request.fields, ...Provider.Request.fields,
variant: Schema.String.pipe(Schema.optional), variant: Schema.String.pipe(Schema.optional),
}), }),
variants: Schema.Struct({ variants: Schema.Struct({
id: VariantID, id: VariantID,
...ModelRequest.Request.fields, ...Provider.Request.fields,
}).pipe(Schema.Array, Schema.mutable), }).pipe(Schema.Array, Schema.mutable),
time: Schema.Struct({ time: Schema.Struct({
released: Schema.Finite, released: Schema.Finite,
@@ -92,7 +91,7 @@ export const Info = Schema.Struct({
name: modelID, name: modelID,
api: { id: modelID, type: "native", settings: {} }, api: { id: modelID, type: "native", settings: {} },
capabilities: { tools: false, input: [], output: [] }, capabilities: { tools: false, input: [], output: [] },
request: { headers: {}, body: {}, generation: {}, options: {} }, request: { headers: {}, body: {} },
variants: [], variants: [],
time: { released: 0 }, time: { released: 0 },
cost: [], cost: [],
-26
View File
@@ -4019,19 +4019,6 @@ export type ModelV2Info = {
body: { body: {
[key: string]: unknown [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<string>
}
options?: {
[key: string]: unknown
}
variant?: string variant?: string
} }
variants: Array<{ variants: Array<{
@@ -4042,19 +4029,6 @@ export type ModelV2Info = {
body: { body: {
[key: string]: unknown [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<string>
}
options?: {
[key: string]: unknown
}
}> }>
time: { time: {
released: number released: number
-352
View File
@@ -26833,182 +26833,6 @@
"body": { "body": {
"type": "object" "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": { "variant": {
"type": "string" "type": "string"
} }
@@ -27032,182 +26856,6 @@
}, },
"body": { "body": {
"type": "object" "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"], "required": ["id", "headers", "body"],