fix(core): derive models.dev reasoning variants (#34726)

This commit is contained in:
Aiden Cline 2026-07-02 00:23:46 -05:00 committed by GitHub
commit 140224b0fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 457 additions and 119 deletions

View file

@ -151,6 +151,7 @@ export type AgentListOutput = {
readonly id: string readonly id: string
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly request: { readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body: { readonly [x: string]: JsonValue }
} }
@ -2192,12 +2193,14 @@ export type ModelListOutput = {
readonly output: ReadonlyArray<string> readonly output: ReadonlyArray<string>
} }
readonly request: { readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body: { readonly [x: string]: JsonValue }
readonly variant?: string readonly variant?: string
} }
readonly variants: ReadonlyArray<{ readonly variants: ReadonlyArray<{
readonly id: string readonly id: string
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body: { readonly [x: string]: JsonValue }
}> }>
@ -2256,6 +2259,7 @@ export type ProviderListOutput = {
} }
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
readonly request: { readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body: { readonly [x: string]: JsonValue }
} }
@ -2289,6 +2293,7 @@ export type ProviderGetOutput = {
} }
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
readonly request: { readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body: { readonly [x: string]: JsonValue }
} }

View file

@ -85,6 +85,7 @@ 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 = {
settings: { ...provider.request.settings, ...model.request.settings },
headers: { ...provider.request.headers, ...model.request.headers }, headers: { ...provider.request.headers, ...model.request.headers },
body: { ...provider.request.body, ...model.request.body }, body: { ...provider.request.body, ...model.request.body },
variant: model.request.variant, variant: model.request.variant,

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 { ProviderV2 } from "../../provider"
export const Plugin = define({ export const Plugin = define({
id: "config-provider", id: "config-provider",
@ -54,6 +53,7 @@ export const Plugin = define({
if (item.name !== undefined) provider.name = item.name if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api } if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) { if (item.request !== undefined) {
Object.assign(provider.request.settings, item.request.settings)
Object.assign(provider.request.headers, item.request.headers) Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body) Object.assign(provider.request.body, item.request.body)
} }
@ -71,6 +71,7 @@ export const Plugin = define({
} }
} }
if (config.request !== undefined) { if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers) Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body) Object.assign(model.request.body, 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
@ -81,11 +82,13 @@ export const Plugin = define({
if (!existing) { if (!existing) {
existing = { existing = {
id: variant.id, id: variant.id,
settings: {},
headers: {}, headers: {},
body: {}, body: {},
} }
model.variants.push(existing) model.variants.push(existing)
} }
Object.assign(existing.settings, variant.settings)
Object.assign(existing.headers, variant.headers) Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body) Object.assign(existing.body, variant.body)
} }

View file

@ -5,6 +5,7 @@ import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
settings: ProviderV2.Settings.pipe(Schema.optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}) {} }) {}

View file

