Skip to content

Commit 0e7b5c3

Browse files
authored
feat(anthropic): support Claude Fable 5 adaptive thinking (#91882)
* feat(anthropic): support Claude Fable 5 * test(anthropic): tighten Fable stream fixtures * fix(anthropic): preserve Vertex input types * test(anthropic): use provider-ready Vertex effort * fix(anthropic): support Fable deployment aliases * fix(anthropic): discard incomplete Fable output * feat(anthropic): support Fable on Bedrock * fix(anthropic): preserve Fable reasoning contracts * refactor(anthropic): unify canonical Claude model policy * fix(anthropic): satisfy extension thinking types * test(anthropic): complete canonical alias fixture * fix(bedrock): scope thinking case declarations
1 parent ade5ac0 commit 0e7b5c3

82 files changed

Lines changed: 4458 additions & 614 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
4768607253fdc720cb2bc280ac285ccfa7f7057a01659691f5be5b1f58422789 plugin-sdk-api-baseline.json
2-
7901bc511cf6f9628df4cd619035265f48c40939e4e8e51c5c10dc73a263f183 plugin-sdk-api-baseline.jsonl
1+
8a2769df428906990ee0d1bf8b0423f2a099b053c64c816d092ff84d61e11633 plugin-sdk-api-baseline.json
2+
28b798973f3fb2a5b33ccbb6e3c1ac0453fa234a3a1c6cdc27935c27639bd104 plugin-sdk-api-baseline.jsonl

docs/plugins/reference/anthropic-vertex.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.
1717
## Surface
1818

1919
providers: anthropic-vertex
20+
21+
## Claude Fable 5
22+
23+
Use `anthropic-vertex/claude-fable-5` where the model is available in your Google Cloud region.
24+
Fable 5 always uses adaptive thinking and defaults to `high` effort. `/think off` and
25+
`/think minimal` use `low` effort because the model does not support disabling thinking.

docs/plugins/reference/microsoft-foundry.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ chat APIs:
5050
Anthropic Claude deployments in Microsoft Foundry use the Anthropic Messages
5151
API shape, not the OpenAI-compatible `/openai/v1` shape. Configure those as a
5252
custom `anthropic-messages` provider until the Microsoft Foundry plugin grows a
53-
native Anthropic runtime.
53+
native Anthropic runtime. When the Foundry deployment name differs from the
54+
Claude model ID, set `params.canonicalModelId` on the model entry so OpenClaw
55+
can apply model-specific wire contracts, map `/think off` correctly, and
56+
preserve signed thinking safely.
5457

5558
## MAI image generation
5659

docs/providers/anthropic.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,12 @@ Anthropic's current public docs:
160160
</Tab>
161161
</Tabs>
162162

163-
## Thinking defaults (Claude 4.8 and 4.6)
163+
## Thinking defaults (Claude Fable 5, 4.8, and 4.6)
164+
165+
`anthropic/claude-fable-5` always uses adaptive thinking and defaults to `high`
166+
effort. Because Anthropic does not allow thinking to be disabled for this model,
167+
`/think off` and `/think minimal` use `low` effort. OpenClaw also omits custom
168+
temperature values for Fable 5 requests.
164169

165170
Claude Opus 4.8 keeps thinking off by default in OpenClaw. When you explicitly enable adaptive thinking with `/think high|xhigh|max`, OpenClaw sends Anthropic's Opus 4.8 effort values; Claude 4.6 models default to `adaptive`.
166171

docs/providers/bedrock.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,25 @@ openclaw models list
310310
the request options object and the `inferenceConfig` payload field.
311311
</Accordion>
312312

313+
<Accordion title="Claude Fable 5">
314+
Use `amazon-bedrock/anthropic.claude-fable-5` in `us-east-1`, or the
315+
regional inference ids such as `us.anthropic.claude-fable-5`.
316+
OpenClaw applies Fable's 1M context window, 128K output limit, always-on
317+
adaptive thinking, and supported effort mapping. `/think off` and
318+
`/think minimal` map to `low`; unsupported temperature and forced tool
319+
choice controls are omitted. Streaming output is held until Bedrock
320+
returns a terminal status so mid-stream refusals do not expose partial text.
321+
Fable supports only the standard service tier; OpenClaw ignores configured
322+
`flex`, `priority`, and `reserved` tiers for this model.
323+
324+
AWS requires an explicit `provider_data_share` data-retention opt-in before
325+
Fable is available. Prompts and completions are shared with Anthropic and
326+
retained for up to 30 days for trust and safety. Review and configure
327+
[Bedrock data retention](https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html)
328+
before enabling the model.
329+
330+
</Accordion>
331+
313332
<Accordion title="Guardrails">
314333
You can apply [Amazon Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html)
315334
to all Bedrock model invocations by adding a `guardrail` object to the

extensions/amazon-bedrock/bedrock-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export function supportsBedrockPromptCaching(modelId: string, modelName?: string
4141
if (candidates.some((s) => s.includes("-4-"))) {
4242
return true;
4343
}
44+
if (candidates.some((s) => s.includes("claude-fable-5"))) {
45+
return true;
46+
}
4447
if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) {
4548
return true;
4649
}

extensions/amazon-bedrock/discovery.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,58 @@ describe("bedrock discovery", () => {
248248
);
249249
});
250250

