fix(provider): generalize Claude adaptive thinking (#38757)
This commit is contained in:
@@ -635,24 +635,23 @@ function openaiCompatibleReasoningEfforts(id: string) {
|
|||||||
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
||||||
}
|
}
|
||||||
|
|
||||||
function anthropicOpus47OrLater(apiId: string) {
|
function anthropicUsesModernAdaptiveThinking(apiId: string) {
|
||||||
// Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
|
if (!apiId.toLowerCase().includes("claude-")) return false
|
||||||
// Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
|
// Covers family-first IDs such as claude-opus-4.7 and version-first IDs such as claude-4.7-opus.
|
||||||
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
|
// Limit minors to two digits so release dates in IDs such as claude-opus-4-20250514 are not versions.
|
||||||
if (!version) return false
|
const version = /claude-(?:[a-z]+-)?(\d+)(?:[.-](\d{1,2}))?(?:[.@-]|$)/i.exec(apiId)
|
||||||
const major = Number(version[1] ?? version[3])
|
if (!version) return true
|
||||||
const minor = Number(version[2] ?? version[4])
|
const major = Number(version[1])
|
||||||
|
const minor = Number(version[2] ?? 0)
|
||||||
return major > 4 || (major === 4 && minor >= 7)
|
return major > 4 || (major === 4 && minor >= 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
function anthropicSonnet5OrLater(apiId: string) {
|
function anthropicOpus45(apiId: string) {
|
||||||
const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId)
|
return ["opus-4-5", "opus-4.5"].some((value) => apiId.includes(value))
|
||||||
if (!version) return false
|
|
||||||
return Number(version[1] ?? version[2]) >= 5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
||||||
if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) {
|
if (anthropicUsesModernAdaptiveThinking(apiId)) {
|
||||||
return ["low", "medium", "high", "xhigh", "max"]
|
return ["low", "medium", "high", "xhigh", "max"]
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -666,7 +665,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function anthropicOmitsThinking(apiId: string) {
|
function anthropicOmitsThinking(apiId: string) {
|
||||||
return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")
|
return anthropicUsesModernAdaptiveThinking(apiId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function googleThinkingLevelEfforts(apiId: string) {
|
function googleThinkingLevelEfforts(apiId: string) {
|
||||||
@@ -990,8 +989,10 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
|
if (anthropicOpus45(model.api.id)) {
|
||||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
|
return Object.fromEntries(
|
||||||
|
WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, anthropicOpus45Effort(model, effort)]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1103,7 +1104,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
if (id.includes("anthropic")) {
|
if (id.includes("anthropic")) {
|
||||||
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). Claude 4.7+ defaults `display` to "omitted".
|
||||||
return wrapInSapModelParams(
|
return wrapInSapModelParams(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
adaptiveEfforts.map((effort) => [
|
adaptiveEfforts.map((effort) => [
|
||||||
@@ -1711,6 +1712,14 @@ function reasoningEffort(model: Provider.Model, effort: string) {
|
|||||||
...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
|
...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if (anthropicOpus45(model.api.id))
|
||||||
|
return {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "enabled",
|
||||||
|
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
|
||||||
|
maxReasoningEffort: effort,
|
||||||
|
},
|
||||||
|
}
|
||||||
if (model.api.id.includes("anthropic")) return
|
if (model.api.id.includes("anthropic")) return
|
||||||
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
||||||
case "@ai-sdk/gateway":
|
case "@ai-sdk/gateway":
|
||||||
@@ -1751,7 +1760,7 @@ function reasoningEffort(model: Provider.Model, effort: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function anthropicEffort(model: Provider.Model, effort: string) {
|
function anthropicEffort(model: Provider.Model, effort: string) {
|
||||||
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
|
if (anthropicOpus45(model.api.id)) return anthropicOpus45Effort(model, effort)
|
||||||
// Kimi defaults to omitting adaptive thinking text unless summarized display is requested.
|
// Kimi defaults to omitting adaptive thinking text unless summarized display is requested.
|
||||||
if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort }
|
if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort }
|
||||||
if (!anthropicAdaptiveEfforts(model.api.id)) return
|
if (!anthropicAdaptiveEfforts(model.api.id)) return
|
||||||
@@ -1764,6 +1773,16 @@ function anthropicEffort(model: Provider.Model, effort: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function anthropicOpus45Effort(model: Provider.Model, effort: string) {
|
||||||
|
return {
|
||||||
|
thinking: {
|
||||||
|
type: "enabled",
|
||||||
|
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
|
||||||
|
},
|
||||||
|
effort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reasoningBudget(model: Provider.Model, budget: number) {
|
function reasoningBudget(model: Provider.Model, budget: number) {
|
||||||
switch (model.api.npm) {
|
switch (model.api.npm) {
|
||||||
case "@openrouter/ai-sdk-provider":
|
case "@openrouter/ai-sdk-provider":
|
||||||
|
|||||||
@@ -3213,6 +3213,7 @@ describe("ProviderTransform.reasoningVariants", () => {
|
|||||||
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
"claude-opus-4-7",
|
"claude-opus-4-7",
|
||||||
],
|
],
|
||||||
|
["@ai-sdk/anthropic", { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-5"],
|
||||||
["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
||||||
["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
||||||
[
|
[
|
||||||
@@ -3264,13 +3265,18 @@ describe("ProviderTransform.reasoningVariants", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses bare effort for Claude Opus 4.5", () => {
|
test("combines effort with extended thinking for Claude Opus 4.5", () => {
|
||||||
expect(
|
expect(
|
||||||
ProviderTransform.reasoningVariants(
|
ProviderTransform.reasoningVariants(
|
||||||
model([{ type: "effort", values: ["high"] }]),
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
target("@ai-sdk/anthropic", "claude-opus-4-5"),
|
target("@ai-sdk/anthropic", "claude-opus-4-5"),
|
||||||
),
|
),
|
||||||
).toEqual({ high: { effort: "high" } })
|
).toEqual({
|
||||||
|
high: {
|
||||||
|
thinking: { type: "enabled", budgetTokens: 16_000 },
|
||||||
|
effort: "high",
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses explicit effort metadata for Anthropic-compatible models", () => {
|
test("uses explicit effort metadata for Anthropic-compatible models", () => {
|
||||||
@@ -3319,6 +3325,38 @@ describe("ProviderTransform.reasoningVariants", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses adaptive reasoning config for Claude Opus 5 on Bedrock", () => {
|
||||||
|
const result = ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }]),
|
||||||
|
target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-5"),
|
||||||
|
)
|
||||||
|
expect(Object.keys(result ?? {})).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||||
|
expect(result?.high).toEqual({
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "adaptive",
|
||||||
|
maxReasoningEffort: "high",
|
||||||
|
display: "summarized",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("combines effort with extended thinking for Claude Opus 4.5 on Bedrock", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
|
target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-4-5-20251101-v1:0"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
high: {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "enabled",
|
||||||
|
budgetTokens: 16_000,
|
||||||
|
maxReasoningEffort: "high",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("does not replace unsupported Anthropic Bedrock effort options with token budgets", () => {
|
test("does not replace unsupported Anthropic Bedrock effort options with token budgets", () => {
|
||||||
expect(
|
expect(
|
||||||
ProviderTransform.reasoningVariants(
|
ProviderTransform.reasoningVariants(
|
||||||
@@ -4617,11 +4655,17 @@ describe("ProviderTransform.variants", () => {
|
|||||||
|
|
||||||
describe("@ai-sdk/anthropic", () => {
|
describe("@ai-sdk/anthropic", () => {
|
||||||
for (const testCase of [
|
for (const testCase of [
|
||||||
|
{
|
||||||
|
name: "opus 4 dated",
|
||||||
|
apiIds: ["claude-opus-4-20250514"],
|
||||||
|
efforts: ["high", "max"],
|
||||||
|
expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "opus 4.5",
|
name: "opus 4.5",
|
||||||
apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"],
|
apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"],
|
||||||
efforts: ["low", "medium", "high"],
|
efforts: ["low", "medium", "high"],
|
||||||
expectedHigh: { effort: "high" },
|
expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 }, effort: "high" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sonnet 4.6",
|
name: "sonnet 4.6",
|
||||||
@@ -4653,6 +4697,18 @@ describe("ProviderTransform.variants", () => {
|
|||||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "opus 5",
|
||||||
|
apiIds: ["claude-opus-5", "claude-opus-5-20260724"],
|
||||||
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unversioned future model",
|
||||||
|
apiIds: ["claude-future"],
|
||||||
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "fable 5",
|
name: "fable 5",
|
||||||
apiIds: ["claude-fable-5"],
|
apiIds: ["claude-fable-5"],
|
||||||
@@ -4772,6 +4828,28 @@ describe("ProviderTransform.variants", () => {
|
|||||||
effort: "high",
|
effort: "high",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("opus 5 uses adaptive reasoning for Vertex model IDs", () => {
|
||||||
|
const result = ProviderTransform.variants(
|
||||||
|
createMockModel({
|
||||||
|
id: "google-vertex-anthropic/claude-opus-5@default",
|
||||||
|
providerID: "google-vertex-anthropic",
|
||||||
|
api: {
|
||||||
|
id: "claude-opus-5@default",
|
||||||
|
url: "https://us-central1-aiplatform.googleapis.com",
|
||||||
|
npm: "@ai-sdk/google-vertex/anthropic",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||||
|
expect(result.high).toEqual({
|
||||||
|
thinking: {
|
||||||
|
type: "adaptive",
|
||||||
|
display: "summarized",
|
||||||
|
},
|
||||||
|
effort: "high",
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("@ai-sdk/amazon-bedrock", () => {
|
describe("@ai-sdk/amazon-bedrock", () => {
|
||||||
@@ -4867,6 +4945,28 @@ describe("ProviderTransform.variants", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("anthropic opus 5 returns adaptive reasoning options with xhigh", () => {
|
||||||
|
const result = ProviderTransform.variants(
|
||||||
|
createMockModel({
|
||||||
|
id: "bedrock/anthropic-claude-opus-5",
|
||||||
|
providerID: "bedrock",
|
||||||
|
api: {
|
||||||
|
id: "us.anthropic.claude-opus-5-v1:0",
|
||||||
|
url: "https://bedrock.amazonaws.com",
|
||||||
|
npm: "@ai-sdk/amazon-bedrock",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||||
|
expect(result.high).toEqual({
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "adaptive",
|
||||||
|
maxReasoningEffort: "high",
|
||||||
|
display: "summarized",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
|
||||||
const model = createMockModel({
|
const model = createMockModel({
|
||||||
id: "bedrock/llama-4",
|
id: "bedrock/llama-4",
|
||||||
@@ -5055,6 +5155,12 @@ describe("ProviderTransform.variants", () => {
|
|||||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
thinking: { type: "adaptive", display: "summarized" },
|
thinking: { type: "adaptive", display: "summarized" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "opus 5",
|
||||||
|
apiIds: ["anthropic--claude-opus-5", "anthropic--claude-5-opus"],
|
||||||
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
thinking: { type: "adaptive", display: "summarized" },
|
||||||
|
},
|
||||||
]) {
|
]) {
|
||||||
for (const apiId of testCase.apiIds) {
|
for (const apiId of testCase.apiIds) {
|
||||||
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
|
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user