@ -26,8 +26,13 @@ export type Api = Model.Api
export const Info = Model.Info export const Info = Model.Info
export type Info = Model.Info export type Info = Model.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { export type MutableRequest = ProviderV2.MutableRequest & { variant?: string }
export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
api: ProviderV2.MutableApi<Api> api: ProviderV2.MutableApi<Api>
request: MutableRequest
variants: MutableVariant[]
} }
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {

View file

@ -70,25 +70,73 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()] return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
} }
function reasoningVariants(model: ModelsDev.Model, packageName: string | undefined): ModelV2Info["variants"] { const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
const result = new Map<ModelV2.VariantID, ModelV2Info["variants"][number]>()
if (packageName === "@ai-sdk/openai" || packageName === "@ai-sdk/openai-compatible") { function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] {
const option = model.reasoning_options?.find((option) => option.type === "effort") const npm = model.provider?.npm ?? provider.npm
for (const value of option?.values ?? []) { const options = model.reasoning_options ?? []
const id = value === null ? "none" : value const effort = options.find((option) => option.type === "effort")
if (typeof id !== "string") continue if (effort?.type === "effort") {
const variantID = ModelV2.VariantID.make(id) return effort.values.flatMap((value) => {
result.set(variantID, { const raw: unknown = value
id: variantID, const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
headers: {}, if (id === undefined) return []
body: const settings = settingsForEffort(npm, id)
packageName === "@ai-sdk/openai" return settings ? [{ id, settings, headers: {}, body: {} }] : []
? { include: ["reasoning.encrypted_content"], reasoning: { effort: id, summary: "auto" } } })
: { reasoning_effort: id }, }
})
const budget = options.find((option) => option.type === "budget_tokens")
if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
// Toggle-only reasoning is intentionally left for a follow-up because V1 has
// provider/model-specific behavior like MiniMax M3 adaptive thinking and
// Qwen/GLM enable_thinking request shapes in packages/opencode.
return []
}
function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
return { thinking: { type: "adaptive", display: "summarized" }, effort }
}
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
}
if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
if (npm === "@ai-sdk/openai") {
return {
reasoningEffort: effort,
reasoningSummary: "auto",
include: OPENAI_INCLUDE_ENCRYPTED_REASONING,
} }
} }
return [...result.values()] if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
}
function budgetVariants(
npm: string | undefined,
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
): ModelV2Info["variants"] {
const max = option.max
const high = option.max === undefined ? Math.max(option.min ?? 0, 16_000) : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
return [
{ id: "high", budget: high },
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
].flatMap((item) => {
const settings = settingsForBudget(npm, item.budget)
return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : []
})
}
function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
return { thinking: { type: "enabled", budgetTokens: budget } }
}
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
}
} }
function modeName(model: ModelsDev.Model, mode: string) { function modeName(model: ModelsDev.Model, mode: string) {
@ -193,7 +241,7 @@ export const ModelsDevPlugin = define({
for (const model of Object.values(item.models)) { for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost) const baseCost = cost(model.cost)
const variants = reasoningVariants(model, model.provider?.npm ?? item.npm) const variants = reasoningVariants(item, model)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants })) catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) => catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>

View file

@ -146,7 +146,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
const variantID = ModelV2.VariantID.make(id) const variantID = ModelV2.VariantID.make(id)
let existing = model.variants.find((item) => item.id === variantID) let existing = model.variants.find((item) => item.id === variantID)
if (!existing) { if (!existing) {
existing = { id: variantID, headers: {}, body: {} } existing = { id: variantID, settings: {}, headers: {}, body: {} }
model.variants.push(existing) model.variants.push(existing)
} }
Object.assign(existing.headers, options.headers) Object.assign(existing.headers, options.headers)

View file

@ -33,7 +33,8 @@ export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({ return ["high", "max"].map((id) => ({
id, id,
settings: { reasoningEffort: id },
headers: {}, headers: {},
body: { reasoning_effort: id }, body: {},
})) }))
} }

View file

@ -19,7 +19,15 @@ export type MutableApi<T extends Api = Api> = T extends Api
export const Request = Provider.Request export const Request = Provider.Request
export type Request = Provider.Request export type Request = Provider.Request
export const Settings = Provider.Settings
export type Settings = Provider.Settings
export const Info = Provider.Info export const Info = Provider.Info
export type Info = Provider.Info export type Info = Provider.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi } export type MutableRequest = Types.DeepMutable<Request>
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & {
api: MutableApi
request: MutableRequest
}

View file