251+
it("applies Fable limits and reasoning metadata to foundation and profile models", async () => {
252+
sendMock
253+
.mockResolvedValueOnce({
254+
modelSummaries: [
255+
{
256+
modelId: "anthropic.claude-fable-5",
257+
modelName: "Claude Fable 5",
258+
providerName: "anthropic",
259+
inputModalities: ["TEXT", "IMAGE"],
260+
outputModalities: ["TEXT"],
261+
responseStreamingSupported: true,
262+
modelLifecycle: { status: "ACTIVE" },
263+
},
264+
],
265+
})
266+
.mockResolvedValueOnce({
267+
inferenceProfileSummaries: [
268+
{
269+
inferenceProfileId: "company-fable",
270+
inferenceProfileName: "Company Fable",
271+
status: "ACTIVE",
272+
type: "APPLICATION",
273+
models: [
274+
{
275+
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5",
276+
},
277+
],
278+
},
279+
],
280+
});
281+
282+
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
283+
const expected = {
284+
reasoning: true,
285+
contextWindow: 1_000_000,
286+
maxTokens: 128_000,
287+
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
288+
};
289+
290+
expectModelFields(
291+
models.find((model) => model.id === "anthropic.claude-fable-5"),
292+
expected,
293+
);
294+
expectModelFields(
295+
models.find((model) => model.id === "company-fable"),
296+
{
297+
...expected,
298+
params: { canonicalModelId: "claude-fable-5" },
299+
},
300+
);
301+
});
302+
251303
it("caches results when refreshInterval is enabled", async () => {
252304
mockSingleActiveSummary();
253305

@@ -410,7 +462,9 @@ describe("bedrock discovery", () => {
410462
input: ["text", "image"],
411463
contextWindow: 1000000,
412464
maxTokens: 4096,
465+
params: { canonicalModelId: "claude-sonnet-4-6" },
413466
});
467+
expect(usProfile?.thinkingLevelMap).toBeUndefined();
414468
expectModelFields(euProfile, { input: ["text", "image"] });
415469
expectModelFields(globalProfile, { input: ["text", "image"] });
416470

@@ -546,6 +600,8 @@ describe("bedrock discovery", () => {
546600
contextWindow: 1_000_000,
547601
maxTokens: 4096,
548602
input: ["text"],
603+
params: { canonicalModelId: "claude-opus-4-6-v1:0" },
604+
thinkingLevelMap: { xhigh: null, max: "max" },
549605
});
550606
});
551607

extensions/amazon-bedrock/discovery.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ import type {
1818
ModelDefinitionConfig,
1919
ModelProviderConfig,
2020
} from "openclaw/plugin-sdk/provider-model-shared";
21+
import {
22+
resolveClaudeFable5ModelIdentity,
23+
resolveClaudeModelIdentity,
24+
supportsClaudeAdaptiveThinking,
25+
} from "openclaw/plugin-sdk/provider-model-shared";
2126
import {
2227
normalizeLowercaseStringOrEmpty,
2328
normalizeOptionalLowercaseString,
2429
} from "openclaw/plugin-sdk/string-coerce-runtime";
2530
import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js";
2631
import { resolveBedrockConfigApiKey } from "./discovery-shared.js";
32+
import { resolveBedrockNativeThinkingLevelMap } from "./thinking-policy.js";
2733

2834
const log = createSubsystemLogger("bedrock-discovery");
2935

