mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-22 18:56:10 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef5212e09 | ||
|
|
5d3588962a | ||
|
|
3e9cbc9178 | ||
|
|
60493ab948 | ||
|
|
383fe107ed | ||
|
|
6ea601eb37 | ||
|
|
6413d66aa2 | ||
|
|
6e3c76e86e | ||
|
|
1cb7b81552 | ||
|
|
3f0f90e94e | ||
|
|
36ed0f4878 | ||
|
|
49a02e2b95 | ||
|
|
e9c243cb67 | ||
|
|
eb6efbccfd | ||
|
|
c7cbbda8f2 | ||
|
|
996a0628ef | ||
|
|
ed20d69d80 |
@@ -44,6 +44,32 @@ const Cost = Schema.Struct({
|
||||
),
|
||||
})
|
||||
|
||||
const ReasoningEffortValue = Schema.Union([Schema.Null, Schema.String])
|
||||
|
||||
export const KnownReasoningOption = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("effort"),
|
||||
values: Schema.Array(ReasoningEffortValue),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("toggle"),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("budget_tokens"),
|
||||
min: Schema.optional(Schema.Finite),
|
||||
max: Schema.optional(Schema.Finite),
|
||||
}),
|
||||
])
|
||||
export type KnownReasoningOption = typeof KnownReasoningOption.Type
|
||||
|
||||
export const ReasoningOption = Schema.Union([
|
||||
KnownReasoningOption,
|
||||
Schema.Struct({
|
||||
type: Schema.String,
|
||||
}),
|
||||
])
|
||||
export type ReasoningOption = typeof ReasoningOption.Type
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
@@ -51,6 +77,7 @@ export const Model = Schema.Struct({
|
||||
release_date: Schema.String,
|
||||
attachment: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
interleaved: Schema.optional(
|
||||
|
||||
@@ -1022,6 +1022,7 @@ export const Model = Schema.Struct({
|
||||
name: Schema.String,
|
||||
family: optional(Schema.String),
|
||||
capabilities: ProviderCapabilities,
|
||||
reasoning_options: optional(Schema.Array(ModelsDev.KnownReasoningOption)),
|
||||
cost: ProviderCost,
|
||||
limit: ProviderLimit,
|
||||
status: ModelStatus,
|
||||
@@ -1185,6 +1186,39 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||
return result
|
||||
}
|
||||
|
||||
type ReasoningOption = NonNullable<Model["reasoning_options"]>[number]
|
||||
|
||||
function reasoningOptions(model: ModelsDev.Model): Model["reasoning_options"] {
|
||||
if (!Array.isArray(model.reasoning_options)) return undefined
|
||||
const normalized = model.reasoning_options.flatMap((option) => {
|
||||
const normalized = normalizeReasoningOption(option)
|
||||
return normalized ? [normalized] : []
|
||||
})
|
||||
return normalized.length > 0 || model.reasoning_options.length === 0 ? normalized : undefined
|
||||
}
|
||||
|
||||
function normalizeReasoningOption(option: unknown): ReasoningOption | undefined {
|
||||
if (!isRecord(option) || typeof option.type !== "string") return undefined
|
||||
if (option.type === "effort") {
|
||||
if (!Array.isArray(option.values)) return undefined
|
||||
return {
|
||||
type: "effort",
|
||||
values: option.values.filter((value): value is string | null => value === null || typeof value === "string"),
|
||||
}
|
||||
}
|
||||
if (option.type === "toggle") return { type: "toggle" }
|
||||
if (option.type === "budget_tokens") {
|
||||
const min = typeof option.min === "number" && Number.isFinite(option.min) ? option.min : undefined
|
||||
const max = typeof option.max === "number" && Number.isFinite(option.max) ? option.max : undefined
|
||||
return {
|
||||
type: "budget_tokens",
|
||||
...(min === undefined ? {} : { min }),
|
||||
...(max === undefined ? {} : { max }),
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||
const base: Model = {
|
||||
id: ModelV2.ID.make(model.id),
|
||||
@@ -1226,6 +1260,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
||||
},
|
||||
interleaved: model.interleaved ?? false,
|
||||
},
|
||||
reasoning_options: reasoningOptions(model),
|
||||
release_date: model.release_date ?? "",
|
||||
variants: {},
|
||||
}
|
||||
@@ -1404,6 +1439,7 @@ const layer = Layer.effect(
|
||||
}
|
||||
|
||||
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
||||
const catalogModel = existing?.models[model.id ?? modelID]
|
||||
const existingModel = parsed.models[model.id ?? modelID]
|
||||
const apiID = model.id ?? existingModel?.api.id ?? modelID
|
||||
const apiNpm =
|
||||
@@ -1456,6 +1492,10 @@ const layer = Layer.effect(
|
||||
? { field: "reasoning_content" }
|
||||
: false),
|
||||
},
|
||||
// Config models don't define reasoning_options. Inherit them only
|
||||
// when the config model is explicitly based on a catalog model;
|
||||
// pure custom models keep undefined so legacy heuristics still run.
|
||||
reasoning_options: catalogModel?.reasoning_options,
|
||||
cost: {
|
||||
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
||||
output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { mergeDeep, unique } from "remeda"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import type * as Provider from "./provider"
|
||||
import type * as ModelsDev from "@opencode-ai/core/models-dev"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||
|
||||
@@ -517,134 +516,8 @@ export function topK(model: Provider.Model) {
|
||||
}
|
||||
|
||||
const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
|
||||
const ANTHROPIC_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh", "max"]
|
||||
const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
||||
const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
|
||||
const OPENAI_GPT5_PRO_EFFORTS = ["high"]
|
||||
const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
|
||||
const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
|
||||
const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
||||
const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
|
||||
|
||||
// OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
|
||||
// Models released before it 400 on `reasoning_effort: "none"`, so we only expose
|
||||
// it as a variant for models new enough to accept it.
|
||||
const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13"
|
||||
|
||||
// OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning.
|
||||
const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
|
||||
|
||||
// Matches members of the gpt-5 family across the id formats we encounter:
|
||||
// "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
|
||||
// Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
|
||||
const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
|
||||
const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
|
||||
const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
|
||||
const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
|
||||
|
||||
function gpt5Version(apiId: string) {
|
||||
return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
|
||||
}
|
||||
|
||||
function versionedGpt5ReasoningEfforts(apiId: string) {
|
||||
if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
|
||||
const version = gpt5Version(apiId)
|
||||
if (version === undefined) return undefined
|
||||
if (version === 1) return OPENAI_GPT5_1_EFFORTS
|
||||
return OPENAI_GPT5_2_PLUS_EFFORTS
|
||||
}
|
||||
|
||||
function gpt5CodexReasoningEfforts(apiId: string) {
|
||||
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
|
||||
const version = gpt5Version(apiId)
|
||||
if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
|
||||
if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
|
||||
return WIDELY_SUPPORTED_EFFORTS
|
||||
}
|
||||
|
||||
function gpt5ChatReasoningEfforts(apiId: string) {
|
||||
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
|
||||
return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
|
||||
}
|
||||
|
||||
// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
|
||||
// routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
|
||||
// to strongest.
|
||||
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
|
||||
const id = apiId.toLowerCase()
|
||||
if (id.includes("deep-research")) return ["medium"]
|
||||
const chatEfforts = gpt5ChatReasoningEfforts(id)
|
||||
if (chatEfforts) return chatEfforts
|
||||
if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
|
||||
const codexEfforts = gpt5CodexReasoningEfforts(id)
|
||||
if (codexEfforts) return codexEfforts
|
||||
const versionedEfforts = versionedGpt5ReasoningEfforts(id)
|
||||
// GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
|
||||
// additionally accepts `xhigh`. Model pages list the supported subset.
|
||||
if (versionedEfforts) return versionedEfforts
|
||||
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
|
||||
if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
|
||||
if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh")
|
||||
return efforts
|
||||
}
|
||||
|
||||
function openaiCompatibleReasoningEfforts(id: string) {
|
||||
const apiId = id.toLowerCase()
|
||||
const chatEfforts = gpt5ChatReasoningEfforts(apiId)
|
||||
if (chatEfforts) return chatEfforts
|
||||
if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_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 anthropicSonnet5OrLater(apiId: string) {
|
||||
const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId)
|
||||
if (!version) return false
|
||||
return Number(version[1] ?? version[2]) >= 5
|
||||
}
|
||||
|
||||
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
||||
if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(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) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")
|
||||
}
|
||||
|
||||
function googleThinkingLevelEfforts(apiId: string) {
|
||||
const id = apiId.toLowerCase()
|
||||
if (!id.includes("gemini-3")) return ["low", "high"]
|
||||
if (id.includes("flash-image")) return ["minimal", "high"]
|
||||
if (id.includes("pro-image")) return ["high"]
|
||||
if (id.includes("flash")) return ["minimal", "low", "medium", "high"]
|
||||
return ["low", "medium", "high"]
|
||||
}
|
||||
|
||||
function googleThinkingBudgetMax(apiId: string) {
|
||||
const id = apiId.toLowerCase()
|
||||
if (id.includes("2.5") && id.includes("pro") && !id.includes("flash")) return 32_768
|
||||
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.
|
||||
@@ -652,31 +525,298 @@ function wrapInSapModelParams(variants: Record<string, Record<string, any>>): Re
|
||||
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
||||
}
|
||||
|
||||
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
const id = model.api.id.toLowerCase()
|
||||
if (id.includes("2.5")) {
|
||||
return {
|
||||
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
max: {
|
||||
thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) },
|
||||
},
|
||||
}
|
||||
}
|
||||
function idIncludes(model: Provider.Model, value: string) {
|
||||
return model.id.toLowerCase().includes(value) || model.api.id.toLowerCase().includes(value)
|
||||
}
|
||||
|
||||
function anthropicAdaptiveVariants() {
|
||||
return Object.fromEntries(
|
||||
googleThinkingLevelEfforts(id).map((effort) => [
|
||||
ANTHROPIC_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinking: anthropicAdaptiveThinking(),
|
||||
effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function bedrockAnthropicAdaptiveVariants() {
|
||||
return Object.fromEntries(
|
||||
ANTHROPIC_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningConfig: {
|
||||
type: "adaptive",
|
||||
maxReasoningEffort: effort,
|
||||
display: "summarized",
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function sapAnthropicAdaptiveVariants() {
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(
|
||||
ANTHROPIC_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinking: anthropicAdaptiveThinking(),
|
||||
output_config: { effort },
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function googleThinkingLevelVariants() {
|
||||
return Object.fromEntries(
|
||||
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{ thinkingConfig: { includeThoughts: true, thinkingLevel: effort } },
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function openAIReasoningEffortVariants() {
|
||||
return Object.fromEntries(
|
||||
OPENAI_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function openAICompatibleReasoningEffortVariants() {
|
||||
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
}
|
||||
|
||||
type ReasoningOption = NonNullable<Provider.Model["reasoning_options"]>[number]
|
||||
type ReasoningEffortOption = Extract<ReasoningOption, { type: "effort" }>
|
||||
type ReasoningBudgetOption = Extract<ReasoningOption, { type: "budget_tokens" }>
|
||||
type ReasoningEffortValue = ReasoningEffortOption["values"][number]
|
||||
|
||||
function reasoningOption<T extends ReasoningOption["type"]>(
|
||||
model: Provider.Model,
|
||||
type: T,
|
||||
): Extract<ReasoningOption, { type: T }> | undefined {
|
||||
return model.reasoning_options?.find((option): option is Extract<ReasoningOption, { type: T }> => option.type === type)
|
||||
}
|
||||
|
||||
function reasoningBudgetVariants(
|
||||
budget: ReasoningBudgetOption,
|
||||
make: (budgetTokens: number) => Record<string, any>,
|
||||
model: Provider.Model,
|
||||
) {
|
||||
const max = Math.max(1, Math.min(budget.max ?? 31_999, maxOutputTokens(model) - 1))
|
||||
const min = Math.min(budget.min ?? 1, max)
|
||||
const high = Math.max(min, max > 16_000 ? 16_000 : Math.ceil(max / 2))
|
||||
return {
|
||||
high: make(high),
|
||||
max: make(max),
|
||||
}
|
||||
}
|
||||
|
||||
function anthropicAdaptiveFromOptions(effort: ReasoningEffortOption) {
|
||||
return effort.values.includes("max")
|
||||
}
|
||||
|
||||
function anthropicAdaptiveThinking() {
|
||||
return {
|
||||
type: "adaptive",
|
||||
display: "summarized",
|
||||
}
|
||||
}
|
||||
|
||||
function effortVariant(model: Provider.Model, effort: ReasoningEffortValue) {
|
||||
switch (model.api.npm) {
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return { reasoning: { effort } }
|
||||
|
||||
case "ai-gateway-provider":
|
||||
return { reasoningEffort: effort }
|
||||
|
||||
case "@ai-sdk/gateway":
|
||||
if (idIncludes(model, "anthropic")) {
|
||||
const option = reasoningOption(model, "effort")
|
||||
if (option && anthropicAdaptiveFromOptions(option)) {
|
||||
return {
|
||||
thinking: anthropicAdaptiveThinking(),
|
||||
effort,
|
||||
}
|
||||
}
|
||||
return { effort }
|
||||
}
|
||||
if (idIncludes(model, "google")) return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||
return { reasoningEffort: effort }
|
||||
|
||||
case "@ai-sdk/anthropic":
|
||||
case "@ai-sdk/google-vertex/anthropic": {
|
||||
const option = reasoningOption(model, "effort")
|
||||
if (option && anthropicAdaptiveFromOptions(option)) {
|
||||
return {
|
||||
thinking: anthropicAdaptiveThinking(),
|
||||
effort,
|
||||
}
|
||||
}
|
||||
return { effort }
|
||||
}
|
||||
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
const option = reasoningOption(model, "effort")
|
||||
const adaptive = option ? anthropicAdaptiveFromOptions(option) : false
|
||||
return {
|
||||
reasoningConfig: {
|
||||
type: adaptive ? "adaptive" : "enabled",
|
||||
maxReasoningEffort: effort,
|
||||
...(adaptive ? { display: "summarized" } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
||||
|
||||
case "@ai-sdk/google":
|
||||
case "@ai-sdk/google-vertex":
|
||||
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||
|
||||
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||
if (!model.api.id.includes("anthropic")) return { modelParams: { reasoning_effort: effort } }
|
||||
const option = reasoningOption(model, "effort")
|
||||
const modelParams =
|
||||
option && anthropicAdaptiveFromOptions(option)
|
||||
? {
|
||||
thinking: anthropicAdaptiveThinking(),
|
||||
output_config: { effort },
|
||||
}
|
||||
: { output_config: { effort } }
|
||||
return { modelParams }
|
||||
}
|
||||
|
||||
case "@ai-sdk/azure":
|
||||
return {
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
}
|
||||
|
||||
case "@ai-sdk/amazon-bedrock/mantle":
|
||||
case "@ai-sdk/openai":
|
||||
return {
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
}
|
||||
|
||||
case "@ai-sdk/github-copilot":
|
||||
return model.id.includes("claude")
|
||||
? { reasoningEffort: effort }
|
||||
: {
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
}
|
||||
|
||||
case "@ai-sdk/cerebras":
|
||||
case "@ai-sdk/togetherai":
|
||||
case "@ai-sdk/xai":
|
||||
case "@ai-sdk/deepinfra":
|
||||
case "venice-ai-sdk-provider":
|
||||
case "@ai-sdk/openai-compatible":
|
||||
case "@ai-sdk/groq":
|
||||
case "@ai-sdk/mistral":
|
||||
return { reasoningEffort: effort }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function reasoningOptionVariants(model: Provider.Model): Record<string, Record<string, any>> | undefined {
|
||||
if (!model.reasoning_options) return undefined
|
||||
const effort = reasoningOption(model, "effort")
|
||||
const budget = reasoningOption(model, "budget_tokens")
|
||||
|
||||
if (effort) {
|
||||
return Object.fromEntries(
|
||||
effort.values.flatMap((value): [string, Record<string, any>][] => {
|
||||
// A null effort is a provider-specific escape hatch in models.dev. Skip
|
||||
// exposing it as a selectable variant until variant IDs can represent it cleanly.
|
||||
if (value === null) return []
|
||||
const variant = effortVariant(model, value)
|
||||
return variant ? [[value, variant]] : []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Toggle-only support needs a product decision about how to expose an on/off
|
||||
// variant without clobbering users' current effort selection. Existing
|
||||
// hand-authored toggle variants, like MiniMax M3 below, remain preserved.
|
||||
if (!budget) return {}
|
||||
|
||||
switch (model.api.npm) {
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return reasoningBudgetVariants(budget, (max_tokens) => ({ reasoning: { max_tokens } }), model)
|
||||
|
||||
case "@ai-sdk/gateway":
|
||||
if (idIncludes(model, "anthropic")) {
|
||||
return reasoningBudgetVariants(
|
||||
budget,
|
||||
(budgetTokens) => ({ thinking: { type: "enabled", budgetTokens } }),
|
||||
model,
|
||||
)
|
||||
}
|
||||
if (idIncludes(model, "google")) {
|
||||
return reasoningBudgetVariants(
|
||||
budget,
|
||||
(thinkingBudget) => ({ thinkingConfig: { includeThoughts: true, thinkingBudget } }),
|
||||
model,
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case "@ai-sdk/anthropic":
|
||||
case "@ai-sdk/google-vertex/anthropic":
|
||||
return reasoningBudgetVariants(budget, (budgetTokens) => ({ thinking: { type: "enabled", budgetTokens } }), model)
|
||||
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
return reasoningBudgetVariants(
|
||||
budget,
|
||||
(budgetTokens) => ({ reasoningConfig: { type: "enabled", budgetTokens } }),
|
||||
model,
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case "@ai-sdk/google":
|
||||
case "@ai-sdk/google-vertex":
|
||||
return reasoningBudgetVariants(
|
||||
budget,
|
||||
(thinkingBudget) => ({ thinkingConfig: { includeThoughts: true, thinkingBudget } }),
|
||||
model,
|
||||
)
|
||||
|
||||
case "@jerome-benoit/sap-ai-provider-v2":
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
return wrapInSapModelParams(
|
||||
reasoningBudgetVariants(budget, (budget_tokens) => ({ thinking: { type: "enabled", budget_tokens } }), model),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
|
||||
const id = model.id.toLowerCase()
|
||||
const glm52 = ["glm-5.2", "glm-5-2", "glm-5p2"].some(
|
||||
(name) => id.includes(name) || model.api.id.toLowerCase().includes(name),
|
||||
)
|
||||
// Historical exception: MiniMax M3's Anthropic-compatible surface exposes an
|
||||
// explicit thinking toggle that predates models.dev reasoning_options.
|
||||
if (
|
||||
model.api.id.toLowerCase().includes("minimax-m3") &&
|
||||
["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"].includes(model.api.npm)
|
||||
@@ -686,161 +826,27 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
}
|
||||
}
|
||||
const adaptiveThinkingOmitted = anthropicOmitsThinking(model.api.id)
|
||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
||||
if (glm52 && model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
// OpenRouter maps xhigh to GLM-5.2's native max effort.
|
||||
return {
|
||||
high: { reasoning: { effort: "high" } },
|
||||
xhigh: { reasoning: { effort: "xhigh" } },
|
||||
}
|
||||
}
|
||||
if (glm52 && model.api.npm === "@ai-sdk/openai-compatible") {
|
||||
return {
|
||||
high: { reasoningEffort: "high" },
|
||||
max: { reasoningEffort: "max" },
|
||||
}
|
||||
}
|
||||
if (glm52 && model.api.npm === "@ai-sdk/anthropic") {
|
||||
return {
|
||||
high: { effort: "high" },
|
||||
max: { effort: "max" },
|
||||
}
|
||||
}
|
||||
if (
|
||||
id.includes("deepseek-chat") ||
|
||||
id.includes("deepseek-reasoner") ||
|
||||
id.includes("deepseek-r1") ||
|
||||
id.includes("deepseek-v3") ||
|
||||
id.includes("minimax") ||
|
||||
(id.includes("glm") && !glm52) ||
|
||||
id.includes("kimi") ||
|
||||
id.includes("k2p") ||
|
||||
id.includes("qwen") ||
|
||||
id.includes("big-pickle")
|
||||
)
|
||||
return {}
|
||||
|
||||
// see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
|
||||
if (id.includes("grok") && id.includes("grok-3-mini")) {
|
||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
return {
|
||||
low: { reasoning: { effort: "low" } },
|
||||
high: { reasoning: { effort: "high" } },
|
||||
}
|
||||
}
|
||||
return {
|
||||
low: { reasoningEffort: "low" },
|
||||
high: { reasoningEffort: "high" },
|
||||
}
|
||||
}
|
||||
if (id.includes("grok")) return {}
|
||||
const fromReasoningOptions = reasoningOptionVariants(model)
|
||||
if (fromReasoningOptions) return fromReasoningOptions
|
||||
|
||||
switch (model.api.npm) {
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return Object.fromEntries(
|
||||
(model.api.id.startsWith("openai/") || id.includes("gpt")
|
||||
? openaiCompatibleReasoningEfforts(model.api.id)
|
||||
: WIDELY_SUPPORTED_EFFORTS
|
||||
).map((effort) => [effort, { reasoning: { effort } }]),
|
||||
)
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
|
||||
|
||||
case "ai-gateway-provider": {
|
||||
// Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
|
||||
// /v1/compat endpoint, so the body is always OAI-shaped. The gateway
|
||||
// translates `reasoning_effort` to the upstream provider's native control
|
||||
// (e.g. Anthropic thinking budgets) when needed. Variants therefore stay
|
||||
// OAI-style for all upstreams, with an extended effort set for OpenAI
|
||||
// models that support it.
|
||||
if (model.api.id.startsWith("openai/")) {
|
||||
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
||||
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
}
|
||||
case "ai-gateway-provider":
|
||||
if (idIncludes(model, "openai")) return openAICompatibleReasoningEffortVariants()
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
}
|
||||
|
||||
case "@ai-sdk/gateway":
|
||||
if (model.id.includes("anthropic")) {
|
||||
if (adaptiveEfforts) {
|
||||
return Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
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").
|
||||
...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
|
||||
},
|
||||
effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
return {
|
||||
high: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 31999,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if (model.id.includes("google")) {
|
||||
if (id.includes("2.5")) {
|
||||
return {
|
||||
high: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: googleThinkingBudgetMax(id),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
["low", "high"].map((effort) => [
|
||||
effort,
|
||||
{
|
||||
includeThoughts: true,
|
||||
thinkingLevel: effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
return Object.fromEntries(
|
||||
openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
|
||||
)
|
||||
if (idIncludes(model, "anthropic")) return anthropicAdaptiveVariants()
|
||||
if (idIncludes(model, "google")) return googleThinkingLevelVariants()
|
||||
if (idIncludes(model, "openai")) return openAIReasoningEffortVariants()
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
case "@ai-sdk/github-copilot":
|
||||
if (model.id.includes("gemini")) {
|
||||
// currently github copilot only returns thinking
|
||||
return {}
|
||||
}
|
||||
if (model.id.includes("claude")) {
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
}
|
||||
const copilotEfforts = iife(() => {
|
||||
if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
|
||||
return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
||||
const arr = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
|
||||
return arr
|
||||
})
|
||||
return Object.fromEntries(
|
||||
copilotEfforts.map((effort) => [
|
||||
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
@@ -861,125 +867,26 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
case "venice-ai-sdk-provider":
|
||||
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
||||
case "@ai-sdk/openai-compatible":
|
||||
if (model.api.id.toLowerCase().includes("north-mini-code")) {
|
||||
return Object.fromEntries(["none", "high"].map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
}
|
||||
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.api.id.toLowerCase().includes("deepseek-v4")) {
|
||||
efforts.push("max")
|
||||
}
|
||||
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
case "@ai-sdk/azure":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
||||
if (id === "o1-mini") return {}
|
||||
return Object.fromEntries(
|
||||
openaiReasoningEfforts(id, model.release_date).map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
},
|
||||
]),
|
||||
)
|
||||
return openAIReasoningEffortVariants()
|
||||
case "@ai-sdk/amazon-bedrock/mantle":
|
||||
case "@ai-sdk/openai": {
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
||||
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
||||
return Object.fromEntries(
|
||||
efforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: INCLUDE_ENCRYPTED_REASONING,
|
||||
},
|
||||
]),
|
||||
)
|
||||
return openAIReasoningEffortVariants()
|
||||
}
|
||||
|
||||
case "@ai-sdk/anthropic":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
|
||||
case "@ai-sdk/google-vertex/anthropic":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
|
||||
if (adaptiveEfforts) {
|
||||
let efforts = [...adaptiveEfforts]
|
||||
if (model.providerID === "github-copilot") {
|
||||
if (model.api.id.includes("opus-4.7")) {
|
||||
efforts = ["medium"]
|
||||
}
|
||||
// Efforts currently supported are: low, medium, high
|
||||
efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
|
||||
}
|
||||
return Object.fromEntries(
|
||||
efforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
|
||||
},
|
||||
effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
|
||||
}
|
||||
|
||||
return {
|
||||
high: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: Math.min(31_999, model.limit.output - 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
return anthropicAdaptiveVariants()
|
||||
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
||||
if (adaptiveEfforts) {
|
||||
return Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningConfig: {
|
||||
type: "adaptive",
|
||||
maxReasoningEffort: effort,
|
||||
...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
// For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
return {
|
||||
high: {
|
||||
reasoningConfig: {
|
||||
type: "enabled",
|
||||
budgetTokens: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
reasoningConfig: {
|
||||
type: "enabled",
|
||||
budgetTokens: 31999,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// For Amazon Nova models, use reasoningConfig with maxReasoningEffort
|
||||
if (idIncludes(model, "anthropic")) return bedrockAnthropicAdaptiveVariants()
|
||||
return Object.fromEntries(
|
||||
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
@@ -996,21 +903,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
||||
case "@ai-sdk/google":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
||||
return googleThinkingVariants(model)
|
||||
return googleThinkingLevelVariants()
|
||||
|
||||
case "@ai-sdk/mistral":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
||||
// https://docs.mistral.ai/capabilities/reasoning/adjustable
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
// Only Mistral Small 4 and Medium 3.5 support reasoning
|
||||
const MISTRAL_REASONING_IDS = [
|
||||
"mistral-small-2603",
|
||||
"mistral-small-latest",
|
||||
"mistral-medium-3.5",
|
||||
"mistral-medium-2604",
|
||||
]
|
||||
const mistralId = model.api.id.toLowerCase()
|
||||
if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
|
||||
return {
|
||||
high: { reasoningEffort: "high" },
|
||||
}
|
||||
@@ -1036,33 +933,12 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
return {}
|
||||
|
||||
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||
if (id.includes("anthropic")) {
|
||||
if (adaptiveEfforts) {
|
||||
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
||||
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinking: { type: "adaptive", ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}) },
|
||||
output_config: { effort },
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
return wrapInSapModelParams({
|
||||
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
||||
})
|
||||
}
|
||||
if (id.includes("gemini") && id.includes("2.5")) {
|
||||
return wrapInSapModelParams(googleThinkingVariants(model))
|
||||
}
|
||||
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
|
||||
const efforts = openaiReasoningEfforts(id, model.release_date)
|
||||
return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])))
|
||||
if (idIncludes(model, "anthropic")) return sapAnthropicAdaptiveVariants()
|
||||
if (idIncludes(model, "google")) return wrapInSapModelParams(googleThinkingLevelVariants())
|
||||
if (idIncludes(model, "openai")) {
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning_effort: effort }])),
|
||||
)
|
||||
}
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])),
|
||||
|
||||
@@ -58,4 +58,24 @@ describe("provider model status schemas", () => {
|
||||
}).status,
|
||||
).toBe("active")
|
||||
})
|
||||
|
||||
test("accepts future models.dev reasoning options without rejecting known structure", () => {
|
||||
expect(() =>
|
||||
Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
release_date: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [
|
||||
{ type: "effort", values: ["turbo"] },
|
||||
{ type: "toggle", modes: ["auto", "off"] },
|
||||
{ type: "new_control", values: ["turbo"] },
|
||||
],
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
limit: { context: 128000, output: 8192 },
|
||||
}),
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1426,6 +1426,69 @@ test("models.dev normalization fills required response fields", () => {
|
||||
expect(model.release_date).toBe("")
|
||||
})
|
||||
|
||||
test("models.dev normalization preserves reasoning options for variant generation", () => {
|
||||
const provider = {
|
||||
id: "custom",
|
||||
name: "Custom",
|
||||
env: [],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
reasoner: {
|
||||
id: "reasoner",
|
||||
name: "Reasoner",
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["high", "xhigh"] }],
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 128_000, output: 32_000 },
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDev.Provider
|
||||
|
||||
const model = Provider.fromModelsDevProvider(provider).models.reasoner
|
||||
expect(model.reasoning_options).toEqual([{ type: "effort", values: ["high", "xhigh"] }])
|
||||
expect(Object.keys(model.variants!)).toEqual(["high", "xhigh"])
|
||||
expect(model.variants!.xhigh).toEqual({ reasoningEffort: "xhigh" })
|
||||
})
|
||||
|
||||
test("models.dev normalization ignores unknown reasoning options and passes new effort strings", () => {
|
||||
const provider = {
|
||||
id: "custom",
|
||||
name: "Custom",
|
||||
env: [],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
reasoner: {
|
||||
id: "reasoner",
|
||||
name: "Reasoner",
|
||||
reasoning: true,
|
||||
reasoning_options: [
|
||||
{ type: "future_control", values: ["turbo"] },
|
||||
{ type: "effort", values: ["turbo", null] },
|
||||
{ type: "budget_tokens", max: 16_000 },
|
||||
],
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 128_000, output: 32_000 },
|
||||
},
|
||||
future: {
|
||||
id: "future",
|
||||
name: "Future",
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "future_control", values: ["turbo"] }],
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 128_000, output: 32_000 },
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDev.Provider
|
||||
|
||||
const models = Provider.fromModelsDevProvider(provider).models
|
||||
expect(models.reasoner.reasoning_options).toEqual([
|
||||
{ type: "effort", values: ["turbo", null] },
|
||||
{ type: "budget_tokens", max: 16_000 },
|
||||
])
|
||||
expect(models.reasoner.variants).toEqual({ turbo: { reasoningEffort: "turbo" } })
|
||||
expect(models.future.reasoning_options).toBeUndefined()
|
||||
})
|
||||
|
||||
it.instance("model variants are generated for reasoning models", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
@@ -1524,7 +1587,13 @@ it.instance(
|
||||
anthropic: {
|
||||
models: {
|
||||
"claude-sonnet-4-20250514": {
|
||||
variants: { high: { disabled: true }, max: { disabled: true } },
|
||||
variants: {
|
||||
low: { disabled: true },
|
||||
medium: { disabled: true },
|
||||
high: { disabled: true },
|
||||
xhigh: { disabled: true },
|
||||
max: { disabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1575,6 +1644,39 @@ it.instance(
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"custom config reasoning model without variants uses fallback variants",
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* list
|
||||
const model = providers[ProviderV2.ID.make("custom-reasoning-fallback")].models["reasoning-model"]
|
||||
expect(model.reasoning_options).toBeUndefined()
|
||||
expect(model.variants).toEqual({
|
||||
low: { reasoningEffort: "low" },
|
||||
medium: { reasoningEffort: "medium" },
|
||||
high: { reasoningEffort: "high" },
|
||||
})
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
"custom-reasoning-fallback": {
|
||||
name: "Custom Reasoning Fallback",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
env: [],
|
||||
models: {
|
||||
"reasoning-model": {
|
||||
name: "Reasoning Model",
|
||||
reasoning: true,
|
||||
limit: { context: 128000, output: 16000 },
|
||||
},
|
||||
},
|
||||
options: { apiKey: "test-key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"custom model with variants enabled and disabled",
|
||||
Effect.gen(function* () {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+180183
-111593
File diff suppressed because it is too large
Load Diff
@@ -316,9 +316,7 @@ describe("tool.read truncation", () => {
|
||||
it.instance("truncates large file by bytes and sets truncated metadata", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const base = yield* load(path.join(FIXTURES_DIR, "models-api.json"))
|
||||
const target = 60 * 1024
|
||||
const content = base.length >= target ? base : base.repeat(Math.ceil(target / base.length))
|
||||
const content = `${"x".repeat(80)}\n`.repeat(1_000)
|
||||
yield* put(path.join(test.directory, "large.json"), content)
|
||||
|
||||
const result = yield* run({ filePath: path.join(test.directory, "large.json") })
|
||||
|
||||
@@ -163,8 +163,7 @@ describe("Truncate", () => {
|
||||
it.live("large single-line file truncates with byte message", () =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Truncate.Service
|
||||
const fsys = yield* FSUtil.Service
|
||||
const content = yield* fsys.readFileString(path.join(FIXTURES_DIR, "models-api.json"))
|
||||
const content = "a".repeat(Truncate.MAX_BYTES + 1)
|
||||
const result = yield* svc.output(content)
|
||||
|
||||
expect(result.truncated).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user