mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7dcfc2680 |
@@ -84,7 +84,16 @@ const profiles = new Map<string, Profile>([
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
["@ai-sdk/anthropic", { namespace: "anthropic", semantics: new Map([["thinking", "thinking"]]) }],
|
[
|
||||||
|
"@ai-sdk/anthropic",
|
||||||
|
{
|
||||||
|
namespace: "anthropic",
|
||||||
|
semantics: new Map([
|
||||||
|
["thinking", "thinking"],
|
||||||
|
["effort", "effort"],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
|
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
|
||||||
|
|||||||
@@ -43,6 +43,30 @@ const Cost = Schema.Struct({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// models.dev curates reasoning_options per provider model as a discriminated
|
||||||
|
// union. The shape is expected to evolve, so stay lenient where it can grow:
|
||||||
|
// effort values are open strings (tiers like "xhigh" were added over time) and
|
||||||
|
// budget bounds are optional. The api.json payload is cast, never decoded, so
|
||||||
|
// option types this union doesn't know about yet can appear at runtime;
|
||||||
|
// consumers must filter for the types they understand instead of matching
|
||||||
|
// exhaustively.
|
||||||
|
export const ReasoningOption = Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("toggle"),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
// null means the provider accepts an explicit "no reasoning" effort.
|
||||||
|
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: Schema.optional(Schema.Finite),
|
||||||
|
max: Schema.optional(Schema.Finite),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
export type ReasoningOption = Schema.Schema.Type<typeof ReasoningOption>
|
||||||
|
|
||||||
export const Model = Schema.Struct({
|
export const Model = Schema.Struct({
|
||||||
id: Schema.String,
|
id: Schema.String,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
@@ -50,6 +74,7 @@ export const Model = Schema.Struct({
|
|||||||
release_date: Schema.String,
|
release_date: Schema.String,
|
||||||
attachment: Schema.Boolean,
|
attachment: Schema.Boolean,
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
|
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ModelRequest } from "../model-request"
|
|||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
import { PluginV2 } from "../plugin"
|
import { PluginV2 } from "../plugin"
|
||||||
import { ProviderV2 } from "../provider"
|
import { ProviderV2 } from "../provider"
|
||||||
|
import { ReasoningVariants } from "../reasoning-variants"
|
||||||
|
|
||||||
function released(date: string) {
|
function released(date: string) {
|
||||||
const time = Date.parse(date)
|
const time = Date.parse(date)
|
||||||
@@ -39,8 +40,8 @@ function cost(input: ModelsDev.Model["cost"]) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function variants(model: ModelsDev.Model, packageName?: string) {
|
function variants(model: ModelsDev.Model, providerID: string, packageName?: string) {
|
||||||
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => {
|
const modes = Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => {
|
||||||
const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
|
const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
|
||||||
return {
|
return {
|
||||||
id: ModelV2.VariantID.make(id),
|
id: ModelV2.VariantID.make(id),
|
||||||
@@ -48,6 +49,20 @@ function variants(model: ModelsDev.Model, packageName?: string) {
|
|||||||
...request,
|
...request,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// reasoning_options effort data generates effort variants with the same wire
|
||||||
|
// encodings the v1 catalog uses; curated experimental modes win on id collision.
|
||||||
|
const efforts = ReasoningVariants.fromOptions(
|
||||||
|
{ npm: packageName, apiID: model.id, modelID: model.id, providerID },
|
||||||
|
model.reasoning_options,
|
||||||
|
)
|
||||||
|
const fromEfforts = Object.entries(efforts ?? {})
|
||||||
|
.filter(([id]) => !modes.some((mode) => mode.id === id))
|
||||||
|
.map(([id, body]) => ({
|
||||||
|
id: ModelV2.VariantID.make(id),
|
||||||
|
headers: {},
|
||||||
|
...ModelRequest.normalizeAiSdkOptions(packageName, body),
|
||||||
|
}))
|
||||||
|
return [...modes, ...fromEfforts]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelsDevPlugin = PluginV2.define({
|
export const ModelsDevPlugin = PluginV2.define({
|
||||||
@@ -102,7 +117,7 @@ export const ModelsDevPlugin = PluginV2.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, item.id, model.provider?.npm ?? item.npm)
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
export * as ReasoningVariants from "./reasoning-variants"
|
||||||
|
|
||||||
|
// Generates reasoning variants from models.dev `reasoning_options` data. The
|
||||||
|
// data only says WHICH efforts a model supports - the wire encoding for each
|
||||||
|
// SDK package lives here so the v1 provider catalog and the v2 catalog plugin
|
||||||
|
// stay in lockstep. Null effort values and option types we don't understand
|
||||||
|
// (toggle, budget_tokens, future additions) are ignored, so models without
|
||||||
|
// usable effort data return undefined and callers fall back to their own
|
||||||
|
// defaults.
|
||||||
|
|
||||||
|
// OpenAI Responses `include` value that returns the encrypted reasoning state
|
||||||
|
// needed for stateless multi-turn reasoning (store: false). Hoisted so every
|
||||||
|
// branch that requests it stays in lockstep.
|
||||||
|
export const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const
|
||||||
|
|
||||||
|
export interface Target {
|
||||||
|
readonly npm?: string
|
||||||
|
readonly apiID: string
|
||||||
|
readonly modelID: string
|
||||||
|
readonly providerID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromOptions(
|
||||||
|
target: Target,
|
||||||
|
options: ReadonlyArray<{ readonly type: string; readonly values?: ReadonlyArray<string | null> }> | undefined,
|
||||||
|
): Record<string, Record<string, unknown>> | undefined {
|
||||||
|
const efforts = [
|
||||||
|
...new Set(
|
||||||
|
(options ?? [])
|
||||||
|
.flatMap((option) => (option.type === "effort" ? (option.values ?? []) : []))
|
||||||
|
.filter((value): value is string => typeof value === "string"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if (efforts.length === 0) return undefined
|
||||||
|
return effortVariants(target, efforts)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anthropicOpus47OrLater(apiID: string) {
|
||||||
|
// Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
|
||||||
|
// Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
|
||||||
|
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiID)
|
||||||
|
if (!version) return false
|
||||||
|
const major = Number(version[1] ?? version[3])
|
||||||
|
const minor = Number(version[2] ?? version[4])
|
||||||
|
return major > 4 || (major === 4 && minor >= 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anthropicAdaptiveEfforts(apiID: string): string[] | null {
|
||||||
|
if (anthropicOpus47OrLater(apiID) || apiID.includes("fable-5")) {
|
||||||
|
return ["low", "medium", "high", "xhigh", "max"]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some((v) =>
|
||||||
|
apiID.includes(v),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return ["low", "medium", "high", "max"]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anthropicOmitsThinking(apiID: string) {
|
||||||
|
return anthropicOpus47OrLater(apiID) || apiID.includes("fable-5")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAP's Zod schema drops unknown top-level keys; reasoning controls survive
|
||||||
|
// only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
|
||||||
|
export function wrapInSapModelParams(
|
||||||
|
variants: Record<string, Record<string, unknown>>,
|
||||||
|
): Record<string, Record<string, unknown>> {
|
||||||
|
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function copilotAnthropicEfforts(apiID: string, efforts: string[]) {
|
||||||
|
// Efforts currently supported by copilot are: low, medium, high
|
||||||
|
if (apiID.includes("opus-4.7")) return ["medium"]
|
||||||
|
return efforts.filter((v) => v !== "max" && v !== "xhigh")
|
||||||
|
}
|
||||||
|
|
||||||
|
function anthropicEffortVariants(target: Target, efforts: string[]): Record<string, Record<string, unknown>> {
|
||||||
|
const filtered = target.providerID === "github-copilot" ? copilotAnthropicEfforts(target.apiID, efforts) : efforts
|
||||||
|
const adaptive = anthropicAdaptiveEfforts(target.apiID) !== null
|
||||||
|
return Object.fromEntries(
|
||||||
|
filtered.map((effort) => [
|
||||||
|
effort,
|
||||||
|
adaptive
|
||||||
|
? {
|
||||||
|
thinking: {
|
||||||
|
type: "adaptive",
|
||||||
|
// Newer adaptive-only models default `display` to "omitted", which
|
||||||
|
// returns empty thinking blocks. Force "summarized" so summaries
|
||||||
|
// survive (4.6/Sonnet 4.6 already default to "summarized").
|
||||||
|
...(anthropicOmitsThinking(target.apiID) ? { display: "summarized" } : {}),
|
||||||
|
},
|
||||||
|
effort,
|
||||||
|
}
|
||||||
|
: { effort },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function effortVariants(target: Target, efforts: string[]): Record<string, Record<string, unknown>> {
|
||||||
|
const fromEffort = (encode: (effort: string) => Record<string, unknown>) =>
|
||||||
|
Object.fromEntries(efforts.map((effort) => [effort, encode(effort)]))
|
||||||
|
|
||||||
|
switch (target.npm) {
|
||||||
|
case "@openrouter/ai-sdk-provider":
|
||||||
|
return fromEffort((effort) => ({ reasoning: { effort } }))
|
||||||
|
|
||||||
|
case "@ai-sdk/gateway":
|
||||||
|
if (target.modelID.includes("anthropic")) return anthropicEffortVariants(target, efforts)
|
||||||
|
if (target.modelID.includes("google"))
|
||||||
|
return fromEffort((effort) => ({ includeThoughts: true, thinkingLevel: effort }))
|
||||||
|
return fromEffort((effort) => ({ reasoningEffort: effort }))
|
||||||
|
|
||||||
|
case "@ai-sdk/github-copilot":
|
||||||
|
// currently github copilot only returns thinking
|
||||||
|
if (target.modelID.includes("gemini")) return {}
|
||||||
|
if (target.modelID.includes("claude")) return fromEffort((effort) => ({ reasoningEffort: effort }))
|
||||||
|
return fromEffort((effort) => ({
|
||||||
|
reasoningEffort: effort,
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: INCLUDE_ENCRYPTED_REASONING,
|
||||||
|
}))
|
||||||
|
|
||||||
|
case "@ai-sdk/azure":
|
||||||
|
case "@ai-sdk/amazon-bedrock/mantle":
|
||||||
|
case "@ai-sdk/openai":
|
||||||
|
return fromEffort((effort) => ({
|
||||||
|
reasoningEffort: effort,
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: INCLUDE_ENCRYPTED_REASONING,
|
||||||
|
}))
|
||||||
|
|
||||||
|
case "@ai-sdk/anthropic":
|
||||||
|
case "@ai-sdk/google-vertex/anthropic":
|
||||||
|
return anthropicEffortVariants(target, efforts)
|
||||||
|
|
||||||
|
case "@ai-sdk/amazon-bedrock":
|
||||||
|
if (anthropicAdaptiveEfforts(target.apiID)) {
|
||||||
|
return fromEffort((effort) => ({
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "adaptive",
|
||||||
|
maxReasoningEffort: effort,
|
||||||
|
...(anthropicOmitsThinking(target.apiID) ? { display: "summarized" } : {}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return fromEffort((effort) => ({
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "enabled",
|
||||||
|
maxReasoningEffort: effort,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
case "@ai-sdk/google-vertex":
|
||||||
|
case "@ai-sdk/google":
|
||||||
|
return fromEffort((effort) => ({ thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }))
|
||||||
|
|
||||||
|
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||||
|
if (target.modelID.toLowerCase().includes("anthropic")) {
|
||||||
|
const adaptive = anthropicAdaptiveEfforts(target.apiID) !== null
|
||||||
|
// Bedrock-flavored Anthropic splits `effort` out into `output_config` (vs
|
||||||
|
// Anthropic native which inlines it).
|
||||||
|
return wrapInSapModelParams(
|
||||||
|
fromEffort((effort) =>
|
||||||
|
adaptive
|
||||||
|
? {
|
||||||
|
thinking: {
|
||||||
|
type: "adaptive",
|
||||||
|
...(anthropicOmitsThinking(target.apiID) ? { display: "summarized" } : {}),
|
||||||
|
},
|
||||||
|
output_config: { effort },
|
||||||
|
}
|
||||||
|
: { output_config: { effort } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return wrapInSapModelParams(fromEffort((effort) => ({ reasoning_effort: effort })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI-compatible `reasoning_effort` is the dominant convention; it covers
|
||||||
|
// @ai-sdk/openai-compatible, cerebras, togetherai, xai, deepinfra, venice,
|
||||||
|
// mistral, groq, ai-gateway-provider, and unknown future packages.
|
||||||
|
return fromEffort((effort) => ({ reasoningEffort: effort }))
|
||||||
|
}
|
||||||
@@ -12,6 +12,28 @@ export const Model = Schema.Struct({
|
|||||||
release_date: Schema.optional(Schema.String),
|
release_date: Schema.optional(Schema.String),
|
||||||
attachment: Schema.optional(Schema.Boolean),
|
attachment: Schema.optional(Schema.Boolean),
|
||||||
reasoning: Schema.optional(Schema.Boolean),
|
reasoning: Schema.optional(Schema.Boolean),
|
||||||
|
reasoning_options: Schema.optional(
|
||||||
|
// Mirrors the models.dev reasoning_options union; effort values stay open
|
||||||
|
// strings so new tiers don't break configs.
|
||||||
|
Schema.mutable(
|
||||||
|
Schema.Array(
|
||||||
|
Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("toggle"),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
values: Schema.mutable(Schema.Array(Schema.NullOr(Schema.String))),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: Schema.optional(Schema.Finite),
|
||||||
|
max: Schema.optional(Schema.Finite),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).annotate({ description: "Reasoning controls this model supports; effort values drive reasoning variants" }),
|
||||||
temperature: Schema.optional(Schema.Boolean),
|
temperature: Schema.optional(Schema.Boolean),
|
||||||
tool_call: Schema.optional(Schema.Boolean),
|
tool_call: Schema.optional(Schema.Boolean),
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
|
|||||||
@@ -568,7 +568,12 @@ describe("Config", () => {
|
|||||||
model: {
|
model: {
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
output_config: { effort: "high", task_budget: 4096 },
|
// `effort` stays aisdk-shaped: the catalog re-partitions it
|
||||||
|
// into the semantic anthropic effort option, which lowers to
|
||||||
|
// output_config.effort plus the effort beta header. taskBudget
|
||||||
|
// has no semantic and lowers to a raw output_config overlay.
|
||||||
|
effort: "high",
|
||||||
|
output_config: { task_budget: 4096 },
|
||||||
metadata: { user_id: "user-1" },
|
metadata: { user_id: "user-1" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
|
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { location } from "./fixture/location"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
const model = (input: Partial<ModelsDev.Model> & { id: string }): ModelsDev.Model => ({
|
||||||
|
name: input.id,
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: true,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 200_000, output: 64_000 },
|
||||||
|
...input,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fixture: Record<string, ModelsDev.Provider> = {
|
||||||
|
anthropic: {
|
||||||
|
id: "anthropic",
|
||||||
|
name: "Anthropic",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/anthropic",
|
||||||
|
models: {
|
||||||
|
"claude-sonnet-4-6": model({
|
||||||
|
id: "claude-sonnet-4-6",
|
||||||
|
reasoning_options: [
|
||||||
|
{ type: "effort", values: ["low", "medium", "high", "max"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compat: {
|
||||||
|
id: "compat",
|
||||||
|
name: "Compat",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
models: {
|
||||||
|
"deepseek-v4": model({
|
||||||
|
id: "deepseek-v4",
|
||||||
|
reasoning_options: [{ type: "toggle" }, { type: "effort", values: [null, "high", "max"] }],
|
||||||
|
experimental: {
|
||||||
|
modes: {
|
||||||
|
high: { provider: { body: { reasoning_effort: "high", custom: true } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationLayer = Layer.succeed(
|
||||||
|
Location.Service,
|
||||||
|
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||||
|
)
|
||||||
|
const modelsDevLayer = Layer.succeed(
|
||||||
|
ModelsDev.Service,
|
||||||
|
ModelsDev.Service.of({
|
||||||
|
get: () => Effect.succeed(fixture),
|
||||||
|
refresh: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const it = testEffect(
|
||||||
|
Layer.mergeAll(modelsDevLayer, Catalog.locationLayer).pipe(
|
||||||
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
|
Layer.provideMerge(locationLayer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe("ModelsDevPlugin reasoning_options", () => {
|
||||||
|
it.effect("generates anthropic effort variants as semantic thinking + effort options", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ModelsDevPlugin.effect
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const info = yield* catalog.model.get(ProviderV2.ID.make("anthropic"), ModelV2.ID.make("claude-sonnet-4-6"))
|
||||||
|
expect(info.variants.map((variant) => variant.id)).toEqual(
|
||||||
|
["low", "medium", "high", "max"].map((id) => ModelV2.VariantID.make(id)),
|
||||||
|
)
|
||||||
|
expect(info.variants[2]).toMatchObject({
|
||||||
|
id: "high",
|
||||||
|
headers: {},
|
||||||
|
body: {},
|
||||||
|
options: { thinking: { type: "adaptive" }, effort: "high" },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("merges effort variants after curated experimental modes, skipping null values and collisions", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ModelsDevPlugin.effect
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const info = yield* catalog.model.get(ProviderV2.ID.make("compat"), ModelV2.ID.make("deepseek-v4"))
|
||||||
|
expect(info.variants.map((variant) => variant.id)).toEqual(["high", "max"].map((id) => ModelV2.VariantID.make(id)))
|
||||||
|
// curated mode wins the "high" id; its body keys survive partitioning
|
||||||
|
expect(info.variants[0]).toMatchObject({
|
||||||
|
id: "high",
|
||||||
|
body: { custom: true },
|
||||||
|
options: { reasoningEffort: "high" },
|
||||||
|
})
|
||||||
|
// data-driven effort variant for "max" uses the openai-compatible encoding
|
||||||
|
expect(info.variants[1]).toMatchObject({
|
||||||
|
id: "max",
|
||||||
|
body: {},
|
||||||
|
options: { reasoningEffort: "max" },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -146,10 +146,20 @@ 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,
|
||||||
|
}),
|
||||||
|
// Adaptive thinking (Claude 4.6+) lets the model choose its own budget.
|
||||||
|
// `display` controls how thinking is surfaced ("summarized" forces summaries
|
||||||
|
// on models that default to "omitted"); keep it an open string so new display
|
||||||
|
// modes flow through.
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.tag("adaptive"),
|
||||||
|
display: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
const AnthropicBodyFields = {
|
const AnthropicBodyFields = {
|
||||||
model: Schema.String,
|
model: Schema.String,
|
||||||
@@ -164,6 +174,8 @@ 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),
|
||||||
|
// Reasoning effort (beta `effort-2025-11-24`); open string so new tiers flow through.
|
||||||
|
output_config: Schema.optional(Schema.Struct({ effort: Schema.String })),
|
||||||
}
|
}
|
||||||
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>
|
||||||
@@ -490,7 +502,14 @@ 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") {
|
||||||
|
return {
|
||||||
|
type: "adaptive" as const,
|
||||||
|
...(typeof thinking.display === "string" ? { display: thinking.display } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (thinking.type !== "enabled") return undefined
|
||||||
const budget =
|
const budget =
|
||||||
typeof thinking.budgetTokens === "number"
|
typeof thinking.budgetTokens === "number"
|
||||||
? thinking.budgetTokens
|
? thinking.budgetTokens
|
||||||
@@ -501,6 +520,14 @@ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (re
|
|||||||
return { type: "enabled" as const, budget_tokens: budget }
|
return { type: "enabled" as const, budget_tokens: budget }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reasoning effort lowers to `output_config.effort` (mirrors @ai-sdk/anthropic);
|
||||||
|
// the matching beta header is added by the route headers hook below.
|
||||||
|
const lowerOutputConfig = (request: LLMRequest) => {
|
||||||
|
const effort = anthropicOptions(request)?.effort
|
||||||
|
if (typeof effort !== "string") return undefined
|
||||||
|
return { effort }
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -539,6 +566,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: lowerOutputConfig(request),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -839,7 +867,12 @@ export const route = Route.make({
|
|||||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||||
auth: Auth.none,
|
auth: Auth.none,
|
||||||
framing: Framing.sse,
|
framing: Framing.sse,
|
||||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
// `output_config.effort` is beta-gated. Explicit per-request `anthropic-beta`
|
||||||
|
// headers override this hook (the transport spreads request headers last).
|
||||||
|
headers: ({ request }) => ({
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
...(typeof anthropicOptions(request)?.effort === "string" ? { "anthropic-beta": "effort-2025-11-24" } : {}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export * as AnthropicMessages from "./anthropic-messages"
|
export * as AnthropicMessages from "./anthropic-messages"
|
||||||
|
|||||||
@@ -57,6 +57,58 @@ describe("Anthropic Messages route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("lowers enabled thinking to a budget", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||||
|
LLM.updateRequest(request, {
|
||||||
|
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 16_000 } } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(prepared.body.thinking).toEqual({ type: "enabled", budget_tokens: 16_000 })
|
||||||
|
expect(prepared.body.output_config).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("lowers adaptive thinking and effort to output_config", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||||
|
LLM.updateRequest(request, {
|
||||||
|
providerOptions: {
|
||||||
|
anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(prepared.body.thinking).toEqual({ type: "adaptive", display: "summarized" })
|
||||||
|
expect(prepared.body.output_config).toEqual({ effort: "high" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("adds the effort beta header only when effort is set", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const seen: Record<string, string>[] = []
|
||||||
|
const body = () =>
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 1 } } },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
|
)
|
||||||
|
const layer = dynamicResponse((input) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
seen.push({ ...input.request.headers })
|
||||||
|
return input.respond(body(), { headers: { "content-type": "text/event-stream" } })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* LLMClient.generate(
|
||||||
|
LLM.updateRequest(request, { providerOptions: { anthropic: { effort: "high" } } }),
|
||||||
|
).pipe(Effect.provide(layer))
|
||||||
|
yield* LLMClient.generate(request).pipe(Effect.provide(layer))
|
||||||
|
|
||||||
|
expect(seen[0]["anthropic-version"]).toBe("2023-06-01")
|
||||||
|
expect(seen[0]["anthropic-beta"]).toBe("effort-2025-11-24")
|
||||||
|
expect(seen[1]["anthropic-beta"]).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
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>(
|
||||||
|
|||||||
@@ -957,9 +957,30 @@ const ProviderInterleaved = Schema.Union([
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Mirrors the models.dev reasoning_options union. Option types this union does
|
||||||
|
// not know about yet, and null effort values (models.dev's marker for "accepts
|
||||||
|
// an explicit no-reasoning effort"), are dropped when mapping models.dev data
|
||||||
|
// into the resolved catalog, so this schema only ever sees known variants;
|
||||||
|
// effort values stay open strings so new tiers flow through.
|
||||||
|
const ProviderReasoningOption = Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("toggle"),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
values: Schema.Array(Schema.String),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: optionalOmitUndefined(Schema.Finite),
|
||||||
|
max: optionalOmitUndefined(Schema.Finite),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
const ProviderCapabilities = Schema.Struct({
|
const ProviderCapabilities = Schema.Struct({
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
|
reasoningOptions: optionalOmitUndefined(Schema.Array(ProviderReasoningOption)),
|
||||||
attachment: Schema.Boolean,
|
attachment: Schema.Boolean,
|
||||||
toolcall: Schema.Boolean,
|
toolcall: Schema.Boolean,
|
||||||
input: ProviderModalities,
|
input: ProviderModalities,
|
||||||
@@ -1155,6 +1176,20 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// models.dev api.json is cast rather than decoded, so reasoning_options can
|
||||||
|
// contain option types newer than our union at runtime; drop those (and null
|
||||||
|
// effort values) here so downstream schema boundaries only ever see known
|
||||||
|
// variants.
|
||||||
|
function reasoningOptions(options: ModelsDev.Model["reasoning_options"]): Model["capabilities"]["reasoningOptions"] {
|
||||||
|
return options
|
||||||
|
?.filter((option) => option.type === "toggle" || option.type === "effort" || option.type === "budget_tokens")
|
||||||
|
.map((option) =>
|
||||||
|
option.type === "effort"
|
||||||
|
? { ...option, values: option.values.filter((value): value is string => value !== null) }
|
||||||
|
: { ...option },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||||
const base: Model = {
|
const base: Model = {
|
||||||
id: ModelV2.ID.make(model.id),
|
id: ModelV2.ID.make(model.id),
|
||||||
@@ -1178,6 +1213,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: model.temperature ?? false,
|
temperature: model.temperature ?? false,
|
||||||
reasoning: model.reasoning ?? false,
|
reasoning: model.reasoning ?? false,
|
||||||
|
reasoningOptions: reasoningOptions(model.reasoning_options),
|
||||||
attachment: model.attachment ?? false,
|
attachment: model.attachment ?? false,
|
||||||
toolcall: model.tool_call ?? true,
|
toolcall: model.tool_call ?? true,
|
||||||
input: {
|
input: {
|
||||||
@@ -1400,6 +1436,8 @@ export const layer = Layer.effect(
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
||||||
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
||||||
|
reasoningOptions:
|
||||||
|
reasoningOptions(model.reasoning_options) ?? existingModel?.capabilities.reasoningOptions,
|
||||||
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
||||||
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mergeDeep, unique } from "remeda"
|
|||||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||||
import type * as Provider from "./provider"
|
import type * as Provider from "./provider"
|
||||||
import type * as ModelsDev from "@opencode-ai/core/models-dev"
|
import type * as ModelsDev from "@opencode-ai/core/models-dev"
|
||||||
|
import { ReasoningVariants } from "@opencode-ai/core/reasoning-variants"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||||
@@ -17,11 +18,6 @@ function mimeToModality(mime: string): Modality | undefined {
|
|||||||
|
|
||||||
export const OUTPUT_TOKEN_MAX = 32_000
|
export const OUTPUT_TOKEN_MAX = 32_000
|
||||||
|
|
||||||
// OpenAI Responses `include` value that returns the encrypted reasoning state
|
|
||||||
// needed for stateless multi-turn reasoning (store: false). Hoisted so every
|
|
||||||
// branch that requests it stays in lockstep.
|
|
||||||
const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const
|
|
||||||
|
|
||||||
export function sanitizeSurrogates(content: string) {
|
export function sanitizeSurrogates(content: string) {
|
||||||
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
||||||
}
|
}
|
||||||
@@ -595,34 +591,6 @@ function openaiCompatibleReasoningEfforts(id: string) {
|
|||||||
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
||||||
}
|
}
|
||||||
|
|
||||||
function anthropicOpus47OrLater(apiId: string) {
|
|
||||||
// Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
|
|
||||||
// Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
|
|
||||||
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
|
|
||||||
if (!version) return false
|
|
||||||
const major = Number(version[1] ?? version[3])
|
|
||||||
const minor = Number(version[2] ?? version[4])
|
|
||||||
return major > 4 || (major === 4 && minor >= 7)
|
|
||||||
}
|
|
||||||
|
|
||||||
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
|
||||||
if (anthropicOpus47OrLater(apiId) || apiId.includes("fable-5")) {
|
|
||||||
return ["low", "medium", "high", "xhigh", "max"]
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some((v) =>
|
|
||||||
apiId.includes(v),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return ["low", "medium", "high", "max"]
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function anthropicOmitsThinking(apiId: string) {
|
|
||||||
return anthropicOpus47OrLater(apiId) || apiId.includes("fable-5")
|
|
||||||
}
|
|
||||||
|
|
||||||
function googleThinkingLevelEfforts(apiId: string) {
|
function googleThinkingLevelEfforts(apiId: string) {
|
||||||
const id = apiId.toLowerCase()
|
const id = apiId.toLowerCase()
|
||||||
if (!id.includes("gemini-3")) return ["low", "high"]
|
if (!id.includes("gemini-3")) return ["low", "high"]
|
||||||
@@ -638,12 +606,6 @@ function googleThinkingBudgetMax(apiId: string) {
|
|||||||
return 24_576
|
return 24_576
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAP's Zod schema drops unknown top-level keys; reasoning controls survive
|
|
||||||
// only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
|
|
||||||
function wrapInSapModelParams(variants: Record<string, Record<string, any>>): Record<string, Record<string, any>> {
|
|
||||||
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
|
||||||
}
|
|
||||||
|
|
||||||
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||||
const id = model.api.id.toLowerCase()
|
const id = model.api.id.toLowerCase()
|
||||||
if (id.includes("2.5")) {
|
if (id.includes("2.5")) {
|
||||||
@@ -665,6 +627,14 @@ function googleThinkingVariants(model: Provider.Model): Record<string, Record<st
|
|||||||
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||||
if (!model.capabilities.reasoning) return {}
|
if (!model.capabilities.reasoning) return {}
|
||||||
|
|
||||||
|
// models.dev reasoning_options effort data drives variants when present;
|
||||||
|
// models without usable effort data fall back to the hardcoded tables below.
|
||||||
|
const fromData = ReasoningVariants.fromOptions(
|
||||||
|
{ npm: model.api.npm, apiID: model.api.id, modelID: model.id, providerID: model.providerID },
|
||||||
|
model.capabilities.reasoningOptions,
|
||||||
|
)
|
||||||
|
if (fromData) return fromData
|
||||||
|
|
||||||
const id = model.id.toLowerCase()
|
const id = model.id.toLowerCase()
|
||||||
if (
|
if (
|
||||||
model.api.id.toLowerCase().includes("minimax-m3") &&
|
model.api.id.toLowerCase().includes("minimax-m3") &&
|
||||||
@@ -675,8 +645,8 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
thinking: { thinking: { type: "adaptive" } },
|
thinking: { thinking: { type: "adaptive" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const adaptiveThinkingOmitted = anthropicOmitsThinking(model.api.id)
|
const adaptiveThinkingOmitted = ReasoningVariants.anthropicOmitsThinking(model.api.id)
|
||||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
const adaptiveEfforts = ReasoningVariants.anthropicAdaptiveEfforts(model.api.id)
|
||||||
if (
|
if (
|
||||||
id.includes("deepseek-chat") ||
|
id.includes("deepseek-chat") ||
|
||||||
id.includes("deepseek-reasoner") ||
|
id.includes("deepseek-reasoner") ||
|
||||||
@@ -815,7 +785,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
{
|
{
|
||||||
reasoningEffort: effort,
|
reasoningEffort: effort,
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: INCLUDE_ENCRYPTED_REASONING,
|
include: ReasoningVariants.INCLUDE_ENCRYPTED_REASONING,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -849,7 +819,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
{
|
{
|
||||||
reasoningEffort: effort,
|
reasoningEffort: effort,
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: INCLUDE_ENCRYPTED_REASONING,
|
include: ReasoningVariants.INCLUDE_ENCRYPTED_REASONING,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -863,7 +833,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
{
|
{
|
||||||
reasoningEffort: effort,
|
reasoningEffort: effort,
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: INCLUDE_ENCRYPTED_REASONING,
|
include: ReasoningVariants.INCLUDE_ENCRYPTED_REASONING,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -1010,7 +980,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
if (adaptiveEfforts) {
|
if (adaptiveEfforts) {
|
||||||
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
||||||
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
||||||
return wrapInSapModelParams(
|
return ReasoningVariants.wrapInSapModelParams(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
adaptiveEfforts.map((effort) => [
|
adaptiveEfforts.map((effort) => [
|
||||||
effort,
|
effort,
|
||||||
@@ -1022,19 +992,19 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return wrapInSapModelParams({
|
return ReasoningVariants.wrapInSapModelParams({
|
||||||
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||||
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (id.includes("gemini") && id.includes("2.5")) {
|
if (id.includes("gemini") && id.includes("2.5")) {
|
||||||
return wrapInSapModelParams(googleThinkingVariants(model))
|
return ReasoningVariants.wrapInSapModelParams(googleThinkingVariants(model))
|
||||||
}
|
}
|
||||||
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
|
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
|
||||||
const efforts = openaiReasoningEfforts(id, model.release_date)
|
const efforts = openaiReasoningEfforts(id, model.release_date)
|
||||||
return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])))
|
return ReasoningVariants.wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])))
|
||||||
}
|
}
|
||||||
return wrapInSapModelParams(
|
return ReasoningVariants.wrapInSapModelParams(
|
||||||
Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])),
|
Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1131,7 @@ export function options(input: {
|
|||||||
result["reasoningSummary"] = "auto"
|
result["reasoningSummary"] = "auto"
|
||||||
}
|
}
|
||||||
if (input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle") {
|
if (input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle") {
|
||||||
result["include"] = INCLUDE_ENCRYPTED_REASONING
|
result["include"] = ReasoningVariants.INCLUDE_ENCRYPTED_REASONING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1148,7 @@ export function options(input: {
|
|||||||
|
|
||||||
if (input.model.providerID.startsWith("opencode")) {
|
if (input.model.providerID.startsWith("opencode")) {
|
||||||
result["promptCacheKey"] = input.sessionID
|
result["promptCacheKey"] = input.sessionID
|
||||||
result["include"] = INCLUDE_ENCRYPTED_REASONING
|
result["include"] = ReasoningVariants.INCLUDE_ENCRYPTED_REASONING
|
||||||
result["reasoningSummary"] = "auto"
|
result["reasoningSummary"] = "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { ConfigProviderV1 } from "@opencode-ai/core/v1/config/provider"
|
||||||
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
|
import { Provider } from "@/provider/provider"
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: [null, "low", "medium", "high", "xhigh", "max", "ultrathink"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
{ type: "budget_tokens", min: 0, max: 24_576 },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("reasoning_options schemas", () => {
|
||||||
|
test("models.dev model schema decodes all known option shapes", () => {
|
||||||
|
const model = Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||||
|
id: "test-model",
|
||||||
|
name: "Test Model",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: options,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 128000, output: 8192 },
|
||||||
|
})
|
||||||
|
expect(model.reasoning_options).toEqual(options as typeof model.reasoning_options)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("config model schema decodes reasoning_options", () => {
|
||||||
|
const model = Schema.decodeUnknownSync(ConfigProviderV1.Model)({ reasoning_options: options })
|
||||||
|
expect(model.reasoning_options).toEqual(options as typeof model.reasoning_options)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("provider capabilities decode reasoningOptions", () => {
|
||||||
|
// The resolved catalog never carries null effort values; they are stripped
|
||||||
|
// when mapping models.dev data, so the resolved schema rejects them.
|
||||||
|
const resolved = [
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: ["low", "medium", "high", "xhigh", "max", "ultrathink"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
{ type: "budget_tokens", min: 0, max: 24_576 },
|
||||||
|
]
|
||||||
|
const capabilities = Schema.decodeUnknownSync(Provider.Model.fields.capabilities)({
|
||||||
|
temperature: true,
|
||||||
|
reasoning: true,
|
||||||
|
reasoningOptions: resolved,
|
||||||
|
attachment: false,
|
||||||
|
toolcall: true,
|
||||||
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
})
|
||||||
|
expect(capabilities.reasoningOptions).toEqual(resolved as typeof capabilities.reasoningOptions)
|
||||||
|
expect(() =>
|
||||||
|
Schema.decodeUnknownSync(Provider.Model.fields.capabilities)({
|
||||||
|
temperature: true,
|
||||||
|
reasoning: true,
|
||||||
|
reasoningOptions: [{ type: "effort", values: [null, "low"] }],
|
||||||
|
attachment: false,
|
||||||
|
toolcall: true,
|
||||||
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
}),
|
||||||
|
).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3962,6 +3962,226 @@ describe("ProviderTransform.variants", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("ProviderTransform.variants - models.dev reasoning_options", () => {
|
||||||
|
const createModel = (overrides: Partial<any> = {}): any => ({
|
||||||
|
id: "test/test-model",
|
||||||
|
providerID: "test",
|
||||||
|
api: {
|
||||||
|
id: "test-model",
|
||||||
|
url: "https://api.test.com",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
},
|
||||||
|
name: "Test Model",
|
||||||
|
capabilities: {
|
||||||
|
temperature: true,
|
||||||
|
reasoning: true,
|
||||||
|
attachment: false,
|
||||||
|
toolcall: true,
|
||||||
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
|
},
|
||||||
|
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||||
|
limit: { context: 200_000, output: 64_000 },
|
||||||
|
status: "active",
|
||||||
|
options: {},
|
||||||
|
headers: {},
|
||||||
|
release_date: "2024-01-01",
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const withOptions = (model: any, reasoningOptions: any[]) => {
|
||||||
|
model.capabilities.reasoningOptions = reasoningOptions
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
test("effort values drive variants for openai-compatible packages", () => {
|
||||||
|
const model = withOptions(createModel(), [{ type: "toggle" }, { type: "effort", values: ["high", "max"] }])
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
high: { reasoningEffort: "high" },
|
||||||
|
max: { reasoningEffort: "max" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("effort values drive variants for openai with summary and encrypted reasoning", () => {
|
||||||
|
const model = withOptions(createModel({ api: { id: "gpt-x", url: "", npm: "@ai-sdk/openai" } }), [
|
||||||
|
{ type: "effort", values: ["low", "high"] },
|
||||||
|
])
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { reasoningEffort: "low", reasoningSummary: "auto", include: ["reasoning.encrypted_content"] },
|
||||||
|
high: { reasoningEffort: "high", reasoningSummary: "auto", include: ["reasoning.encrypted_content"] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("null efforts, duplicates, and unknown option types are ignored", () => {
|
||||||
|
const model = withOptions(createModel(), [
|
||||||
|
{ type: "future-thing", anything: true },
|
||||||
|
{ type: "effort", values: [null, "low"] },
|
||||||
|
{ type: "effort", values: ["low", "high"] },
|
||||||
|
])
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { reasoningEffort: "low" },
|
||||||
|
high: { reasoningEffort: "high" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggle-only data falls back to the hardcoded tables", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "minimax/minimax-m3",
|
||||||
|
providerID: "minimax",
|
||||||
|
api: { id: "MiniMax-M3", url: "", npm: "@ai-sdk/anthropic" },
|
||||||
|
}),
|
||||||
|
[{ type: "toggle" }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
none: { thinking: { type: "disabled" } },
|
||||||
|
thinking: { thinking: { type: "adaptive" } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("budget-only data falls back to the hardcoded anthropic budget variants", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "anthropic/claude-haiku-4-5",
|
||||||
|
providerID: "anthropic",
|
||||||
|
api: { id: "claude-haiku-4-5", url: "", npm: "@ai-sdk/anthropic" },
|
||||||
|
}),
|
||||||
|
[{ type: "budget_tokens", min: 1024 }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
high: { thinking: { type: "enabled", budgetTokens: 16_000 } },
|
||||||
|
max: { thinking: { type: "enabled", budgetTokens: 31_999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("empty effort values fall back to the hardcoded tables", () => {
|
||||||
|
const model = withOptions(createModel({ api: { id: "gpt-x", url: "", npm: "@ai-sdk/openai" } }), [
|
||||||
|
{ type: "effort", values: [] },
|
||||||
|
])
|
||||||
|
expect(Object.keys(ProviderTransform.variants(model))).toEqual(["low", "medium", "high"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("anthropic adaptive models wrap data efforts in adaptive thinking", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "anthropic/claude-sonnet-4-6",
|
||||||
|
providerID: "anthropic",
|
||||||
|
api: { id: "claude-sonnet-4-6", url: "", npm: "@ai-sdk/anthropic" },
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{ type: "effort", values: ["low", "medium", "high", "max"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { thinking: { type: "adaptive" }, effort: "low" },
|
||||||
|
medium: { thinking: { type: "adaptive" }, effort: "medium" },
|
||||||
|
high: { thinking: { type: "adaptive" }, effort: "high" },
|
||||||
|
max: { thinking: { type: "adaptive" }, effort: "max" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("anthropic non-adaptive models encode data efforts as plain effort", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "anthropic/claude-opus-4-5",
|
||||||
|
providerID: "anthropic",
|
||||||
|
api: { id: "claude-opus-4-5", url: "", npm: "@ai-sdk/anthropic" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "medium", "high"] }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { effort: "low" },
|
||||||
|
medium: { effort: "medium" },
|
||||||
|
high: { effort: "high" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("github-copilot anthropic models filter unsupported data efforts", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "github-copilot/claude-sonnet-4-6",
|
||||||
|
providerID: "github-copilot",
|
||||||
|
api: { id: "claude-sonnet-4-6", url: "", npm: "@ai-sdk/anthropic" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "medium", "high", "max"] }],
|
||||||
|
)
|
||||||
|
expect(Object.keys(ProviderTransform.variants(model))).toEqual(["low", "medium", "high"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("openrouter encodes data efforts as reasoning.effort", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "openrouter/x-ai/grok-4.3",
|
||||||
|
providerID: "openrouter",
|
||||||
|
api: { id: "x-ai/grok-4.3", url: "", npm: "@openrouter/ai-sdk-provider" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "high"] }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { reasoning: { effort: "low" } },
|
||||||
|
high: { reasoning: { effort: "high" } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("google encodes data efforts as thinkingConfig levels", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "google/gemini-3-pro-preview",
|
||||||
|
providerID: "google",
|
||||||
|
api: { id: "gemini-3-pro-preview", url: "", npm: "@ai-sdk/google" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "high"] }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } },
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("bedrock non-adaptive models encode data efforts as enabled maxReasoningEffort", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "amazon-bedrock/anthropic.claude-opus-4-5-20251101-v1:0",
|
||||||
|
providerID: "amazon-bedrock",
|
||||||
|
api: { id: "anthropic.claude-opus-4-5-20251101-v1:0", url: "", npm: "@ai-sdk/amazon-bedrock" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "medium", "high"] }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||||
|
medium: { reasoningConfig: { type: "enabled", maxReasoningEffort: "medium" } },
|
||||||
|
high: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sap anthropic adaptive models wrap data efforts in modelParams", () => {
|
||||||
|
const model = withOptions(
|
||||||
|
createModel({
|
||||||
|
id: "sap-ai-core/anthropic--claude-4.6-sonnet",
|
||||||
|
providerID: "sap-ai-core",
|
||||||
|
api: { id: "anthropic--claude-4.6-sonnet", url: "", npm: "@jerome-benoit/sap-ai-provider-v2" },
|
||||||
|
}),
|
||||||
|
[{ type: "effort", values: ["low", "max"] }],
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { modelParams: { thinking: { type: "adaptive" }, output_config: { effort: "low" } } },
|
||||||
|
max: { modelParams: { thinking: { type: "adaptive" }, output_config: { effort: "max" } } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("unknown packages default to openai-compatible reasoningEffort", () => {
|
||||||
|
const model = withOptions(createModel({ api: { id: "some-model", url: "", npm: "@ai-sdk/some-future-sdk" } }), [
|
||||||
|
{ type: "effort", values: ["low", "ultrathink"] },
|
||||||
|
])
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
low: { reasoningEffort: "low" },
|
||||||
|
ultrathink: { reasoningEffort: "ultrathink" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
|
describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
|
||||||
const createModel = (apiId: string) => {
|
const createModel = (apiId: string) => {
|
||||||
const model = {
|
const model = {
|
||||||
|
|||||||
@@ -1816,6 +1816,20 @@ export type ProviderConfig = {
|
|||||||
release_date?: string
|
release_date?: string
|
||||||
attachment?: boolean
|
attachment?: boolean
|
||||||
reasoning?: boolean
|
reasoning?: boolean
|
||||||
|
reasoning_options?: Array<
|
||||||
|
| {
|
||||||
|
type: "toggle"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "effort"
|
||||||
|
values: Array<string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "budget_tokens"
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
>
|
||||||
temperature?: boolean
|
temperature?: boolean
|
||||||
tool_call?: boolean
|
tool_call?: boolean
|
||||||
interleaved?:
|
interleaved?:
|
||||||
@@ -2082,6 +2096,20 @@ export type Model = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: boolean
|
temperature: boolean
|
||||||
reasoning: boolean
|
reasoning: boolean
|
||||||
|
reasoningOptions?: Array<
|
||||||
|
| {
|
||||||
|
type: "toggle"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "effort"
|
||||||
|
values: Array<string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "budget_tokens"
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
>
|
||||||
attachment: boolean
|
attachment: boolean
|
||||||
toolcall: boolean
|
toolcall: boolean
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -17917,6 +17917,58 @@
|
|||||||
"reasoning": {
|
"reasoning": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"reasoning_options": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["toggle"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["effort"]
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "values"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["budget_tokens"]
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -18652,6 +18704,58 @@
|
|||||||
"reasoning": {
|
"reasoning": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"reasoningOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["toggle"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["effort"]
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "values"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["budget_tokens"]
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"attachment": {
|
"attachment": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user