@@ -53,6 +59,7 @@ const DEFAULT_MAX_TOKENS = 4096;
5359
*/
5460
const KNOWN_CONTEXT_WINDOWS: Record<string, number> = {
5561
// Anthropic Claude
62+
"anthropic.claude-fable-5": 1_000_000,
5663
"anthropic.claude-3-7-sonnet-20250219-v1:0": 200_000,
5764
"anthropic.claude-opus-4-8": 1_000_000,
5865
"anthropic.claude-opus-4-7": 1_000_000,
@@ -130,6 +137,9 @@ function resolveKnownContextWindow(modelId: string): number | undefined {
130137
const stripped = modelId.replace(/^(?:us|eu|ap|apac|au|jp|global)\./, "");
131138
const candidates = [modelId, stripped];
132139
for (const candidate of candidates) {
140+
if (resolveClaudeFable5ModelIdentity({ id: candidate })) {
141+
return 1_000_000;
142+
}
133143
if (/(?:^|[/.:])anthropic\.claude-opus-4[.-]8(?:$|[-.:/])/i.test(candidate)) {
134144
return 1_000_000;
135145
}
@@ -147,20 +157,14 @@ function resolveKnownContextWindow(modelId: string): number | undefined {
147157
return undefined;
148158
}
149159

150-
function isKnownClaudeOpus47OrNewerModelId(modelId: string): boolean {
151-
const stripped = modelId.replace(/^(?:us|eu|ap|apac|au|jp|global)\./, "");
152-
return [modelId, stripped].some((candidate) =>
153-
/(?:^|[/.:])anthropic\.claude-opus-4[.-][78](?:$|[-.:/])/i.test(candidate),
154-
);
155-
}
156-
157160
function resolveKnownThinkingLevelMap(
158161
modelId: string,
159162
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
160-
if (!isKnownClaudeOpus47OrNewerModelId(modelId)) {
161-
return undefined;
162-
}
163-
return { xhigh: "xhigh", max: "max" };
163+
return resolveBedrockNativeThinkingLevelMap(modelId);
164+
}
165+
166+
function resolveKnownMaxTokens(modelId: string): number | undefined {
167+
return resolveClaudeFable5ModelIdentity({ id: modelId }) ? 128_000 : undefined;
164168
}
165169

166170
const DEFAULT_COST = {
@@ -271,7 +275,7 @@ function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image
271275
}
272276

273277
function inferReasoningSupport(summary: BedrockModelSummary): boolean {
274-
if (isKnownClaudeOpus47OrNewerModelId(summary.modelId ?? "")) {
278+
if (supportsClaudeAdaptiveThinking({ id: summary.modelId })) {
275279
return true;
276280
}
277281
const haystack = normalizeLowercaseStringOrEmpty(
@@ -340,7 +344,7 @@ function toModelDefinition(
340344
input: mapInputModalities(summary),
341345
cost: DEFAULT_COST,
342346
contextWindow: resolveKnownContextWindow(id) ?? defaults.contextWindow,
343-
maxTokens: defaults.maxTokens,
347+
maxTokens: resolveKnownMaxTokens(id) ?? defaults.maxTokens,
344348
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
345349
};
346350
}
@@ -456,23 +460,30 @@ function resolveInferenceProfiles(
456460
const knownThinkingLevelMap = resolveKnownThinkingLevelMap(
457461
baseModelId ?? profile.inferenceProfileId,
458462
);
463+
const canonicalClaudeId = resolveClaudeModelIdentity({ id: baseModelId });
459464

460465
discovered.push({
461466
id: profile.inferenceProfileId,
462467
name: profile.inferenceProfileName?.trim() || profile.inferenceProfileId,
463468
reasoning:
464469
baseModel?.reasoning ??
465-
isKnownClaudeOpus47OrNewerModelId(baseModelId ?? profile.inferenceProfileId),
470+
supportsClaudeAdaptiveThinking({ id: baseModelId ?? profile.inferenceProfileId }),
466471
input: baseModel?.input ?? ["text"],
467472
cost: baseModel?.cost ?? DEFAULT_COST,
468473
contextWindow:
469474
baseModel?.contextWindow ??
470475
resolveKnownContextWindow(baseModelId ?? profile.inferenceProfileId ?? "") ??
471476
defaults.contextWindow,
472-
maxTokens: baseModel?.maxTokens ?? defaults.maxTokens,
477+
maxTokens:
478+
baseModel?.maxTokens ??
479+
resolveKnownMaxTokens(baseModelId ?? profile.inferenceProfileId) ??
480+
defaults.maxTokens,
473481
...(baseModel?.thinkingLevelMap || knownThinkingLevelMap
474482
? { thinkingLevelMap: baseModel?.thinkingLevelMap ?? knownThinkingLevelMap }
475483
: {}),
484+
...(canonicalClaudeId.startsWith("claude-")
485+
? { params: { canonicalModelId: canonicalClaudeId } }
486+
: {}),
476487
});
477488
}
478489
return discovered;

0 commit comments

Comments
 (0)