@ -97,11 +97,20 @@ 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,
providerOptions: providerOptions(model),
http: { body: httpBody }, http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output }, limits: { context: model.limit.context, output: model.limit.output },
}) })
} }
const providerOptions = (model: ModelV2.Info) => {
if (Object.keys(model.request.settings).length === 0) return undefined
if (model.api.type !== "aisdk") return undefined
if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings }
if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings }
if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings }
}
export const withVariant = ( export const withVariant = (
model: ModelV2.Info, model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined, variantID: ModelV2.VariantID | undefined,
@ -119,6 +128,7 @@ export const withVariant = (
return Effect.succeed( return Effect.succeed(
variant variant
? produce(model, (draft) => { ? produce(model, (draft) => {
Object.assign(draft.request.settings, variant.settings)
Object.assign(draft.request.headers, variant.headers) Object.assign(draft.request.headers, variant.headers)
Object.assign(draft.request.body, variant.body) Object.assign(draft.request.body, variant.body)
}) })

View file

@ -0,0 +1,67 @@
{
"openai": {
"id": "openai",
"name": "OpenAI",
"env": ["OPENAI_API_KEY"],
"npm": "@ai-sdk/openai",
"api": "https://api.openai.com/v1",
"models": {
"gpt-reasoning": {
"id": "gpt-reasoning",
"name": "GPT Reasoning",
"release_date": "2026-01-01",
"attachment": false,
"reasoning": true,
"reasoning_options": [
{ "type": "effort", "values": ["low", "high"] },
{ "type": "budget_tokens", "min": 1024, "max": 64000 },
{ "type": "toggle" }
],
"temperature": true,
"tool_call": true,
"limit": { "context": 128000, "output": 8192 },
"experimental": {
"modes": {
"high": {
"provider": {
"headers": { "x-mode": "high" },
"body": { "service_tier": "priority" }
}
}
}
}
}
}
},
"anthropic": {
"id": "anthropic",
"name": "Anthropic",
"env": ["ANTHROPIC_API_KEY"],
"npm": "@ai-sdk/anthropic",
"api": "https://api.anthropic.com/v1",
"models": {
"claude-budget": {
"id": "claude-budget",
"name": "Claude Budget",
"release_date": "2026-01-01",
"attachment": false,
"reasoning": true,
"reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 64000 }],
"temperature": true,
"tool_call": true,
"limit": { "context": 128000, "output": 8192 }
},
"claude-effort": {
"id": "claude-effort",
"name": "Claude Effort",
"release_date": "2026-01-01",
"attachment": false,
"reasoning": true,
"reasoning_options": [{ "type": "effort", "values": ["low"] }],
"temperature": true,
"tool_call": true,
"limit": { "context": 128000, "output": 8192 }
}
}
}
}

View file

@ -288,7 +288,7 @@ function providerInfo(value: ProviderV2.MutableInfo) {
return { return {
...value, ...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } }, api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } }, request: { settings: { ...value.request.settings }, headers: { ...value.request.headers }, body: { ...value.request.body } },
} }
} }
@ -303,11 +303,13 @@ function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
}, },
request: { request: {
...value.request, ...value.request,
settings: { ...value.request.settings },
headers: { ...value.request.headers }, headers: { ...value.request.headers },
body: { ...value.request.body }, body: { ...value.request.body },
}, },
variants: value.variants.map((variant) => ({ variants: value.variants.map((variant) => ({
...variant, ...variant,
settings: { ...variant.settings },
headers: { ...variant.headers }, headers: { ...variant.headers },
body: { ...variant.body }, body: { ...variant.body },
})), })),

View file

