mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 18:26:09 +00:00
fix(core): expand reasoning option variants (#36894)
This commit is contained in:
+43
-11
@@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||
}
|
||||
|
||||
function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||
const projected = mapBodyToProviderOptions(model)
|
||||
const projected = mapBodyToProviderOptions(model, pkg)
|
||||
const options: Record<string, any> = {
|
||||
name: model.providerID,
|
||||
...(model.settings ?? {}),
|
||||
@@ -265,7 +265,7 @@ export const locationLayer = Layer.effect(
|
||||
cause: new Error(`Unsupported package ${model.package}`),
|
||||
})
|
||||
|
||||
const packageName = ProviderV2.packageName(model.package) ?? ""
|
||||
const packageName = ProviderV2.packageName(model.package)
|
||||
const options = prepareOptions(model, packageName)
|
||||
const sdkKey = cacheKey({
|
||||
providerID: model.providerID,
|
||||
@@ -301,11 +301,17 @@ export const locationLayer = Layer.effect(
|
||||
export const defaultLayer = locationLayer
|
||||
|
||||
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
||||
const packageName = ProviderV2.packageName(info.package)
|
||||
const projected = mapBodyToProviderOptions(info)
|
||||
const packageName = ProviderV2.packageName(info.package!)
|
||||
const projected = mapBodyToProviderOptions(info, packageName)
|
||||
const optionKey = providerOptionKey(packageName, info.providerID)
|
||||
const providerOptions = (() => {
|
||||
if (projected.settings === undefined) return
|
||||
if (packageName === "@ai-sdk/gateway") return gatewayProviderOptions(info.modelID ?? info.id, projected.settings)
|
||||
if (packageName === "@ai-sdk/azure") return { openai: projected.settings, azure: projected.settings }
|
||||
return { [optionKey]: projected.settings }
|
||||
})()
|
||||
const route: AnyRoute = {
|
||||
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
|
||||
id: `ai-sdk:${packageName}`,
|
||||
provider: ProviderID.make(info.providerID),
|
||||
providerMetadataKey: optionKey,
|
||||
protocol: "ai-sdk",
|
||||
@@ -326,7 +332,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
||||
headers: info.headers,
|
||||
},
|
||||
limits: { context: info.limit.context, output: info.limit.output },
|
||||
providerOptions: projected.settings === undefined ? undefined : { [optionKey]: projected.settings },
|
||||
providerOptions,
|
||||
},
|
||||
body: {
|
||||
schema: Schema.Unknown,
|
||||
@@ -340,13 +346,35 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
||||
return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
|
||||
}
|
||||
|
||||
function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
|
||||
const gateway =
|
||||
typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
|
||||
? Object.fromEntries(Object.entries(settings.gateway))
|
||||
: undefined
|
||||
const model = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== "gateway"))
|
||||
if (Object.keys(model).length === 0) return gateway === undefined ? undefined : { gateway }
|
||||
|
||||
const separator = modelID.indexOf("/")
|
||||
const prefix = separator > 0 ? modelID.slice(0, separator) : undefined
|
||||
if (prefix)
|
||||
return { ...(gateway === undefined ? {} : { gateway }), [prefix === "amazon" ? "bedrock" : prefix]: model }
|
||||
if (typeof gateway === "object" && gateway !== null && !Array.isArray(gateway))
|
||||
return { gateway: { ...gateway, ...model } }
|
||||
return { gateway: model }
|
||||
}
|
||||
|
||||
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
|
||||
if (packageName === "@ai-sdk/google") return "google"
|
||||
if (packageName === "@ai-sdk/google-vertex") return "vertex"
|
||||
if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
|
||||
if (packageName === "@ai-sdk/amazon-bedrock" || packageName === "@ai-sdk/amazon-bedrock/mantle") return "bedrock"
|
||||
if (packageName === "@ai-sdk/amazon-bedrock") return "bedrock"
|
||||
if (packageName === "@ai-sdk/amazon-bedrock/mantle") return "openai"
|
||||
if (packageName === "@ai-sdk/azure") return "azure"
|
||||
if (packageName === "@ai-sdk/github-copilot") return "copilot"
|
||||
if (packageName === "@jerome-benoit/sap-ai-provider-v2") return "sap-ai"
|
||||
if (packageName === "@ai-sdk/openai-compatible") return providerID.split(".")[0]
|
||||
if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
|
||||
if (packageName === "ai-gateway-provider") return "openaiCompatible"
|
||||
if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
|
||||
return providerID
|
||||
}
|
||||
@@ -361,14 +389,18 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
|
||||
return Object.keys(result).length === 0 ? undefined : result
|
||||
}
|
||||
|
||||
function mapBodyToProviderOptions(model: ModelV2.Info) {
|
||||
function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
|
||||
const settings = requestSettings(model.settings)
|
||||
if (!Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning))
|
||||
return { settings, body: model.body }
|
||||
const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
|
||||
const forceReasoning =
|
||||
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
|
||||
(pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
|
||||
const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
|
||||
if (!pro) return { settings: normalized, body: model.body }
|
||||
const body = { ...model.body }
|
||||
delete body.reasoning
|
||||
return {
|
||||
settings: ProviderV2.mergeOverlay(settings, { reasoningMode: "pro" }),
|
||||
settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
|
||||
body: Object.keys(body).length === 0 ? undefined : body,
|
||||
}
|
||||
}
|
||||
|
||||
+263
-27
@@ -56,7 +56,10 @@ type SourceModel = {
|
||||
string,
|
||||
{
|
||||
readonly cost?: Cost
|
||||
readonly provider?: { readonly body?: ProviderV2.Settings; readonly headers?: Readonly<Record<string, string>> }
|
||||
readonly provider?: {
|
||||
readonly body?: ProviderV2.Settings
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
}
|
||||
}
|
||||
>
|
||||
>
|
||||
@@ -70,7 +73,7 @@ type SourceProvider = {
|
||||
readonly name: string
|
||||
readonly env: readonly string[]
|
||||
readonly id: string
|
||||
readonly npm?: string
|
||||
readonly npm: string
|
||||
readonly models: Readonly<Record<string, SourceModel>>
|
||||
}
|
||||
|
||||
@@ -87,7 +90,7 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
|
||||
const info = {
|
||||
id: providerID,
|
||||
name: item.name,
|
||||
package: item.npm ? ProviderV2.aisdk(item.npm) : "",
|
||||
package: ProviderV2.aisdk(item.npm),
|
||||
...(item.api ? { settings: { baseURL: item.api } } : {}),
|
||||
} satisfies ProviderV2.Info
|
||||
const models: ModelV2.Info[] = []
|
||||
@@ -185,61 +188,294 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u
|
||||
}
|
||||
|
||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
const OUTPUT_TOKEN_MAX = 32_000
|
||||
|
||||
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<ModelV2.Info["variants"]> {
|
||||
const npm = model.provider?.npm ?? provider.npm
|
||||
const options = model.reasoning_options ?? []
|
||||
const options = model.reasoning_options
|
||||
if (!options?.length) return []
|
||||
const toggle = options.some((option) => option.type === "toggle")
|
||||
const effort = options.find((option) => option.type === "effort")
|
||||
if (effort?.type === "effort") {
|
||||
return effort.values.flatMap((value) => {
|
||||
const raw: unknown = value
|
||||
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
|
||||
if (id === undefined) return []
|
||||
const settings = settingsForEffort(npm, id)
|
||||
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
|
||||
})
|
||||
const off = toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []
|
||||
const variants = [
|
||||
...off,
|
||||
...effort.values.flatMap((value) => {
|
||||
const raw: unknown = value
|
||||
const id = typeof raw === "string" && raw !== "null" ? raw : undefined
|
||||
if (id === undefined) return []
|
||||
if (id === "none" && off.length > 0) return []
|
||||
const settings = settingsForEffort(npm, model.id, id)
|
||||
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
|
||||
}),
|
||||
]
|
||||
return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
|
||||
}
|
||||
const budget = options.find((option) => option.type === "budget_tokens")
|
||||
if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
|
||||
if (budget?.type === "budget_tokens")
|
||||
return [
|
||||
...(toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []),
|
||||
...budgetVariants(npm, model, budget),
|
||||
]
|
||||
if (toggle) return toggleVariants(npm, model.id)
|
||||
return []
|
||||
}
|
||||
|
||||
function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
|
||||
function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined {
|
||||
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
|
||||
return { thinking: { type: "adaptive", display: "summarized" }, effort }
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
|
||||
if (anthropicManualThinking(modelID)) return { effort }
|
||||
return {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort,
|
||||
}
|
||||
}
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
|
||||
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||
if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
|
||||
if (npm === "@ai-sdk/openai")
|
||||
if (npm === "@ai-sdk/amazon-bedrock") {
|
||||
if (modelID.includes("anthropic"))
|
||||
return {
|
||||
reasoningConfig: {
|
||||
...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
|
||||
maxReasoningEffort: effort,
|
||||
},
|
||||
}
|
||||
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
||||
}
|
||||
if (npm === "@ai-sdk/gateway") {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
if (upstream) return settingsForEffort(upstream, modelID, effort)
|
||||
return { reasoningEffort: effort }
|
||||
}
|
||||
if (npm === "@ai-sdk/github-copilot") {
|
||||
if (modelID.includes("gemini")) return
|
||||
if (modelID.includes("claude")) return { reasoningEffort: effort }
|
||||
return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
|
||||
if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
|
||||
}
|
||||
if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/amazon-bedrock/mantle" || npm === "@ai-sdk/azure")
|
||||
return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
|
||||
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
|
||||
if (modelID.includes("anthropic"))
|
||||
return {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
|
||||
output_config: { effort },
|
||||
},
|
||||
},
|
||||
}
|
||||
if (modelID.includes("gemini"))
|
||||
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } }
|
||||
if (modelID.includes("amazon--nova"))
|
||||
return { modelParams: { additionalModelRequestFields: { output_config: { effort } } } }
|
||||
return { modelParams: { reasoning_effort: effort } }
|
||||
}
|
||||
if (
|
||||
[
|
||||
"@ai-sdk/openai-compatible",
|
||||
"@ai-sdk/xai",
|
||||
"@ai-sdk/mistral",
|
||||
"@ai-sdk/groq",
|
||||
"@ai-sdk/cerebras",
|
||||
"@ai-sdk/deepinfra",
|
||||
"@ai-sdk/togetherai",
|
||||
"venice-ai-sdk-provider",
|
||||
"ai-gateway-provider",
|
||||
].includes(npm)
|
||||
)
|
||||
return { reasoningEffort: effort }
|
||||
}
|
||||
|
||||
function budgetVariants(
|
||||
npm: string | undefined,
|
||||
npm: string,
|
||||
model: SourceModel,
|
||||
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||
): NonNullable<ModelV2.Info["variants"]> {
|
||||
const max = option.max
|
||||
const high =
|
||||
option.max === undefined
|
||||
? Math.max(option.min ?? 0, 16_000)
|
||||
: Math.min(Math.max(option.min ?? 0, 16_000), option.max)
|
||||
const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
|
||||
if (maximum <= 0) return []
|
||||
const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
|
||||
return [
|
||||
{ id: "high", budget: high },
|
||||
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
|
||||
{ id: "max", budget: maximum },
|
||||
].flatMap((item) => {
|
||||
const settings = settingsForBudget(npm, item.budget)
|
||||
const settings = settingsForBudget(npm, model.id, item.budget)
|
||||
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
|
||||
})
|
||||
}
|
||||
|
||||
function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
|
||||
function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info["variants"]> {
|
||||
if (npm === "@ai-sdk/gateway") {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
if (upstream) return toggleVariants(upstream, modelID)
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { reasoning: { enabled: false } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { reasoning: { enabled: true } },
|
||||
},
|
||||
]
|
||||
}
|
||||
if (npm === "@openrouter/ai-sdk-provider")
|
||||
return [
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
|
||||
]
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
|
||||
return [
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
},
|
||||
]
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
|
||||
},
|
||||
]
|
||||
if (npm === "@ai-sdk/amazon-bedrock") {
|
||||
const anthropic = modelID.includes("anthropic")
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: {
|
||||
additionalModelRequestFields: anthropic
|
||||
? { thinking: { type: "disabled" } }
|
||||
: { reasoningConfig: { type: "disabled" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: {
|
||||
additionalModelRequestFields: anthropic
|
||||
? { thinking: { type: "adaptive", display: "summarized" } }
|
||||
: { reasoningConfig: { type: "enabled" } },
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (npm === "@ai-sdk/alibaba")
|
||||
return [
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
|
||||
]
|
||||
if (npm === "@ai-sdk/cohere")
|
||||
return [
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
|
||||
]
|
||||
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
|
||||
if (modelID.includes("gemini"))
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
|
||||
},
|
||||
]
|
||||
if (modelID.includes("cohere"))
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { modelParams: { thinking: { type: "enabled" } } },
|
||||
},
|
||||
]
|
||||
if (modelID.includes("amazon--nova"))
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
|
||||
},
|
||||
]
|
||||
if (modelID.includes("anthropic"))
|
||||
return [
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function settingsForBudget(npm: string, modelID: string, budget: number): ProviderV2.Settings | undefined {
|
||||
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
|
||||
return { thinking: { type: "enabled", budgetTokens: budget } }
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
|
||||
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
|
||||
if (npm === "@ai-sdk/amazon-bedrock") return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
|
||||
if (npm === "@ai-sdk/gateway") {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
return upstream ? settingsForBudget(upstream, modelID, budget) : { reasoning: { max_tokens: budget } }
|
||||
}
|
||||
if (npm === "@ai-sdk/cohere") return { thinking: { type: "enabled", tokenBudget: budget } }
|
||||
if (npm === "@ai-sdk/alibaba") return { enableThinking: true, thinkingBudget: budget }
|
||||
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
|
||||
if (modelID.includes("anthropic"))
|
||||
return {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } },
|
||||
},
|
||||
}
|
||||
if (modelID.includes("gemini"))
|
||||
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
|
||||
if (modelID.includes("cohere")) return { modelParams: { thinking: { type: "enabled", token_budget: budget } } }
|
||||
}
|
||||
}
|
||||
|
||||
function gatewayPackage(modelID: string) {
|
||||
const separator = modelID.indexOf("/")
|
||||
if (separator <= 0) return
|
||||
const prefix = modelID.slice(0, separator)
|
||||
if (prefix === "anthropic") return "@ai-sdk/anthropic"
|
||||
if (prefix === "google") return "@ai-sdk/google"
|
||||
if (prefix === "amazon") return "@ai-sdk/amazon-bedrock"
|
||||
if (prefix === "alibaba") return "@ai-sdk/alibaba"
|
||||
}
|
||||
|
||||
function anthropicManualThinking(modelID: string) {
|
||||
const familyFirst = /(?:claude-)?(?:opus|sonnet|haiku)-(\d+)(?:[.-](\d+))?/i.exec(modelID)
|
||||
const versionFirst = /claude-(\d+)(?:[.-](\d+))?-(?:opus|sonnet|haiku)/i.exec(modelID)
|
||||
const major = Number(familyFirst?.[1] ?? versionFirst?.[1])
|
||||
const rawMinor = Number(familyFirst?.[2] ?? versionFirst?.[2] ?? 0)
|
||||
if (!Number.isFinite(major)) return false
|
||||
const minor = rawMinor > 9 ? 0 : rawMinor
|
||||
return major < 4 || (major === 4 && minor < 6)
|
||||
}
|
||||
|
||||
function modeName(model: SourceModel, mode: string) {
|
||||
|
||||
@@ -11,9 +11,12 @@ export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const AISDK_PREFIX = "aisdk:"
|
||||
export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false
|
||||
export const isAISDK = (value: string | undefined): value is string => value?.startsWith(AISDK_PREFIX) ?? false
|
||||
export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`)
|
||||
export const packageName = (value: string | undefined) => {
|
||||
export function packageName(value: string): string
|
||||
export function packageName(value: undefined): undefined
|
||||
export function packageName(value: string | undefined): string | undefined
|
||||
export function packageName(value: string | undefined) {
|
||||
if (value === undefined || !isAISDK(value)) return value
|
||||
return value.slice(AISDK_PREFIX.length)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,105 @@ it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
|
||||
)
|
||||
|
||||
expect(body).toBeUndefined()
|
||||
expect(prepared.body.providerOptions).toEqual({ openai: { reasoningMode: "pro" } })
|
||||
expect(prepared.body.providerOptions).toEqual({
|
||||
openai: { forceReasoning: true, reasoningMode: "pro" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps package-specific AI SDK provider option keys", () =>
|
||||
Effect.gen(function* () {
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* aisdk.hook.sdk((event) => {
|
||||
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||
})
|
||||
|
||||
const cases = [
|
||||
["@ai-sdk/github-copilot", "copilot", { reasoningEffort: "high" }],
|
||||
["@ai-sdk/amazon-bedrock/mantle", "openai", { reasoningEffort: "high", forceReasoning: true }],
|
||||
["@ai-sdk/openai-compatible", "test-provider", { reasoningEffort: "high" }],
|
||||
["@jerome-benoit/sap-ai-provider-v2", "sap-ai", { reasoningEffort: "high" }],
|
||||
["ai-gateway-provider", "openaiCompatible", { reasoningEffort: "high" }],
|
||||
] as const
|
||||
for (const [packageName, key, settings] of cases) {
|
||||
const resolved = yield* aisdk.model(model(packageName, { reasoningEffort: "high" }))
|
||||
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: resolved, prompt: "Hello" }),
|
||||
)
|
||||
expect(prepared.body.providerOptions).toEqual({ [key]: settings })
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("forces reasoning and projects both Azure AI SDK namespaces", () =>
|
||||
Effect.gen(function* () {
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* aisdk.hook.sdk((event) => {
|
||||
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||
})
|
||||
|
||||
const openai = yield* aisdk.model(model("@ai-sdk/openai", { reasoningEffort: "high" }))
|
||||
const openaiPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: openai, prompt: "Hello" }),
|
||||
)
|
||||
expect(openaiPrepared.body.providerOptions).toEqual({
|
||||
openai: { reasoningEffort: "high", forceReasoning: true },
|
||||
})
|
||||
|
||||
const azure = yield* aisdk.model(model("@ai-sdk/azure", { reasoningEffort: "high" }))
|
||||
const azurePrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: azure, prompt: "Hello" }),
|
||||
)
|
||||
expect(azurePrepared.body.providerOptions).toEqual({
|
||||
openai: { reasoningEffort: "high", forceReasoning: true },
|
||||
azure: { reasoningEffort: "high", forceReasoning: true },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("routes AI Gateway model options by upstream prefix", () =>
|
||||
Effect.gen(function* () {
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* aisdk.hook.sdk((event) => {
|
||||
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||
})
|
||||
|
||||
const anthropic = yield* aisdk.model({
|
||||
...model("@ai-sdk/gateway", {
|
||||
gateway: { order: ["anthropic"] },
|
||||
thinking: { type: "adaptive" },
|
||||
}),
|
||||
modelID: ModelV2.ID.make("anthropic/claude-sonnet-5"),
|
||||
})
|
||||
const anthropicPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: anthropic, prompt: "Hello" }),
|
||||
)
|
||||
expect(anthropicPrepared.body.providerOptions).toEqual({
|
||||
gateway: { order: ["anthropic"] },
|
||||
anthropic: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
|
||||
const bedrock = yield* aisdk.model({
|
||||
...model("@ai-sdk/gateway", { reasoningConfig: { type: "enabled" } }),
|
||||
modelID: ModelV2.ID.make("amazon/nova-2-lite"),
|
||||
})
|
||||
const bedrockPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: bedrock, prompt: "Hello" }),
|
||||
)
|
||||
expect(bedrockPrepared.body.providerOptions).toEqual({
|
||||
bedrock: { reasoningConfig: { type: "enabled" } },
|
||||
})
|
||||
|
||||
const fallback = yield* aisdk.model({
|
||||
...model("@ai-sdk/gateway", { reasoningEffort: "high" }),
|
||||
modelID: ModelV2.ID.make("deepseek/deepseek-v4"),
|
||||
})
|
||||
const fallbackPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||
LLM.request({ model: fallback, prompt: "Hello" }),
|
||||
)
|
||||
expect(fallbackPrepared.body.providerOptions).toEqual({
|
||||
deepseek: { reasoningEffort: "high" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const fixture = {
|
||||
id: "acme",
|
||||
name: "Acme",
|
||||
env: ["ACME_API_KEY"],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
"acme-1": {
|
||||
id: "acme-1",
|
||||
@@ -57,7 +58,7 @@ const fixtureSnapshot = [
|
||||
info: {
|
||||
id: ProviderV2.ID.make("acme"),
|
||||
name: "Acme",
|
||||
package: "",
|
||||
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||
},
|
||||
models: [
|
||||
{
|
||||
@@ -97,6 +98,7 @@ const fixture2 = {
|
||||
id: "beta",
|
||||
name: "Beta",
|
||||
env: ["BETA_API_KEY"],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
"beta-1": {
|
||||
id: "beta-1",
|
||||
@@ -117,7 +119,7 @@ const fixture2Snapshot = [
|
||||
info: {
|
||||
id: ProviderV2.ID.make("beta"),
|
||||
name: "Beta",
|
||||
package: "",
|
||||
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||
},
|
||||
models: [
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
{ "type": "effort", "values": ["low", "high"] },
|
||||
{ "type": "effort", "values": [null, "null", "low", "high"] },
|
||||
{ "type": "budget_tokens", "min": 1024, "max": 64000 },
|
||||
{ "type": "toggle" }
|
||||
],
|
||||
@@ -27,6 +27,11 @@
|
||||
"headers": { "x-mode": "high" },
|
||||
"body": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"provider": {
|
||||
"body": { "reasoning": { "mode": "pro" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,18 +54,296 @@
|
||||
"reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 64000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 8192 }
|
||||
"limit": { "context": 128000, "output": 64000 }
|
||||
},
|
||||
"claude-effort": {
|
||||
"id": "claude-effort",
|
||||
"id": "claude-opus-4.7",
|
||||
"name": "Claude Effort",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 8192 }
|
||||
},
|
||||
"claude-toggle": {
|
||||
"id": "claude-toggle",
|
||||
"name": "Claude Toggle",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 8192 }
|
||||
},
|
||||
"claude-opus-4-5": {
|
||||
"id": "claude-opus-4-5",
|
||||
"name": "Claude Opus 4.5",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
{ "type": "effort", "values": ["low", "high"] },
|
||||
{ "type": "budget_tokens", "min": 1024 }
|
||||
],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 8192 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"xai": {
|
||||
"id": "xai",
|
||||
"name": "xAI",
|
||||
"env": ["XAI_API_KEY"],
|
||||
"npm": "@ai-sdk/xai",
|
||||
"models": {
|
||||
"grok-4.5": {
|
||||
"id": "grok-4.5",
|
||||
"name": "Grok 4.5",
|
||||
"release_date": "2026-07-08",
|
||||
"attachment": true,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "effort", "values": ["low", "medium", "high"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 500000, "output": 500000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"opencode-go": {
|
||||
"id": "opencode-go",
|
||||
"name": "OpenCode Go",
|
||||
"env": ["OPENCODE_API_KEY"],
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"models": {
|
||||
"minimax-m3": {
|
||||
"id": "minimax-m3",
|
||||
"name": "MiniMax-M3",
|
||||
"release_date": "2026-05-31",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 1000000, "output": 131072 },
|
||||
"provider": { "npm": "@ai-sdk/anthropic" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"alibaba": {
|
||||
"id": "alibaba",
|
||||
"name": "Alibaba",
|
||||
"env": ["ALIBABA_API_KEY"],
|
||||
"npm": "@ai-sdk/alibaba",
|
||||
"models": {
|
||||
"toggle-only": {
|
||||
"id": "toggle-only",
|
||||
"name": "Toggle Only",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"toggle-budget": {
|
||||
"id": "toggle-budget",
|
||||
"name": "Toggle Budget",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"vercel": {
|
||||
"id": "vercel",
|
||||
"name": "Vercel AI Gateway",
|
||||
"env": ["AI_GATEWAY_API_KEY"],
|
||||
"npm": "@ai-sdk/gateway",
|
||||
"models": {
|
||||
"alibaba/qwen-toggle": {
|
||||
"id": "alibaba/qwen-toggle",
|
||||
"name": "Gateway Alibaba Toggle",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"amazon/nova-2-lite": {
|
||||
"id": "amazon/nova-2-lite",
|
||||
"name": "Gateway Nova 2 Lite",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"deepseek/deepseek-toggle": {
|
||||
"id": "deepseek/deepseek-toggle",
|
||||
"name": "Gateway DeepSeek Toggle",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"id": "openrouter",
|
||||
"name": "OpenRouter",
|
||||
"env": ["OPENROUTER_API_KEY"],
|
||||
"npm": "@openrouter/ai-sdk-provider",
|
||||
"models": {
|
||||
"openrouter-toggle": {
|
||||
"id": "openrouter-toggle",
|
||||
"name": "OpenRouter Toggle",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"id": "google",
|
||||
"name": "Google",
|
||||
"env": ["GOOGLE_GENERATIVE_AI_API_KEY"],
|
||||
"npm": "@ai-sdk/google",
|
||||
"models": {
|
||||
"gemini-2.5-flash": {
|
||||
"id": "gemini-2.5-flash",
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 0, "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"google-vertex": {
|
||||
"id": "google-vertex",
|
||||
"name": "Google Vertex",
|
||||
"env": ["GOOGLE_VERTEX_PROJECT"],
|
||||
"npm": "@ai-sdk/google-vertex",
|
||||
"models": {
|
||||
"gemini-2.5-flash-lite": {
|
||||
"id": "gemini-2.5-flash-lite",
|
||||
"name": "Gemini 2.5 Flash Lite",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 512, "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": {
|
||||
"id": "amazon-bedrock",
|
||||
"name": "Amazon Bedrock",
|
||||
"env": ["AWS_ACCESS_KEY_ID"],
|
||||
"npm": "@ai-sdk/amazon-bedrock",
|
||||
"models": {
|
||||
"amazon.nova-2-lite-v1:0": {
|
||||
"id": "amazon.nova-2-lite-v1:0",
|
||||
"name": "Nova 2 Lite",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"sap-ai-core": {
|
||||
"id": "sap-ai-core",
|
||||
"name": "SAP AI Core",
|
||||
"env": ["AICORE_SERVICE_KEY"],
|
||||
"npm": "@jerome-benoit/sap-ai-provider-v2",
|
||||
"models": {
|
||||
"gemini-2.5-flash": {
|
||||
"id": "gemini-2.5-flash",
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 0, "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"amazon--nova-lite": {
|
||||
"id": "amazon--nova-lite",
|
||||
"name": "Nova Lite",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"cohere--command-a-reasoning": {
|
||||
"id": "cohere--command-a-reasoning",
|
||||
"name": "Command A Reasoning",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
{ "type": "toggle" },
|
||||
{ "type": "effort", "values": ["low", "high"] },
|
||||
{ "type": "budget_tokens", "min": 1 }
|
||||
],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"anthropic--claude-4.7-opus": {
|
||||
"id": "anthropic--claude-4.7-opus",
|
||||
"name": "Claude 4.7 Opus",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "effort", "values": ["low"] }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 8192 }
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
},
|
||||
"anthropic--claude-4-sonnet": {
|
||||
"id": "anthropic--claude-4-sonnet",
|
||||
"name": "Claude 4 Sonnet",
|
||||
"release_date": "2026-01-01",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 16000 }],
|
||||
"temperature": true,
|
||||
"tool_call": true,
|
||||
"limit": { "context": 128000, "output": 20000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,12 @@ describe("ModelsDevPlugin", () => {
|
||||
ModelV2.VariantID.make("high"),
|
||||
])
|
||||
|
||||
const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro"))
|
||||
expect(pro).toMatchObject({
|
||||
id: "gpt-reasoning-pro",
|
||||
body: { reasoning: { mode: "pro" } },
|
||||
})
|
||||
|
||||
const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
@@ -299,17 +305,292 @@ describe("ModelsDevPlugin", () => {
|
||||
})
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
|
||||
})
|
||||
|
||||
const anthropicEffortModel = yield* catalog.model.get(
|
||||
ProviderV2.ID.anthropic,
|
||||
ModelV2.ID.make("claude-effort"),
|
||||
ModelV2.ID.make("claude-opus-4.7"),
|
||||
)
|
||||
expect(anthropicEffortModel?.variants).toContainEqual({
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||
})
|
||||
expect(anthropicEffortModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||
},
|
||||
])
|
||||
|
||||
const anthropicToggleModel = yield* catalog.model.get(
|
||||
ProviderV2.ID.anthropic,
|
||||
ModelV2.ID.make("claude-toggle"),
|
||||
)
|
||||
expect(anthropicToggleModel?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5"))
|
||||
expect(opus45?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("low"), settings: { effort: "low" } },
|
||||
{ id: ModelV2.VariantID.make("high"), settings: { effort: "high" } },
|
||||
])
|
||||
|
||||
const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5"))
|
||||
expect(grok?.variants).toEqual(
|
||||
["low", "medium", "high"].map((id) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
settings: { reasoningEffort: id },
|
||||
})),
|
||||
)
|
||||
|
||||
const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3"))
|
||||
expect(minimax?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
settings: { thinking: { type: "adaptive", display: "summarized" } },
|
||||
},
|
||||
])
|
||||
|
||||
const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only"))
|
||||
expect(toggle?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
|
||||
])
|
||||
|
||||
const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget"))
|
||||
expect(combined?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle"))
|
||||
expect(gateway?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { enableThinking: true, thinkingBudget: 8000 },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { enableThinking: true, thinkingBudget: 16000 },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayNova = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("amazon/nova-2-lite"),
|
||||
)
|
||||
expect(gatewayNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const gatewayFallback = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("deepseek/deepseek-toggle"),
|
||||
)
|
||||
expect(gatewayFallback?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { reasoning: { enabled: false } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningEffort: "low" },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningEffort: "high" },
|
||||
},
|
||||
])
|
||||
|
||||
const openrouter = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("openrouter"),
|
||||
ModelV2.ID.make("openrouter-toggle"),
|
||||
)
|
||||
expect(openrouter?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
|
||||
])
|
||||
|
||||
const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash"))
|
||||
expect(google?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const vertex = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("google-vertex"),
|
||||
ModelV2.ID.make("gemini-2.5-flash-lite"),
|
||||
)
|
||||
expect(vertex?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
},
|
||||
])
|
||||
|
||||
const bedrock = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("amazon-bedrock"),
|
||||
ModelV2.ID.make("amazon.nova-2-lite-v1:0"),
|
||||
)
|
||||
expect(bedrock?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapGemini = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("gemini-2.5-flash"),
|
||||
)
|
||||
expect(sapGemini?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapNova = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("amazon--nova-lite"),
|
||||
)
|
||||
expect(sapNova?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapCohere = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("cohere--command-a-reasoning"),
|
||||
)
|
||||
expect(sapCohere?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
settings: { modelParams: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: { modelParams: { reasoning_effort: "low" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: { modelParams: { reasoning_effort: "high" } },
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicEffort = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4.7-opus"),
|
||||
)
|
||||
expect(sapAnthropicEffort?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("low"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
output_config: { effort: "low" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const sapAnthropicBudget = yield* catalog.model.get(
|
||||
ProviderV2.ID.make("sap-ai-core"),
|
||||
ModelV2.ID.make("anthropic--claude-4-sonnet"),
|
||||
)
|
||||
expect(sapAnthropicBudget?.variants).toEqual([
|
||||
{
|
||||
id: ModelV2.VariantID.make("high"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("max"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
|
||||
Reference in New Issue
Block a user