From 5a9d72ea027c1ee3960154b0b6f09cab4e6c0937 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 9 Jun 2026 22:42:48 +0200 Subject: [PATCH] feat(ai): add Claude Fable 5 metadata --- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/scripts/generate-models.ts | 9 ++++++++- packages/ai/src/providers/amazon-bedrock.ts | 9 +++++++-- packages/ai/src/providers/anthropic.ts | 4 ++-- .../anthropic-force-adaptive-thinking.test.ts | 7 +++++++ .../ai/test/bedrock-thinking-payload.test.ts | 19 +++++++++++++++++++ packages/ai/test/supports-xhigh.test.ts | 14 +++++++++++++- 7 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8e26b4e2c..dfd89b7d7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added Claude Fable 5 to Anthropic and Amazon Bedrock model metadata, with adaptive thinking and `xhigh` effort support. + ### Fixed - Fixed Amazon Bedrock inference profile ARN region resolution to prefer the ARN's embedded region over `AWS_REGION` ([#5527](https://github.com/earendil-works/pi/pull/5527) by [@AJM10565](https://github.com/AJM10565)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 408a80345..2cc0e1718 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -232,7 +232,8 @@ function isAnthropicAdaptiveThinkingModel(modelId: string): boolean { modelId.includes("opus-4-8") || modelId.includes("opus-4.8") || modelId.includes("sonnet-4-6") || - modelId.includes("sonnet-4.6") + modelId.includes("sonnet-4.6") || + modelId.includes("fable-5") ); } @@ -294,6 +295,12 @@ function applyThinkingLevelMetadata(model: Model): void { ) { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } + if ( + (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") && + model.id.includes("fable-5") + ) { + mergeThinkingLevelMap(model, { xhigh: "xhigh" }); + } if (model.api === "anthropic-messages" && isAnthropicAdaptiveThinkingModel(model.id)) { mergeAnthropicMessagesCompat(model, { forceAdaptiveThinking: true }); } diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index e357bac51..b022b5223 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -528,13 +528,18 @@ function getModelMatchCandidates(modelId: string, modelName?: string): string[] function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean { const candidates = getModelMatchCandidates(modelId, modelName); return candidates.some( - (s) => s.includes("opus-4-6") || s.includes("opus-4-7") || s.includes("opus-4-8") || s.includes("sonnet-4-6"), + (s) => + s.includes("opus-4-6") || + s.includes("opus-4-7") || + s.includes("opus-4-8") || + s.includes("sonnet-4-6") || + s.includes("fable-5"), ); } function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean { const candidates = getModelMatchCandidates(model.id, model.name); - return candidates.some((s) => s.includes("opus-4-7") || s.includes("opus-4-8")); + return candidates.some((s) => s.includes("opus-4-7") || s.includes("opus-4-8") || s.includes("fable-5")); } function mapThinkingLevelToEffort( diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 56b82d8c5..158311b07 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -200,7 +200,7 @@ export interface AnthropicOptions extends StreamOptions { * Effort level for adaptive thinking models. * Controls how much thinking Claude allocates: * - "max": Always thinks with no constraints (Opus 4.6 only) - * - "xhigh": Highest reasoning level (Opus 4.7) + * - "xhigh": Highest reasoning level (Opus 4.7+, Fable 5) * - "high": Always thinks, deep reasoning * - "medium": Moderate thinking, may skip for simple queries * - "low": Minimal thinking, skips for simple tasks @@ -711,7 +711,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti /** * Map ThinkingLevel to Anthropic effort levels for adaptive thinking. - * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7 supports "xhigh". + * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7+ and Fable 5 support "xhigh". */ function mapThinkingLevelToEffort( model: Model<"anthropic-messages">, diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index 5b3511ca4..e797c3a94 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -83,6 +83,13 @@ describe("Anthropic forceAdaptiveThinking compat override", () => { expect(payload.output_config).toEqual({ effort: "medium" }); }); + it("uses adaptive thinking with native xhigh effort for Claude Fable 5", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-fable-5"), { reasoning: "xhigh" }); + + expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(payload.output_config).toEqual({ effort: "xhigh" }); + }); + it("allows built-in adaptive models to opt out with compat.forceAdaptiveThinking false", async () => { const model: Model<"anthropic-messages"> = { ...getModel("anthropic", "claude-opus-4-8"), diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index 93143e384..8f4e06e72 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -83,6 +83,25 @@ describe("Bedrock thinking payload", () => { expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); }); + it("uses adaptive thinking for Claude Fable 5 when reasoning is enabled", async () => { + const model = getModel("amazon-bedrock", "global.anthropic.claude-fable-5"); + + const payload = await capturePayload(model); + + expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" }); + expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); + }); + + it("maps xhigh reasoning to effort=xhigh for Claude Fable 5", async () => { + const model = getModel("amazon-bedrock", "global.anthropic.claude-fable-5"); + + const payload = await capturePayload(model, { reasoning: "xhigh" }); + + expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" }); + }); + it("omits display for GovCloud model ids on non-adaptive Claude thinking", async () => { const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); const model: Model<"bedrock-converse-stream"> = { diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index d5ec3b5bf..80b3e683c 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -20,7 +20,13 @@ describe("getSupportedThinkingLevels", () => { expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); - it("does not include xhigh for non-Opus Anthropic models", () => { + it("includes xhigh for Anthropic Claude Fable 5 on anthropic-messages API", () => { + const model = getModel("anthropic", "claude-fable-5"); + expect(model).toBeDefined(); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); + }); + + it("does not include xhigh for Claude Sonnet 4.5", () => { const model = getModel("anthropic", "claude-sonnet-4-5"); expect(model).toBeDefined(); expect(getSupportedThinkingLevels(model!)).not.toContain("xhigh"); @@ -79,4 +85,10 @@ describe("getSupportedThinkingLevels", () => { expect(model).toBeDefined(); expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); }); + + it("includes xhigh for Bedrock Claude Fable 5", () => { + const model = getModel("amazon-bedrock", "global.anthropic.claude-fable-5"); + expect(model).toBeDefined(); + expect(getSupportedThinkingLevels(model!)).toContain("xhigh"); + }); });