@ -168,14 +168,14 @@ describe("ModelsDevPlugin", () => {
), ),
) )
it.effect("derives OpenAI reasoning variants from models.dev reasoning options", () => it.effect("converts reasoning options into settings variants", () =>
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
const previous = { const previous = {
path: Flag.OPENCODE_MODELS_PATH, path: Flag.OPENCODE_MODELS_PATH,
disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH, disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH,
} }
Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev.json") Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev-reasoning.json")
Flag.OPENCODE_DISABLE_MODELS_FETCH = true Flag.OPENCODE_DISABLE_MODELS_FETCH = true
return previous return previous
}), }),
@ -183,17 +183,6 @@ describe("ModelsDevPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
yield* catalog.transform((catalog) => {
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("gpt-5.5"), (model) => {
model.variants = [
{
id: ModelV2.VariantID.make("high"),
headers: { custom: "true" },
body: { custom: true },
},
]
})
})
yield* ModelsDevPlugin.effect( yield* ModelsDevPlugin.effect(
host({ host({
catalog: catalogHost(catalog), catalog: catalogHost(catalog),
@ -201,42 +190,67 @@ describe("ModelsDevPlugin", () => {
}), }),
) )
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("gpt-5.5")))?.variants).toEqual([ const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
{ expect(model?.variants.map((variant) => variant.id)).toEqual([
id: ModelV2.VariantID.make("none"), ModelV2.VariantID.make("low"),
headers: {}, ModelV2.VariantID.make("high"),
body: {
include: ["reasoning.encrypted_content"],
reasoning: { effort: "none", summary: "auto" },
},
},
expect.objectContaining({
id: "low",
body: {
include: ["reasoning.encrypted_content"],
reasoning: { effort: "low", summary: "auto" },
},
}),
expect.objectContaining({
id: "medium",
body: {
include: ["reasoning.encrypted_content"],
reasoning: { effort: "medium", summary: "auto" },
},
}),
expect.objectContaining({
id: "high",
headers: { custom: "true" },
body: { custom: true },
}),
expect.objectContaining({
id: "xhigh",
body: {
include: ["reasoning.encrypted_content"],
reasoning: { effort: "xhigh", summary: "auto" },
},
}),
]) ])
expect(model?.variants).toContainEqual({
id: ModelV2.VariantID.make("low"),
settings: {
reasoningEffort: "low",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
},
headers: {},
body: {},
})
expect(model?.variants).toContainEqual({
id: ModelV2.VariantID.make("high"),
settings: {
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
},
headers: {},
body: {},
})
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
expect(mode).toMatchObject({
id: "gpt-reasoning-high",
name: "GPT Reasoning High",
request: {
headers: { "x-mode": "high" },
body: { service_tier: "priority" },
},
})
expect(mode?.variants.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("low"),
ModelV2.VariantID.make("high"),
])
const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
expect(budgetModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
headers: {},
body: {},
})
expect(budgetModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
headers: {},
body: {},
})
const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-effort"))
expect(anthropicEffortModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("low"),
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
headers: {},
body: {},
})
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))), }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
(previous) => (previous) =>
Effect.sync(() => { Effect.sync(() => {
@ -245,5 +259,4 @@ describe("ModelsDevPlugin", () => {
}), }),
), ),
) )
}) })

View file

@ -142,6 +142,7 @@ describe("OpencodePlugin", () => {
model.variants = [ model.variants = [
{ {
id: ModelV2.VariantID.make("custom"), id: ModelV2.VariantID.make("custom"),
settings: {},
headers: { "x-custom": "true" }, headers: { "x-custom": "true" },
body: { custom: true }, body: { custom: true },
}, },
@ -177,7 +178,7 @@ describe("OpencodePlugin", () => {
url: `${server.url.origin}/v1`, url: `${server.url.origin}/v1`,
}, },
}) })
expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { custom: "value" } }) expect(provider.request).toEqual({ settings: {}, headers: { "x-org-id": "org" }, body: { custom: "value" } })
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined() expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"))) const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
@ -192,11 +193,13 @@ describe("OpencodePlugin", () => {
expect(model.variants).toEqual([ expect(model.variants).toEqual([
{ {
id: ModelV2.VariantID.make("custom"), id: ModelV2.VariantID.make("custom"),
settings: {},
headers: { "x-custom": "true" }, headers: { "x-custom": "true" },
body: { custom: true }, body: { custom: true },
}, },
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
settings: {},
headers: {}, headers: {},
body: { temperature: 0.2 }, body: { temperature: 0.2 },
}, },
@ -359,6 +362,7 @@ describe("OpencodePlugin", () => {
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, api: { type: "aisdk", package: "test-provider" },
request: { request: {
settings: {},
headers: {}, headers: {},
body: { apiKey: "configured" }, body: { apiKey: "configured" },
}, },

View file

@ -37,8 +37,8 @@ describe("VariantPlugin", () => {
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) 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((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: "high", settings: { reasoningEffort: "high" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
]) ])
}), }),
) )
@ -53,14 +53,14 @@ describe("VariantPlugin", () => {
type: "aisdk", type: "aisdk",
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
} }
model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }] model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
}) })
}) })
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) 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((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: "high", headers: { custom: "true" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
]) ])
}), }),
) )

View file

@ -30,6 +30,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
api: { id: ModelV2.ID.make("api-test-model"), ...api }, api: { id: ModelV2.ID.make("api-test-model"), ...api },
capabilities: { tools: true, input: ["text"], output: ["text"] }, capabilities: { tools: true, input: ["text"], output: ["text"] },
request: { request: {
settings: {},
headers: { "x-test": "header" }, headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } }, body: { apiKey: "secret", custom_extension: { enabled: true } },
}, },
@ -83,7 +84,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: {} }, request: { settings: {}, headers: {}, body: {} },
}), }),
) )
const request = LLM.request({ model: resolved, prompt: "Hello" }) const request = LLM.request({ model: resolved, prompt: "Hello" })
@ -100,17 +101,17 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("overlays selected OpenAI Session variant bodies", () => it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = 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"),
settings: { reasoningEffort: "high" },
headers: { "x-variant": "high" }, headers: { "x-variant": "high" },
body: { body: {
store: false, store: false,
service_tier: "priority", service_tier: "priority",
temperature: 0.2, temperature: 0.2,
reasoning: { effort: "high" },
}, },
}, },
]) ])
@ -137,7 +138,9 @@ describe("SessionRunnerModel", () => {
store: false, store: false,
service_tier: "priority", service_tier: "priority",
temperature: 0.2, temperature: 0.2,
reasoning: { effort: "high" }, })
expect(resolved.route.defaults.providerOptions).toEqual({
openai: { store: false, reasoningEffort: "high" },
}) })
}), }),
) )
@ -149,6 +152,7 @@ describe("SessionRunnerModel", () => {
[ [
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
settings: {},
headers: {}, headers: {},
body: { store: false, reasoning_effort: "high" }, body: { store: false, reasoning_effort: "high" },
}, },
@ -205,13 +209,14 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("overlays selected Anthropic Session variant bodies", () => it.effect("overlays selected Anthropic Session variant settings", () =>
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"),
settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
headers: {}, headers: {},
body: { thinking: { type: "enabled", budget_tokens: 12000 } }, body: {},
}, },
]) ])
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
@ -229,7 +234,9 @@ describe("SessionRunnerModel", () => {
expect(resolved.route.defaults.http?.body).toEqual({ expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true }, custom_extension: { enabled: true },
thinking: { type: "enabled", budget_tokens: 12000 }, })
expect(resolved.route.defaults.providerOptions).toEqual({
anthropic: { thinking: { type: "enabled", budgetTokens: 12000 } },
}) })
}), }),
) )
@ -252,7 +259,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: {} }, request: { settings: {}, headers: {}, body: {} },
}), }),
Credential.Key.make({ type: "key", key: "secret" }), Credential.Key.make({ type: "key", key: "secret" }),
) )
@ -275,7 +282,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" } }, request: { settings: {}, headers: {}, body: { apiKey: "configured-secret" } },
}), }),
credential, credential,
) )
@ -297,7 +304,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: {} }, request: { settings: {}, headers: {}, body: {} },
}), }),
Credential.OAuth.make({ Credential.OAuth.make({
type: "oauth", type: "oauth",

View file

@ -148,9 +148,22 @@ const AnthropicToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
]) ])
const AnthropicThinking = Schema.Struct({ const AnthropicThinking = Schema.Union([
type: Schema.tag("enabled"), Schema.Struct({
budget_tokens: Schema.Number, type: Schema.tag("enabled"),
budget_tokens: Schema.Number,
}),
Schema.Struct({
type: Schema.tag("adaptive"),
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
}),
Schema.Struct({
type: Schema.tag("disabled"),
}),
])
const AnthropicOutputConfig = Schema.Struct({
effort: Schema.optional(Schema.String),
}) })
const AnthropicBodyFields = { const AnthropicBodyFields = {
@ -166,6 +179,7 @@ const AnthropicBodyFields = {
top_k: Schema.optional(Schema.Number), top_k: Schema.optional(Schema.Number),
stop_sequences: optionalArray(Schema.String), stop_sequences: optionalArray(Schema.String),
thinking: Schema.optional(AnthropicThinking), thinking: Schema.optional(AnthropicThinking),
output_config: Schema.optional(AnthropicOutputConfig),
} }
const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody> export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
@ -492,7 +506,16 @@ const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthr
const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) { const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
const thinking = anthropicOptions(request)?.thinking const thinking = anthropicOptions(request)?.thinking
if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined if (!ProviderShared.isRecord(thinking)) return undefined
if (thinking.type === "adaptive") {
const display = thinking.display
return {
type: "adaptive" as const,
...(display === "summarized" || display === "omitted" ? { display } : {}),
}
}
if (thinking.type === "disabled") return { type: "disabled" as const }
if (thinking.type !== "enabled") return undefined
const budget = const budget =
typeof thinking.budgetTokens === "number" typeof thinking.budgetTokens === "number"
? thinking.budgetTokens ? thinking.budgetTokens
@ -503,6 +526,11 @@ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (re
return { type: "enabled" as const, budget_tokens: budget } return { type: "enabled" as const, budget_tokens: budget }
}) })
const outputConfig = (request: LLMRequest) => {
const effort = anthropicOptions(request)?.effort
return typeof effort === "string" ? { effort } : undefined
}
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const generation = request.generation const generation = request.generation
@ -549,6 +577,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_k: generation?.topK, top_k: generation?.topK,
stop_sequences: generation?.stop, stop_sequences: generation?.stop,
thinking: yield* lowerThinking(request), thinking: yield* lowerThinking(request),
output_config: outputConfig(request),
} }
}) })

View file

@ -168,8 +168,6 @@ interface ParserState {
readonly lifecycle: Lifecycle.State readonly lifecycle: Lifecycle.State
} }
const invalid = ProviderShared.invalidRequest
// ============================================================================= // =============================================================================
// Request Lowering // Request Lowering
// ============================================================================= // =============================================================================
@ -333,8 +331,6 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) {
const store = OpenAIOptions.store(request) const store = OpenAIOptions.store(request)
const reasoningEffort = OpenAIOptions.reasoningEffort(request) const reasoningEffort = OpenAIOptions.reasoningEffort(request)
if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort))
return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`)
return { return {
...(store !== undefined ? { store } : {}), ...(store !== undefined ? { store } : {}),
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),

View file

@ -457,8 +457,6 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques
const store = OpenAIOptions.store(request) const store = OpenAIOptions.store(request)
const promptCacheKey = OpenAIOptions.promptCacheKey(request) const promptCacheKey = OpenAIOptions.promptCacheKey(request)
const effort = OpenAIOptions.reasoningEffort(request) const effort = OpenAIOptions.reasoningEffort(request)
if (effort && !OpenAIOptions.isReasoningEffort(effort))
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
const summary = OpenAIOptions.reasoningSummary(request) const summary = OpenAIOptions.reasoningSummary(request)
const include = OpenAIOptions.include(request) const include = OpenAIOptions.include(request)
const verbosity = OpenAIOptions.textVerbosity(request) const verbosity = OpenAIOptions.textVerbosity(request)

View file

@ -1,11 +1,9 @@
import { Schema } from "effect" import { Schema } from "effect"
import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema" import type { LLMRequest, TextVerbosity as TextVerbosityValue } from "../../schema"
import { ReasoningEfforts, TextVerbosity } from "../../schema" import { ReasoningEfforts, TextVerbosity } from "../../schema"
export const OpenAIReasoningEfforts = ReasoningEfforts.filter( export const OpenAIReasoningEfforts = ReasoningEfforts
(effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max", export type OpenAIReasoningEffort = string
)
export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number]
// Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
// in lockstep with `openai-node/src/resources/responses/responses.ts`. // in lockstep with `openai-node/src/resources/responses/responses.ts`.
@ -23,22 +21,16 @@ export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number
export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const
export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number]
const REASONING_EFFORTS = new Set<string>(ReasoningEfforts)
const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts)
const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"]) const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
const INCLUDABLES = new Set<string>(OpenAIResponseIncludables) const INCLUDABLES = new Set<string>(OpenAIResponseIncludables)
const SERVICE_TIERS = new Set<string>(OpenAIServiceTiers) const SERVICE_TIERS = new Set<string>(OpenAIServiceTiers)
export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts) export const OpenAIReasoningEffort = Schema.String
export const OpenAITextVerbosity = TextVerbosity export const OpenAITextVerbosity = TextVerbosity
export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables) export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables)
export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers) export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers)
const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort => export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => typeof effort === "string"
typeof effort === "string" && REASONING_EFFORTS.has(effort)
export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort =>
typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort)
const isTextVerbosity = (value: unknown): value is TextVerbosityValue => const isTextVerbosity = (value: unknown): value is TextVerbosityValue =>
typeof value === "string" && TEXT_VERBOSITY.has(value) typeof value === "string" && TEXT_VERBOSITY.has(value)
@ -50,9 +42,9 @@ export const store = (request: LLMRequest): boolean | undefined => {
return typeof value === "boolean" ? value : undefined return typeof value === "boolean" ? value : undefined
} }
export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => { export const reasoningEffort = (request: LLMRequest): string | undefined => {
const value = options(request)?.reasoningEffort const value = options(request)?.reasoningEffort
return isAnyReasoningEffort(value) ? value : undefined return typeof value === "string" ? value : undefined
} }
export const reasoningSummary = (request: LLMRequest): "auto" | undefined => export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>

View file

@ -27,7 +27,7 @@ export const ToolCallID = Schema.String
export type ToolCallID = Schema.Schema.Type<typeof ToolCallID> export type ToolCallID = Schema.Schema.Type<typeof ToolCallID>
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export const ReasoningEffort = Schema.Literals(ReasoningEfforts) export const ReasoningEffort = Schema.String
export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort> export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
export const TextVerbosity = Schema.Literals(["low", "medium", "high"]) export const TextVerbosity = Schema.Literals(["low", "medium", "high"])

View file

@ -57,6 +57,23 @@ describe("Anthropic Messages route", () => {
}), }),
) )
it.effect("lowers adaptive thinking settings with effort", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.updateRequest(request, {
providerOptions: {
anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
},
}),
)
expect(prepared.body).toMatchObject({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "low" },
})
}),
)
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () => it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(

View file

@ -98,12 +98,26 @@ describe("OpenAI Chat route", () => {
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"),
prompt: "think", prompt: "think",
providerOptions: { openai: { reasoningEffort: "low" } }, providerOptions: { openai: { reasoningEffort: "max" } },
}), }),
) )
expect(prepared.body.store).toBe(false) expect(prepared.body.store).toBe(false)
expect(prepared.body.reasoning_effort).toBe("low") expect(prepared.body.reasoning_effort).toBe("max")
}),
)
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({
model,
prompt: "think",
providerOptions: { openai: { reasoningEffort: "experimental" } },
}),
)
expect(prepared.body.reasoning_effort).toBe("experimental")
}), }),
) )

View file

@ -69,6 +69,16 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("passes through custom OpenAI reasoning effort strings", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.updateRequest(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }),
)
expect(prepared.body.reasoning).toEqual({ effort: "experimental" })
}),
)
it.effect("omits unsupported semantic service tiers", () => it.effect("omits unsupported semantic service tiers", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* LLMClient.prepare( const prepared = yield* LLMClient.prepare(

View file

@ -38,7 +38,7 @@ export const Info = Schema.Struct({
empty: (id: ID) => empty: (id: ID) =>
schema.make({ schema.make({
id, id,
request: { headers: {}, body: {} }, request: { settings: {}, headers: {}, body: {} },
mode: "all", mode: "all",
hidden: false, hidden: false,
permissions: [ permissions: [

View file

@ -94,7 +94,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: {} }, request: { settings: {}, headers: {}, body: {} },
variants: [], variants: [],
time: { released: 0 }, time: { released: 0 },
cost: [], cost: [],

View file

@ -1,6 +1,6 @@
export * as Provider from "./provider.js" export * as Provider from "./provider.js"
import { Schema } from "effect" import { Effect, Schema } from "effect"
import { optional } from "./schema.js" import { optional } from "./schema.js"
import { Integration } from "./integration.js" import { Integration } from "./integration.js"
import { statics } from "./schema.js" import { statics } from "./schema.js"
@ -43,8 +43,12 @@ export const Api = Schema.Union([AISDK, Native])
.annotate({ identifier: "Provider.Api" }) .annotate({ identifier: "Provider.Api" })
export type Api = typeof Api.Type export type Api = typeof Api.Type
export const Settings = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Provider.Settings" })
export type Settings = typeof Settings.Type
export interface Request extends Schema.Schema.Type<typeof Request> {} export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Request = Schema.Struct({ export const Request = Schema.Struct({
settings: Settings.pipe(Schema.withConstructorDefault(Effect.succeed({}))),
headers: Schema.Record(Schema.String, Schema.String), headers: Schema.Record(Schema.String, Schema.String),
body: Schema.Record(Schema.String, Schema.Json), body: Schema.Record(Schema.String, Schema.Json),
}).annotate({ identifier: "Provider.Request" }) }).annotate({ identifier: "Provider.Request" })
@ -66,7 +70,7 @@ export const Info = Schema.Struct({
id, id,
name: id, name: id,
api: { type: "native", settings: {} }, api: { type: "native", settings: {} },
request: { headers: {}, body: {} }, request: { settings: {}, headers: {}, body: {} },
}), }),
})), })),
) )

View file

@ -399,6 +399,8 @@ import type {
V2SessionSwitchAgentResponses, V2SessionSwitchAgentResponses,
V2SessionSwitchModelErrors, V2SessionSwitchModelErrors,
V2SessionSwitchModelResponses, V2SessionSwitchModelResponses,
V2SessionSyntheticErrors,
V2SessionSyntheticResponses,
V2SessionWaitErrors, V2SessionWaitErrors,
V2SessionWaitResponses, V2SessionWaitResponses,
V2ShellCreateErrors, V2ShellCreateErrors,
@ -5816,6 +5818,47 @@ export class Session3 extends HeyApiClient {
}) })
} }
/**
* Add synthetic message
*
* Append a synthetic message to a session and resume execution.
*/
public synthetic<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
text?: string
description?: string
metadata?: {
[key: string]: unknown
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "body", key: "text" },
{ in: "body", key: "description" },
{ in: "body", key: "metadata" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionSyntheticResponses, V2SessionSyntheticErrors, ThrowOnError>({
url: "/api/session/{sessionID}/synthetic",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/** /**
* Compact session * Compact session
* *

View file

@ -942,6 +942,9 @@ export type GlobalEvent = {
messageID: string messageID: string
text: string text: string
description?: string description?: string
metadata?: {
[key: string]: unknown
}
} }
} }
| { | {
@ -3618,6 +3621,9 @@ export type SyncEventSessionNextSynthetic = {
messageID: string messageID: string
text: string text: string
description?: string description?: string
metadata?: {
[key: string]: unknown
}
} }
} }
} }
@ -4091,7 +4097,12 @@ export type LocationInfo = {
} }
} }
export type ProviderSettings = {
[key: string]: unknown
}
export type ProviderRequest = { export type ProviderRequest = {
settings: ProviderSettings
headers: { headers: {
[key: string]: string [key: string]: string
} }
@ -4583,6 +4594,9 @@ export type SessionNextSynthetic = {
messageID: string messageID: string
text: string text: string
description?: string description?: string
metadata?: {
[key: string]: unknown
}
} }
} }
@ -5120,6 +5134,7 @@ export type ModelV2Info = {
api: ModelApi api: ModelApi
capabilities: ModelCapabilities capabilities: ModelCapabilities
request: { request: {
settings: ProviderSettings
headers: { headers: {
[key: string]: string [key: string]: string
} }
@ -5130,6 +5145,7 @@ export type ModelV2Info = {
} }
variants: Array<{ variants: Array<{
id: string id: string
settings: ProviderSettings
headers: { headers: {
[key: string]: string [key: string]: string
} }
@ -6806,6 +6822,9 @@ export type EventSessionNextSynthetic = {
messageID: string messageID: string
text: string text: string
description?: string description?: string
metadata?: {
[key: string]: unknown
}
} }
} }
@ -12284,6 +12303,47 @@ export type V2SessionSkillResponses = {
export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkillResponses] export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkillResponses]
export type V2SessionSyntheticData = {
body: {
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
path: {
sessionID: string
}
query?: never
url: "/api/session/{sessionID}/synthetic"
}
export type V2SessionSyntheticErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors]
export type V2SessionSyntheticResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses]
export type V2SessionCompactData = { export type V2SessionCompactData = {
body?: never body?: never
path: { path: {