feat(models): add Claude Mythos 5 support (#101238)

This commit is contained in:
Vincent Koc
2026-07-06 17:19:49 -07:00
committed by GitHub
parent 090c3a26d0
commit f305e707a3
39 changed files with 848 additions and 207 deletions
+13 -5
View File
@@ -185,7 +185,7 @@ OpenClaw release:
</Tab>
</Tabs>
## Thinking defaults (Claude Sonnet 5, Fable 5, 4.8, and 4.6)
## Thinking defaults (Claude Sonnet 5, Mythos 5, Fable 5, 4.8, and 4.6)
`anthropic/claude-sonnet-5` uses adaptive thinking at `high` effort by default.
Use `/think off` to disable thinking, or `/think xhigh|max` for the model's
@@ -201,6 +201,12 @@ effort. Anthropic does not allow thinking to be disabled for this model, so
omits custom temperature values for Fable 5 requests, since Anthropic rejects
a temperature override on any thinking-enabled request.
`anthropic/claude-mythos-5` is a limited-access model with the same always-on
adaptive-thinking contract. OpenClaw defaults to `high`, maps `/think off` and
`/think minimal` to `low`, and omits caller-selected sampling parameters.
The catalog publishes its 1,000,000-token context window, 128,000-token output
limit, image input, and `$10/$50` input/output pricing.
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 (Opus 4.6 and Sonnet 4.6)
@@ -414,10 +420,11 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
</Accordion>
<Accordion title="1M context window">
Claude Sonnet 5 has an exact 1,000,000-token input window and supports up to
128,000 output tokens. Anthropic's 1M context window is also GA on Claude 4.x
models with adaptive thinking: Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6.
OpenClaw sizes these models automatically, no `params.context1m` needed:
Claude Sonnet 5, Mythos 5, and Fable 5 have an exact 1,000,000-token input
window and support up to 128,000 output tokens. Anthropic's 1M context
window is also GA on Claude 4.x models with adaptive thinking: Opus 4.8,
Opus 4.7, Opus 4.6, and Sonnet 4.6. OpenClaw sizes these models
automatically, no `params.context1m` needed:
```json5
{
@@ -425,6 +432,7 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
defaults: {
models: {
"anthropic/claude-sonnet-5": {},
"anthropic/claude-mythos-5": {},
"anthropic/claude-opus-4-6": {},
},
},
+8 -2
View File
@@ -3,7 +3,7 @@ summary: "Use Amazon Bedrock Mantle OpenAI-compatible and Claude Messages models
read_when:
- You want to use Bedrock Mantle hosted OSS models with OpenClaw
- You need the Mantle OpenAI-compatible endpoint for GPT-OSS, Qwen, Kimi, or GLM
- You want to use Claude Sonnet 5 through Amazon Bedrock Mantle
- You want to use Claude Sonnet 5 or Mythos 5 through Amazon Bedrock Mantle
title: "Amazon Bedrock Mantle"
---
@@ -162,10 +162,11 @@ want to use.
</Accordion>
<Accordion title="Claude via the Anthropic Messages route">
When automatic discovery owns the model list, OpenClaw appends three Claude
When automatic discovery owns the model list, OpenClaw appends four Claude
models after a successful lookup, regardless of what `/v1/models` returns:
`amazon-bedrock-mantle/anthropic.claude-sonnet-5` (Claude Sonnet 5),
`amazon-bedrock-mantle/anthropic.claude-opus-4-7` (Claude Opus 4.7), and
`amazon-bedrock-mantle/anthropic.claude-mythos-5` (Claude Mythos 5), plus
`amazon-bedrock-mantle/anthropic.claude-mythos-preview` (Claude Mythos
Preview). They use the `anthropic-messages` API surface and stream through
the same bearer-authenticated Anthropic-compatible endpoint
@@ -177,6 +178,11 @@ want to use.
route cannot disable thinking. OpenClaw also omits custom temperature for
Sonnet 5 requests.
Claude Mythos 5 is limited access. It publishes a 1,000,000-token context
window and 128,000-token output limit, always uses adaptive thinking, maps
`/think off` and `/think minimal` to `low`, and omits caller-selected
sampling parameters.
Claude Mythos Preview always requests reasoning, defaulting to `high`
effort when no `/think` level is set (mapped from `xhigh`/`max` down to
`high`, and `minimal` up to `low`). Opus 4.7 on Mantle streams without
+14
View File
@@ -347,6 +347,20 @@ openclaw models list
</Accordion>
<Accordion title="Claude Mythos 5">
Claude Mythos 5 is available through Bedrock only for accounts with the
required limited-access approval. OpenClaw recognizes the foundation model
`anthropic.claude-mythos-5` and regional or global inference profiles such
as `us.anthropic.claude-mythos-5`.
OpenClaw applies the 1,000,000-token context window, 128,000-token output
limit, image input, prompt caching, refusal-safe streaming, and native
effort levels. Adaptive thinking is always enabled: `/think off` and
`/think minimal` map to `low`, while `xhigh` and `max` remain available.
Custom sampling and forced tool choice values are omitted.
</Accordion>
<Accordion title="Claude Sonnet 5">
AWS documents Sonnet 5 for both the
[`bedrock-runtime` and `bedrock-mantle` endpoints](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5.html).
@@ -520,7 +520,7 @@ describe("bedrock mantle discovery", () => {
expect(provider?.api).toBe("openai-completions");
expect(provider?.auth).toBe("api-key");
expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK");
expect(provider?.models).toHaveLength(4);
expect(provider?.models).toHaveLength(5);
const sonnet = provider?.models?.find((model) => model.id === "anthropic.claude-sonnet-5");
expect(sonnet).toMatchObject({
api: "anthropic-messages",
@@ -536,10 +536,20 @@ describe("bedrock mantle discovery", () => {
expect(opus?.api).toBe("anthropic-messages");
expect(opus?.reasoning).toBe(false);
expect(opus).not.toHaveProperty("baseUrl");
const mythos = provider?.models?.find(
const mythos = provider?.models?.find((model) => model.id === "anthropic.claude-mythos-5");
expect(mythos).toMatchObject({
api: "anthropic-messages",
reasoning: true,
params: { canonicalModelId: "claude-mythos-5" },
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
const mythosPreview = provider?.models?.find(
(model) => model.id === "anthropic.claude-mythos-preview",
);
expect(mythos).toMatchObject({
expect(mythosPreview).toMatchObject({
api: "anthropic-messages",
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
@@ -463,6 +463,18 @@ export async function resolveImplicitMantleProvider(params: {
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "anthropic.claude-mythos-5",
name: "Claude Mythos 5",
api: "anthropic-messages" as const,
reasoning: true,
params: { canonicalModelId: "claude-mythos-5" },
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
},
{
id: "anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
@@ -213,6 +213,40 @@ describe("createMantleAnthropicStreamFn", () => {
expect(streamOptions.effort).toBe("low");
});
it.each([
{ reasoning: undefined, effort: "high" },
{ reasoning: "off" as const, effort: "low" },
{ reasoning: "max" as const, effort: "max" },
])("maps Mythos 5 $reasoning reasoning to adaptive $effort", ({ reasoning, effort }) => {
const model = createTestModel({
id: "anthropic.claude-mythos-5",
name: "Claude Mythos 5",
reasoning: true,
params: { canonicalModelId: "claude-mythos-5" },
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
const deps = createTestDeps();
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
void createMantleAnthropicStreamFn(deps)(
model,
{ messages: [] },
{
apiKey: "bedrock-bearer-token",
maxTokens: 1_000,
temperature: 0.2,
...(reasoning ? { reasoning } : {}),
},
);
const streamOptions = firstStreamOptions(deps);
expect(streamOptions.thinkingEnabled).toBe(true);
expect(streamOptions.effort).toBe(effort);
expect(streamOptions.maxTokens).toBe(1_000);
expect(streamOptions).not.toHaveProperty("thinkingBudgetTokens");
expect(streamOptions.temperature).toBeUndefined();
});
it("normalizes Mantle provider URLs to the Anthropic endpoint", () => {
expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe(
"https://bedrock-mantle.us-east-1.api.aws/anthropic",
@@ -10,7 +10,11 @@ import {
type SimpleStreamOptions,
type ThinkingLevel,
} from "openclaw/plugin-sdk/llm";
import { resolveClaudeSonnet5ModelIdentity } from "openclaw/plugin-sdk/provider-model-shared";
import {
requiresClaudeDefaultSampling,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
} from "openclaw/plugin-sdk/provider-model-shared";
const MANTLE_ANTHROPIC_BETA = "fine-grained-tool-streaming-2025-05-14";
type AnthropicOptions = ConstructorParameters<typeof Anthropic>[0];
@@ -33,7 +37,7 @@ function isClaudeSonnet5Model(model: Model): boolean {
}
function requiresDefaultSampling(model: Model): boolean {
return model.id.includes("claude-opus-4-7") || isClaudeSonnet5Model(model);
return requiresClaudeDefaultSampling(model);
}
function isClaudeMythosPreviewModel(model: Model): boolean {
@@ -49,6 +53,14 @@ function isClaudeMythosPreviewModel(model: Model): boolean {
);
}
function isClaudeMythos5Model(model: Model): boolean {
return resolveClaudeMythos5ModelIdentity(model) !== undefined;
}
function requiresClaudeMythosAdaptiveThinking(model: Model): boolean {
return isClaudeMythos5Model(model) || isClaudeMythosPreviewModel(model);
}
function resolveMantleReasoning(
model: Model,
options: SimpleStreamOptions | undefined,
@@ -57,21 +69,19 @@ function resolveMantleReasoning(
return undefined;
}
const sonnet5 = isClaudeSonnet5Model(model);
const reasoning =
options?.reasoning ?? (isClaudeMythosPreviewModel(model) || sonnet5 ? "high" : undefined);
const mythosPreview = isClaudeMythosPreviewModel(model);
const mandatoryMythos = isClaudeMythos5Model(model) || mythosPreview;
const reasoning = options?.reasoning ?? (mandatoryMythos || sonnet5 ? "high" : undefined);
if (sonnet5) {
return reasoning === "off" || reasoning === "minimal" ? "low" : reasoning;
}
if (!isClaudeMythosPreviewModel(model)) {
if (!mandatoryMythos) {
return reasoning;
}
if (reasoning === "off") {
return "high";
}
if (reasoning === "minimal") {
if (reasoning === "off" || reasoning === "minimal") {
return "low";
}
return reasoning === "xhigh" || reasoning === "max" ? "high" : reasoning;
return mythosPreview && (reasoning === "xhigh" || reasoning === "max") ? "high" : reasoning;
}
function mapSonnet5Effort(
@@ -107,7 +117,9 @@ function buildMantleAnthropicBaseOptions(
...(requiresDefaultSampling(model) ? {} : { temperature: options?.temperature }),
maxTokens:
options?.maxTokens ||
(isClaudeSonnet5Model(model) ? model.maxTokens : Math.min(model.maxTokens, 32_000)),
(isClaudeSonnet5Model(model) || isClaudeMythos5Model(model)
? model.maxTokens
: Math.min(model.maxTokens, 32_000)),
signal: options?.signal,
apiKey,
cacheRetention: options?.cacheRetention,
@@ -172,6 +184,7 @@ export function createMantleAnthropicStreamFn(deps?: {
const streamClient = client as unknown as Anthropic;
const reasoning = resolveMantleReasoning(model, options);
const sonnet5 = isClaudeSonnet5Model(model);
const mythos5 = isClaudeMythos5Model(model);
if (!reasoning || reasoning === "off") {
return streamFn(model as Model<"anthropic-messages">, context, {
...base,
@@ -180,12 +193,12 @@ export function createMantleAnthropicStreamFn(deps?: {
});
}
if (sonnet5) {
if (sonnet5 || mythos5) {
return streamFn(model as Model<"anthropic-messages">, context, {
...base,
client: streamClient,
thinkingEnabled: true,
effort: mapSonnet5Effort(reasoning),
effort: sonnet5 ? mapSonnet5Effort(reasoning) : reasoning,
});
}
@@ -200,7 +213,7 @@ export function createMantleAnthropicStreamFn(deps?: {
client: streamClient,
maxTokens: adjusted.maxTokens,
thinkingEnabled: true,
...(isClaudeMythosPreviewModel(model) ? { effort: reasoning } : {}),
...(requiresClaudeMythosAdaptiveThinking(model) ? { effort: reasoning } : {}),
thinkingBudgetTokens: adjusted.thinkingBudget,
});
};
+8 -1
View File
@@ -41,7 +41,14 @@ export function supportsBedrockPromptCaching(modelId: string, modelName?: string
if (candidates.some((s) => s.includes("-4-"))) {
return true;
}
if (candidates.some((s) => s.includes("claude-fable-5") || s.includes("claude-sonnet-5"))) {
if (
candidates.some(
(candidate) =>
candidate.includes("claude-fable-5") ||
candidate.includes("claude-mythos-5") ||
candidate.includes("claude-sonnet-5"),
)
) {
return true;
}
if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) {
+47 -30
View File
@@ -166,37 +166,54 @@ describe("bedrock discovery", () => {
});
});
it("marks known Fable inference profile fallbacks as reasoning capable", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-fable-5",
inferenceProfileName: "US Claude Fable 5",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5",
},
],
},
],
it.each([
{
label: "Fable",
profileId: "us.anthropic.claude-fable-5",
profileName: "US Claude Fable 5",
foundationId: "anthropic.claude-fable-5",
},
{
label: "Mythos",
profileId: "us.anthropic.claude-mythos-5",
profileName: "US Claude Mythos 5",
foundationId: "anthropic.claude-mythos-5",
},
])(
"marks known $label inference profile fallbacks as reasoning capable",
async ({ profileId, profileName, foundationId }) => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: profileId,
inferenceProfileName: profileName,
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: `arn:aws:bedrock:us-east-1::foundation-model/${foundationId}`,
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: profileId,
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: "us.anthropic.claude-fable-5",
reasoning: true,
contextWindow: 1_000_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
});
},
);
it("applies the Sonnet 5 contract to inference-profile-only discovery", async () => {
sendMock.mockResolvedValueOnce({ modelSummaries: [] }).mockResolvedValueOnce({
+5
View File
@@ -21,6 +21,7 @@ import type {
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
supportsClaudeAdaptiveThinking,
} from "openclaw/plugin-sdk/provider-model-shared";
@@ -61,6 +62,7 @@ const DEFAULT_MAX_TOKENS = 4096;
const KNOWN_CONTEXT_WINDOWS: Record<string, number> = {
// Anthropic Claude
"anthropic.claude-fable-5": 1_000_000,
"anthropic.claude-mythos-5": 1_000_000,
// AWS publishes Sonnet 5 on both bedrock-runtime (Invoke/Converse) and Mantle.
"anthropic.claude-sonnet-5": 1_000_000,
"anthropic.claude-3-7-sonnet-20250219-v1:0": 200_000,
@@ -142,6 +144,7 @@ function resolveKnownContextWindow(modelId: string): number | undefined {
for (const candidate of candidates) {
if (
resolveClaudeFable5ModelIdentity({ id: candidate }) ||
resolveClaudeMythos5ModelIdentity({ id: candidate }) ||
resolveClaudeSonnet5ModelIdentity({ id: candidate })
) {
return 1_000_000;
@@ -178,6 +181,7 @@ function resolveKnownThinkingLevelMap(
function resolveKnownMaxTokens(modelId: string): number | undefined {
return resolveClaudeFable5ModelIdentity({ id: modelId }) ||
resolveClaudeMythos5ModelIdentity({ id: modelId }) ||
resolveClaudeSonnet5ModelIdentity({ id: modelId })
? 128_000
: undefined;
@@ -185,6 +189,7 @@ function resolveKnownMaxTokens(modelId: string): number | undefined {
function resolveKnownInput(modelId: string): ModelDefinitionConfig["input"] | undefined {
return resolveClaudeFable5ModelIdentity({ id: modelId }) ||
resolveClaudeMythos5ModelIdentity({ id: modelId }) ||
resolveClaudeSonnet5ModelIdentity({ id: modelId })
? ["text", "image"]
: undefined;
+6 -2
View File
@@ -377,13 +377,16 @@ describe("amazon-bedrock provider plugin", () => {
}
});
it("keeps Claude Fable 5 always adaptive with high default effort", async () => {
it("keeps mandatory-adaptive Claude 5 models at high default effort", async () => {
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
for (const modelId of [
"anthropic.claude-fable-5",
"us.anthropic.claude-fable-5",
"global.anthropic.claude-fable-5",
"anthropic.claude-mythos-5",
"us.anthropic.claude-mythos-5",
"global.anthropic.claude-mythos-5",
]) {
expectThinkingProfile(
provider.resolveThinkingProfile?.({
@@ -414,8 +417,9 @@ describe("amazon-bedrock provider plugin", () => {
);
});
it("recognizes direct Fable model refs as prompt-cache eligible", () => {
it("recognizes direct Claude 5 model refs as prompt-cache eligible", () => {
expect(supportsBedrockPromptCaching("us.anthropic.claude-fable-5")).toBe(true);
expect(supportsBedrockPromptCaching("us.anthropic.claude-mythos-5")).toBe(true);
expect(supportsBedrockPromptCaching("global.anthropic.claude-sonnet-5")).toBe(true);
});
@@ -73,6 +73,11 @@ describe("amazon-bedrock provider-policy-api", () => {
defaultLevel: "high",
preservesCatalogOptOut: true,
},
{
canonicalModelId: "claude-mythos-5",
defaultLevel: "high",
preservesCatalogOptOut: true,
},
{
canonicalModelId: "claude-opus-4-8",
defaultLevel: "off",
@@ -15,6 +15,7 @@ import {
normalizeProviderId,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
} from "openclaw/plugin-sdk/provider-model-shared";
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
@@ -57,7 +58,8 @@ function normalizeBedrockResolvedModel({ modelId, model }: ProviderNormalizeReso
}
const reasoning =
model.reasoning ||
resolveClaudeFable5ModelIdentity({ id: modelId, params: model.params }) !== undefined;
resolveClaudeFable5ModelIdentity({ id: modelId, params: model.params }) !== undefined ||
resolveClaudeMythos5ModelIdentity({ id: modelId, params: model.params }) !== undefined;
const current = model.thinkingLevelMap;
const currentEfforts = current as Record<string, string | null | undefined> | undefined;
if (
@@ -254,23 +254,47 @@ describe("Bedrock thinking effort mapping", () => {
expect(options.maxTokens).toBe(32_000);
});
it("forces adaptive thinking for Bedrock Mythos Preview when callers omit reasoning", () => {
const model = bedrockModel({
id: "us.anthropic.claude-mythos-preview",
name: "US Claude Mythos Preview",
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 128_000,
});
const options = testing.resolveSimpleBedrockOptions(model, {});
it.each(["claude-mythos-preview", "claude-mythos-5"])(
"forces adaptive thinking for Bedrock %s when callers omit reasoning",
(modelId) => {
const model = bedrockModel({
id: `us.anthropic.${modelId}`,
name: modelId,
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 128_000,
});
const options = testing.resolveSimpleBedrockOptions(model, {});
expect(options.reasoning).toBe("high");
expect(options.maxTokens).toBe(128_000);
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "high" },
});
});
expect(options.reasoning).toBe("high");
expect(options.maxTokens).toBe(128_000);
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "high" },
});
},
);
it.each(["claude-mythos-preview", "claude-mythos-5"])(
"maps explicit off to low effort for Bedrock %s",
(modelId) => {
const model = bedrockModel({
id: `us.anthropic.${modelId}`,
name: modelId,
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 128_000,
});
const options = testing.resolveSimpleBedrockOptions(model, { reasoning: "off" });
expect(options.reasoning).toBe("low");
expect(options.maxTokens).toBe(128_000);
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "low" },
});
},
);
it("clamps max effort for Claude models without native max support", () => {
expect(
@@ -308,6 +332,18 @@ describe("Bedrock thinking effort mapping", () => {
).toBe("max");
});
it("preserves max effort for Claude Mythos 5", () => {
expect(
testing.mapThinkingLevelToEffort(
bedrockModel({
id: "anthropic.claude-mythos-5",
name: "Claude Mythos 5",
}),
"max",
),
).toBe("max");
});
it("uses canonical Claude policy for deployment aliases", () => {
expect(
testing.mapThinkingLevelToEffort(
@@ -422,7 +458,20 @@ describe("Bedrock Fable contract", () => {
expect(command.input?.toolConfig).toBeUndefined();
});
it("quarantines partial output when Fable returns a terminal refusal", async () => {
it.each([
["Fable", () => fableModel()],
[
"Mythos 5",
() =>
bedrockModel({
id: "production-mythos",
name: "Production deployment",
params: { canonicalModelId: "claude-mythos-5" },
contextWindow: 1_000_000,
maxTokens: 128_000,
}),
],
])("quarantines partial output when %s returns a terminal refusal", async (_name, model) => {
vi.spyOn(BedrockRuntimeClient.prototype, "send").mockResolvedValue({
$metadata: { httpStatusCode: 200 },
stream: streamEvents([
@@ -446,7 +495,7 @@ describe("Bedrock Fable contract", () => {
]),
} as never);
const stream = streamSimpleBedrock(fableModel(), context());
const stream = streamSimpleBedrock(model(), context());
const eventTypes: string[] = [];
for await (const event of stream) {
eventTypes.push(event.type);
+15 -10
View File
@@ -55,7 +55,9 @@ import {
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
requiresClaudeMandatoryAdaptiveThinking,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeXhighEffort,
} from "openclaw/plugin-sdk/provider-model-shared";
@@ -81,7 +83,11 @@ function usesClaudeSonnet5BedrockContract(model: Model<"bedrock-converse-stream"
function usesClaudeStreamingRefusalBedrockContract(
model: Model<"bedrock-converse-stream">,
): boolean {
return usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model);
return (
usesClaudeFable5BedrockContract(model) ||
resolveClaudeMythos5ModelIdentity(model) !== undefined ||
usesClaudeSonnet5BedrockContract(model)
);
}
function readBedrockStopDetails(fields: DocumentType | undefined): unknown {
@@ -387,7 +393,7 @@ function resolveSimpleBedrockOptions(
options?: SimpleStreamOptions,
): BedrockOptions {
const base = buildBaseOptions(model, options, undefined);
if (usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model)) {
if (requiresMandatoryAdaptiveThinking(model)) {
return {
...base,
maxTokens: resolveAdaptiveBedrockMaxTokens(model, base.maxTokens),
@@ -600,7 +606,7 @@ function resolveClaudeProfileNameModelId(modelName?: string): string | undefined
if (!normalized.includes("claude")) {
return undefined;
}
const family = /(?:fable-5|mythos-preview|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?:$|-)/.exec(
const family = /(?:fable-5|mythos-(?:5|preview)|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?:$|-)/.exec(
normalized,
)?.[0];
return family ? `claude-${family.replace(/-$/, "")}` : undefined;
@@ -631,6 +637,8 @@ function supportsAdaptiveThinking(model: Model<"bedrock-converse-stream">): bool
function requiresMandatoryAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean {
const profileModelId = resolveClaudeProfileNameModelId(model.name);
return (
requiresClaudeMandatoryAdaptiveThinking(model) ||
requiresClaudeMandatoryAdaptiveThinking({ id: profileModelId }) ||
isClaudeMythosPreviewModelId(resolveClaudeModelIdentity(model)) ||
isClaudeMythosPreviewModelId(profileModelId) ||
usesClaudeSonnet5BedrockContract(model) ||
@@ -1060,17 +1068,14 @@ function buildAdditionalModelRequestFields(
model: Model<"bedrock-converse-stream">,
options: BedrockOptions,
): DocumentType | undefined {
// Bedrock keeps Fable/Sonnet 5 adaptive. Preserve the public `off` control by
// Mandatory-adaptive Claude routes preserve the public `off` control by
// lowering effort instead of silently falling back to the route's high default.
const mandatoryAdaptiveThinking =
usesClaudeFable5BedrockContract(model) || requiresMandatoryAdaptiveThinking(model);
const mandatoryAdaptiveThinking = requiresMandatoryAdaptiveThinking(model);
const reasoning =
options.reasoning === "off"
? usesClaudeFable5BedrockContract(model) || usesClaudeSonnet5BedrockContract(model)
? mandatoryAdaptiveThinking
? "low"
: mandatoryAdaptiveThinking
? "high"
: "off"
: "off"
: (options.reasoning ?? (mandatoryAdaptiveThinking ? "high" : undefined));
if (reasoning === "off") {
return undefined;
+5 -1
View File
@@ -9,6 +9,7 @@ import type {
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
} from "openclaw/plugin-sdk/provider-model-shared";
@@ -59,6 +60,7 @@ export function isLatestAdaptiveBedrockModelRef(
const canonicalModelId = resolveClaudeModelIdentity(modelRef);
return (
resolveClaudeFable5ModelIdentity(modelRef) !== undefined ||
resolveClaudeMythos5ModelIdentity(modelRef) !== undefined ||
resolveClaudeSonnet5ModelIdentity(modelRef) !== undefined ||
[modelId, canonicalModelId].some(
(candidate) =>
@@ -74,6 +76,7 @@ export function supportsBedrockNativeMaxEffort(
): boolean {
if (
resolveClaudeFable5ModelIdentity({ id: modelId, params }) ||
resolveClaudeMythos5ModelIdentity({ id: modelId, params }) ||
resolveClaudeSonnet5ModelIdentity({ id: modelId, params })
) {
return true;
@@ -90,7 +93,7 @@ export function resolveBedrockNativeThinkingLevelMap(
params?: Record<string, unknown>,
): ProviderRuntimeModel["thinkingLevelMap"] | undefined {
const modelRef = { id: modelId, params };
if (resolveClaudeFable5ModelIdentity(modelRef)) {
if (resolveClaudeFable5ModelIdentity(modelRef) || resolveClaudeMythos5ModelIdentity(modelRef)) {
return { off: "low", minimal: "low", xhigh: "xhigh", max: "max" };
}
if (resolveClaudeSonnet5ModelIdentity(modelRef)) {
@@ -116,6 +119,7 @@ export function resolveBedrockClaudeThinkingProfile(
const modelRefs = [trimmed, canonicalModelId];
if (
resolveClaudeFable5ModelIdentity({ id: trimmed, params }) ||
resolveClaudeMythos5ModelIdentity({ id: trimmed, params }) ||
resolveClaudeSonnet5ModelIdentity({ id: trimmed, params })
) {
return {
+42 -2
View File
@@ -80,6 +80,7 @@ describe("anthropic-vertex provider plugin", () => {
expect(result.provider.headers).toEqual({ "x-test-header": "1" });
expect(result.provider.models.map((model) => model.id)).toEqual([
"claude-fable-5",
"claude-mythos-5",
"claude-opus-4-8",
"claude-opus-4-6",
"claude-sonnet-4-6",
@@ -90,9 +91,15 @@ describe("anthropic-vertex provider plugin", () => {
xhigh: "xhigh",
max: "max",
});
expect(result.provider.models[1]?.thinkingLevelMap).toEqual({ xhigh: "xhigh", max: "max" });
expect(result.provider.models[2]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
expect(result.provider.models[1]?.thinkingLevelMap).toEqual({
off: "low",
minimal: "low",
xhigh: "xhigh",
max: "max",
});
expect(result.provider.models[2]?.thinkingLevelMap).toEqual({ xhigh: "xhigh", max: "max" });
expect(result.provider.models[3]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
expect(result.provider.models[4]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
});
it.each(["global", "us", "eu"])("publishes Sonnet 5 for the %s endpoint", (region) => {
@@ -289,6 +296,39 @@ describe("anthropic-vertex provider plugin", () => {
});
});
it("restores Mythos 5 metadata for explicit Vertex catalog rows", async () => {
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
const normalized = provider.normalizeResolvedModel?.({
provider: "anthropic-vertex",
modelId: "claude-mythos-5",
model: {
id: "claude-mythos-5",
name: "Claude Mythos 5",
api: "anthropic-messages",
provider: "anthropic-vertex",
baseUrl: "https://aiplatform.googleapis.com",
reasoning: false,
input: ["text"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 200_000,
maxTokens: 8192,
},
} as never);
expect(normalized).toMatchObject({
reasoning: true,
input: ["text", "image"],
contextWindow: 1_000_000,
contextTokens: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: {
off: "low",
minimal: "low",
xhigh: "xhigh",
max: "max",
},
});
});
it("resolves synthetic auth when ADC is available", async () => {
hasAnthropicVertexAvailableAuthMock.mockReturnValue(true);
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
@@ -9,6 +9,7 @@ import type {
} from "openclaw/plugin-sdk/provider-model-shared";
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -16,7 +17,7 @@ import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexRegion } from
/** Default Anthropic Vertex model used for implicit provider catalogs. */
export const ANTHROPIC_VERTEX_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000;
const ANTHROPIC_VERTEX_FABLE_MAX_TOKENS = 128_000;
const ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS = 128_000;
// Vertex's introductory rate expires at the documented UTC month boundary.
const SONNET_5_STANDARD_PRICING_START_MS = Date.UTC(2026, 8, 1);
const SONNET_5_SUPPORTED_REGIONS = new Set(["global", "us", "eu"]);
@@ -96,7 +97,16 @@ function buildAnthropicVertexCatalog(region: string, nowMs: number): ModelDefini
reasoning: true,
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
maxTokens: ANTHROPIC_VERTEX_FABLE_MAX_TOKENS,
maxTokens: ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
}),
buildAnthropicVertexModel({
id: "claude-mythos-5",
name: "Claude Mythos 5",
reasoning: true,
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
maxTokens: ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
}),
...sonnet5,
@@ -137,15 +147,16 @@ export function normalizeAnthropicVertexResolvedModel(
): ProviderRuntimeModel | undefined {
const ref = { id: modelId, params: model.params };
const fable5 = resolveClaudeFable5ModelIdentity(ref) !== undefined;
const mythos5 = resolveClaudeMythos5ModelIdentity(ref) !== undefined;
const sonnet5 = resolveClaudeSonnet5ModelIdentity(ref) !== undefined;
if (!fable5 && !sonnet5) {
if (!fable5 && !mythos5 && !sonnet5) {
return undefined;
}
const input: ProviderRuntimeModel["input"] = model.input.includes("image")
? model.input
: [...model.input, "image"];
const nativeThinkingLevelMap = {
...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}),
...(fable5 || mythos5 ? { off: "low" as const, minimal: "low" as const } : {}),
xhigh: "xhigh",
max: "max",
};
@@ -156,7 +167,8 @@ export function normalizeAnthropicVertexResolvedModel(
const nativeThinkingLevelsMatch =
model.thinkingLevelMap?.xhigh === "xhigh" &&
model.thinkingLevelMap.max === "max" &&
(!fable5 || (model.thinkingLevelMap.off === "low" && model.thinkingLevelMap.minimal === "low"));
(!(fable5 || mythos5) ||
(model.thinkingLevelMap.off === "low" && model.thinkingLevelMap.minimal === "low"));
const cost = sonnet5
? resolveSonnet5Cost(resolveAnthropicVertexClientRegion({ baseUrl: model.baseUrl }))
: undefined;
@@ -171,7 +183,7 @@ export function normalizeAnthropicVertexResolvedModel(
input === model.input &&
model.contextWindow === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW &&
model.contextTokens === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW &&
(model.maxTokens ?? 0) >= ANTHROPIC_VERTEX_FABLE_MAX_TOKENS &&
(model.maxTokens ?? 0) >= ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS &&
nativeThinkingLevelsMatch &&
costMatches
) {
@@ -183,7 +195,7 @@ export function normalizeAnthropicVertexResolvedModel(
input,
contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
contextTokens: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
maxTokens: Math.max(model.maxTokens ?? 0, ANTHROPIC_VERTEX_FABLE_MAX_TOKENS),
maxTokens: Math.max(model.maxTokens ?? 0, ANTHROPIC_VERTEX_CLAUDE_5_MAX_TOKENS),
thinkingLevelMap,
...(cost ? { cost } : {}),
};
@@ -32,16 +32,19 @@ describe("anthropic-vertex provider-policy-api", () => {
expect(profile?.levels.map((level) => level.id)).not.toContain("xhigh");
});
it("inherits Claude Fable 5's provider-agnostic thinking contract", () => {
const profile = resolveThinkingProfile({
provider: "anthropic-vertex",
modelId: "claude-fable-5",
});
it.each(["claude-fable-5", "claude-mythos-5"])(
"inherits %s's provider-agnostic thinking contract",
(modelId) => {
const profile = resolveThinkingProfile({
provider: "anthropic-vertex",
modelId,
});
expect(profile?.defaultLevel).toBe("high");
expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true);
expect(profile?.levels.map((level) => level.id)).toContain("max");
});
expect(profile?.defaultLevel).toBe("high");
expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true);
expect(profile?.levels.map((level) => level.id)).toContain("max");
},
);
it("resolves deployment aliases from canonical model metadata", () => {
const profile = resolveThinkingProfile({
@@ -14,7 +14,9 @@ import {
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
requiresClaudeMandatoryAdaptiveThinking,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
@@ -60,11 +62,11 @@ function isClaudeSonnet5Model(modelId: string): boolean {
}
function isClaudeMythos5Model(modelId: string): boolean {
return /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId }));
return resolveClaudeMythos5ModelIdentity({ id: modelId }) !== undefined;
}
function supportsAdaptiveThinking(modelId: string): boolean {
return supportsClaudeAdaptiveThinking({ id: modelId }) || isClaudeMythos5Model(modelId);
return supportsClaudeAdaptiveThinking({ id: modelId });
}
function mapAnthropicAdaptiveEffort(
@@ -153,15 +155,14 @@ export function createAnthropicVertexStreamFn(
requestedMaxTokens: options?.maxTokens,
});
const contractModelId = resolveClaudeModelIdentity(model);
const fable5 = isClaudeFable5Model(contractModelId);
const sonnet5 = isClaudeSonnet5Model(contractModelId);
const mandatoryAdaptiveThinking = fable5 || isClaudeMythos5Model(contractModelId);
const mandatoryAdaptiveThinking = requiresClaudeMandatoryAdaptiveThinking({
id: contractModelId,
});
const requestedReasoning = options?.reasoning;
const reasoning =
requestedReasoning === "off" && mandatoryAdaptiveThinking
? fable5
? "low"
: "high"
? "low"
: (requestedReasoning ?? (mandatoryAdaptiveThinking || sonnet5 ? "high" : undefined));
const adaptiveThinking =
mandatoryAdaptiveThinking ||
@@ -206,7 +207,7 @@ export function createAnthropicVertexStreamFn(
? budgets[reasoning as keyof typeof budgets]
: undefined) ?? 10000;
}
} else if (fable5) {
} else if (mandatoryAdaptiveThinking) {
opts.thinkingEnabled = true;
opts.effort = "high";
} else {
+52
View File
@@ -696,6 +696,58 @@ describe("anthropic provider replay hooks", () => {
});
});
it("resolves Claude Mythos 5 with its direct-only mandatory-adaptive contract", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const resolved = provider.resolveDynamicModel?.({
provider: "anthropic",
modelId: "claude-mythos-5",
modelRegistry: createModelRegistry([]),
} as ProviderResolveDynamicModelContext);
expectFields(resolved, {
provider: "anthropic",
id: "claude-mythos-5",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 1_000_000,
contextTokens: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: {
off: "low",
minimal: "low",
xhigh: "xhigh",
max: "max",
},
});
expect(requireRecord(resolved, "Mythos model").mediaInput).toEqual({
image: { maxSidePx: 2576, preferredSidePx: 2576, tokenMode: "provider" },
});
expect(
provider.resolveThinkingProfile?.({
provider: "anthropic",
modelId: "claude-mythos-5",
} as never)?.defaultLevel,
).toBe("high");
expect(
provider.resolveDynamicModel?.({
provider: "claude-cli",
modelId: "claude-mythos-5",
modelRegistry: createModelRegistry([]),
} as ProviderResolveDynamicModelContext),
).toBeUndefined();
expect(
provider.resolveThinkingProfile?.({
provider: "claude-cli",
modelId: "claude-mythos-5",
} as never),
).toEqual({
levels: [{ id: "off" }],
defaultLevel: "off",
});
});
it("rolls Claude Sonnet 5 to standard pricing on September 1, 2026", async () => {
vi.useFakeTimers();
vi.setSystemTime(Date.UTC(2026, 8, 1));
+19
View File
@@ -110,6 +110,24 @@
"max": "max"
}
},
{
"id": "claude-mythos-5",
"name": "Claude Mythos 5",
"reasoning": true,
"input": ["text", "image"],
"mediaInput": {
"image": { "maxSidePx": 2576, "preferredSidePx": 2576, "tokenMode": "provider" }
},
"cost": { "input": 10, "output": 50, "cacheRead": 1, "cacheWrite": 12.5 },
"contextWindow": 1000000,
"maxTokens": 128000,
"thinkingLevelMap": {
"off": "low",
"minimal": "low",
"xhigh": "xhigh",
"max": "max"
}
},
{
"id": "claude-opus-4-8",
"name": "Claude Opus 4.8",
@@ -194,6 +212,7 @@
"opus-4.8": "claude-opus-4-8",
"opus": "claude-opus-4-8",
"opus-4.6": "claude-opus-4-6",
"mythos-5": "claude-mythos-5",
"sonnet-5": "claude-sonnet-5",
"sonnet": "claude-sonnet-5",
"sonnet-4.6": "claude-sonnet-4-6"
@@ -136,36 +136,39 @@ describe("anthropic provider policy public artifact", () => {
expect(profile?.defaultLevel).toBe("off");
});
it("exposes the always-adaptive Claude Fable 5 thinking profile", () => {
const profile = resolveThinkingProfile({
provider: "anthropic",
modelId: "claude-fable-5",
});
it.each(["claude-fable-5", "claude-mythos-5"])(
"exposes the mandatory-adaptive %s thinking profile",
(modelId) => {
const profile = resolveThinkingProfile({
provider: "anthropic",
modelId,
});
expect(profile).toEqual({
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
{ id: "xhigh" },
{ id: "adaptive" },
{ id: "max" },
],
defaultLevel: "high",
preserveWhenCatalogReasoningFalse: true,
});
expect(
resolveThinkingProfile({
provider: "claude-cli",
modelId: "claude-fable-5",
}),
).toEqual({
levels: [{ id: "off" }],
defaultLevel: "off",
});
});
expect(profile).toEqual({
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
{ id: "xhigh" },
{ id: "adaptive" },
{ id: "max" },
],
defaultLevel: "high",
preserveWhenCatalogReasoningFalse: true,
});
expect(
resolveThinkingProfile({
provider: "claude-cli",
modelId,
}),
).toEqual({
levels: [{ id: "off" }],
defaultLevel: "off",
});
},
);
it("does not return fable-5 off-thinking profile for claude-fable-50 (prefix boundary check)", () => {
const profile = resolveThinkingProfile({
@@ -177,6 +180,16 @@ describe("anthropic provider policy public artifact", () => {
expect(profile?.defaultLevel).not.toBe("off");
});
it("preserves the existing Claude CLI Mythos Preview thinking profile", () => {
const profile = resolveThinkingProfile({
provider: "claude-cli",
modelId: "claude-mythos-preview",
});
expect(profile?.defaultLevel).toBe("adaptive");
expect(profile?.levels.map((level) => level.id)).toContain("max");
});
it("exposes native max without xhigh for direct Claude 4.6 routes", () => {
for (const provider of ["anthropic", "claude-cli"]) {
const profile = resolveThinkingProfile({
+6 -2
View File
@@ -3,8 +3,9 @@
* path for config defaults and thinking profiles.
*/
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeFable5ModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeThinkingProfile,
} from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
@@ -40,7 +41,10 @@ export function resolveThinkingProfile(params: {
includeNativeMax: true,
});
case "claude-cli":
if (resolveClaudeFable5ModelIdentity({ id: contractModelId })) {
if (
resolveClaudeFable5ModelIdentity({ id: contractModelId }) ||
resolveClaudeMythos5ModelIdentity({ id: contractModelId })
) {
return CLAUDE_CLI_OFF_THINKING_PROFILE;
}
return resolveClaudeThinkingProfile(contractModelId, undefined, {
+31 -13
View File
@@ -30,6 +30,7 @@ import {
type ProviderPlugin,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
resolveClaudeThinkingProfile,
supportsClaudeAdaptiveThinking,
@@ -292,7 +293,7 @@ function buildAnthropicForwardCompatModel(
if (trimmedModelId !== lower || !matchesAnthropicModernModel(lower)) {
return undefined;
}
if (isAnthropicFable5Model(lower) && normalizedProvider !== PROVIDER_ID) {
if (isAnthropicMandatoryClaude5Model(lower) && normalizedProvider !== PROVIDER_ID) {
return undefined;
}
const provider =
@@ -305,7 +306,7 @@ function buildAnthropicForwardCompatModel(
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: isAnthropicFable5Model(trimmedModelId)
cost: isAnthropicMandatoryClaude5Model(trimmedModelId)
? { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }
: isAnthropicSonnet5Model(trimmedModelId) && provider === PROVIDER_ID
? resolveAnthropicSonnet5Cost()
@@ -315,7 +316,15 @@ function buildAnthropicForwardCompatModel(
? ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS
: 64_000,
...(supportsClaudeNativeXhighEffort({ id: trimmedModelId })
? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } }
? {
thinkingLevelMap: {
...(isAnthropicMandatoryClaude5Model(trimmedModelId)
? { off: "low" as const, minimal: "low" as const }
: {}),
xhigh: "xhigh",
max: "max",
},
}
: supportsAnthropicNativeMaxEffort(trimmedModelId)
? { thinkingLevelMap: { max: "max" } }
: {}),
@@ -370,19 +379,27 @@ function isAnthropicFable5Model(modelId: string): boolean {
return resolveClaudeFable5ModelIdentity({ id: modelId }) !== undefined;
}
function isAnthropicMythos5Model(modelId: string): boolean {
return resolveClaudeMythos5ModelIdentity({ id: modelId }) !== undefined;
}
function isAnthropicMandatoryClaude5Model(modelId: string): boolean {
return isAnthropicFable5Model(modelId) || isAnthropicMythos5Model(modelId);
}
function isAnthropicSonnet5Model(modelId: string): boolean {
return resolveClaudeSonnet5ModelIdentity({ id: modelId }) !== undefined;
}
function resolveAnthropicFixedContextWindow(modelId: string): number | undefined {
if (isAnthropicFable5Model(modelId) || isAnthropicSonnet5Model(modelId)) {
if (isAnthropicMandatoryClaude5Model(modelId) || isAnthropicSonnet5Model(modelId)) {
return ANTHROPIC_EXACT_1M_CONTEXT_TOKENS;
}
return isAnthropicGa1MModel(modelId) ? ANTHROPIC_GA_1M_CONTEXT_TOKENS : undefined;
}
function isAnthropic128kOutputModel(modelId: string): boolean {
if (isAnthropicFable5Model(modelId) || isAnthropicSonnet5Model(modelId)) {
if (isAnthropicMandatoryClaude5Model(modelId) || isAnthropicSonnet5Model(modelId)) {
return true;
}
return /^claude-opus-4-8(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId }));
@@ -453,7 +470,7 @@ function applyAnthropicFixedContextWindow(params: {
return undefined;
}
const exactContextWindow =
isAnthropicFable5Model(params.contractModelId) ||
isAnthropicMandatoryClaude5Model(params.contractModelId) ||
isAnthropicSonnet5Model(params.contractModelId);
const nextContextWindow = exactContextWindow
? fixedContextWindow
@@ -496,8 +513,8 @@ function applyAnthropicThinkingLevelMap(params: {
modelId: string;
model: ProviderRuntimeModel;
}): ProviderRuntimeModel | undefined {
const fable5 = isAnthropicFable5Model(params.modelId);
const nativeXhigh = fable5 || supportsClaudeNativeXhighEffort({ id: params.modelId });
const mandatoryClaude5 = isAnthropicMandatoryClaude5Model(params.modelId);
const nativeXhigh = mandatoryClaude5 || supportsClaudeNativeXhighEffort({ id: params.modelId });
if (!supportsAnthropicNativeMaxEffort(params.modelId)) {
return undefined;
}
@@ -505,7 +522,7 @@ function applyAnthropicThinkingLevelMap(params: {
const nativeDefaults = isAnthropicMythosPreviewModel(params.modelId)
? { max: "max" as const }
: {
...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}),
...(mandatoryClaude5 ? { off: "low" as const, minimal: "low" as const } : {}),
xhigh: nativeXhigh ? ("xhigh" as const) : null,
max: "max" as const,
};
@@ -594,13 +611,14 @@ function normalizeAnthropicResolvedModel(
params: ctx.model.params,
});
if (
isAnthropicFable5Model(contractModelId) &&
isAnthropicMandatoryClaude5Model(contractModelId) &&
normalizeLowercaseStringOrEmpty(ctx.provider) !== PROVIDER_ID
) {
return undefined;
}
const contractModel =
(isAnthropicFable5Model(contractModelId) || isAnthropicSonnet5Model(contractModelId)) &&
(isAnthropicMandatoryClaude5Model(contractModelId) ||
isAnthropicSonnet5Model(contractModelId)) &&
!ctx.model.reasoning
? { ...ctx.model, reasoning: true }
: ctx.model;
@@ -877,12 +895,12 @@ export function buildAnthropicProvider(): ProviderPlugin {
...NATIVE_ANTHROPIC_REPLAY_HOOKS,
isModernModelRef: ({ provider, modelId }) =>
matchesAnthropicModernModel(modelId) &&
(!isAnthropicFable5Model(modelId) ||
(!isAnthropicMandatoryClaude5Model(modelId) ||
normalizeLowercaseStringOrEmpty(provider) === PROVIDER_ID),
resolveReasoningOutputMode: () => "native",
resolveThinkingProfile: ({ provider, modelId, params }) => {
const contractModelId = resolveClaudeModelIdentity({ id: modelId, params });
return isAnthropicFable5Model(contractModelId) &&
return isAnthropicMandatoryClaude5Model(contractModelId) &&
normalizeLowercaseStringOrEmpty(provider) !== PROVIDER_ID
? CLAUDE_CLI_OFF_THINKING_PROFILE
: resolveClaudeThinkingProfile(contractModelId, undefined, {
+4 -4
View File
@@ -1,7 +1,7 @@
// Provides model selection, usage, and thinking-level utility helpers.
import {
resolveClaudeFable5ModelIdentity,
resolveClaudeNativeThinkingLevelMap,
requiresClaudeMandatoryAdaptiveThinking,
} from "@openclaw/llm-core";
import type { Api, Model, ModelThinkingLevel, Usage } from "./types.js";
@@ -36,9 +36,9 @@ function resolveThinkingLevelMap<TApi extends Api>(model: Model<TApi>) {
export function getSupportedThinkingLevels<TApi extends Api>(
model: Model<TApi>,
): ModelThinkingLevel[] {
const fableContract =
model.api === "anthropic-messages" && resolveClaudeFable5ModelIdentity(model) !== undefined;
if (!model.reasoning && !fableContract) {
const mandatoryAdaptiveContract =
model.api === "anthropic-messages" && requiresClaudeMandatoryAdaptiveThinking(model);
if (!model.reasoning && !mandatoryAdaptiveContract) {
return ["off"];
}
const thinkingLevelMap = resolveThinkingLevelMap(model);
@@ -1,14 +1,19 @@
// Model-bound thinking cannot be exposed or replayed after a model switch.
import {
requiresClaudeDefaultSampling,
requiresClaudeMandatoryAdaptiveThinking,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
} from "@openclaw/llm-core";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { Context, Model } from "../types.js";
export {
requiresClaudeDefaultSampling,
requiresClaudeMandatoryAdaptiveThinking,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeNativeThinkingLevelMap,
resolveClaudeSonnet5ModelIdentity,
supportsClaudeAdaptiveThinking,
@@ -66,6 +71,7 @@ export function usesClaudeStreamingRefusalContract(model: {
}
return (
resolveClaudeFable5ModelIdentity(model) !== undefined ||
resolveClaudeMythos5ModelIdentity(model) !== undefined ||
resolveClaudeSonnet5ModelIdentity(model) !== undefined
);
}
@@ -78,11 +84,7 @@ export function requiresClaudeAdaptiveThinking(model: {
if (normalizeApi(model.api) !== "anthropic-messages") {
return false;
}
const modelId = resolveClaudeModelIdentity(model);
return (
resolveClaudeFable5ModelIdentity(model) !== undefined ||
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId)
);
return requiresClaudeMandatoryAdaptiveThinking(model);
}
/** Return whether omitted thinking should default to adaptive/high. */
@@ -120,14 +122,46 @@ export function prepareClaudeSonnet5RequestContext(model: Model, context: Contex
: { ...context, messages: context.messages.slice(0, end) };
}
function resolveReplayFableIdentity(ref: ReplayModelRef): string | undefined {
export function applyClaudeRequestContract(
params: Record<string, unknown>,
model: {
id?: string;
params?: Record<string, unknown>;
api?: string;
},
): void {
if (normalizeApi(model.api) !== "anthropic-messages") {
return;
}
const sonnet5 = resolveClaudeSonnet5ModelIdentity(model) !== undefined;
if (!requiresClaudeDefaultSampling(model) && !sonnet5) {
return;
}
delete params.temperature;
delete params.top_p;
delete params.top_k;
if (sonnet5) {
delete params.service_tier;
}
}
function resolveReplayModelBoundIdentity(ref: ReplayModelRef): string | undefined {
if (normalizeApi(ref.api) !== "anthropic-messages") {
return undefined;
}
if (hasConcreteResponseModel(ref)) {
return resolveClaudeFable5ModelIdentity({ id: ref.responseModelId });
const modelRef = hasConcreteResponseModel(ref)
? { id: ref.responseModelId }
: { id: ref.modelId, params: ref.modelParams };
const fableIdentity = resolveClaudeFable5ModelIdentity(modelRef);
if (fableIdentity) {
return `fable:${fableIdentity}`;
}
return resolveClaudeFable5ModelIdentity({ id: ref.modelId, params: ref.modelParams });
const mythosIdentity = resolveClaudeMythos5ModelIdentity(modelRef);
if (mythosIdentity) {
return `mythos:${mythosIdentity}`;
}
const sonnetIdentity = resolveClaudeSonnet5ModelIdentity(modelRef);
return sonnetIdentity ? `sonnet:${sonnetIdentity}` : undefined;
}
export function resolveModelBoundThinkingReplayMode(params: {
@@ -136,8 +170,8 @@ export function resolveModelBoundThinkingReplayMode(params: {
}): "default" | "preserve" | "drop" {
const sourceApi = normalizeApi(params.source.api);
const targetApi = normalizeApi(params.target.api);
const sourceIdentity = resolveReplayFableIdentity(params.source);
const targetIdentity = resolveReplayFableIdentity(params.target);
const sourceIdentity = resolveReplayModelBoundIdentity(params.source);
const targetIdentity = resolveReplayModelBoundIdentity(params.target);
const sameRoute =
normalizeLowercaseStringOrEmpty(params.source.provider) ===
normalizeLowercaseStringOrEmpty(params.target.provider) &&
@@ -862,6 +862,8 @@ describe("Anthropic provider", () => {
it.each([
["claude-fable-5", "Claude Fable 5", "anthropic", "sk-ant-provider"],
["claude-mythos-5", "Claude Mythos 5", "anthropic", "sk-ant-provider"],
["claude-mythos-5", "Claude Mythos 5", "anthropic-vertex", "vertex-token"],
["claude-sonnet-5", "Claude Sonnet 5", "anthropic", "sk-ant-provider"],
["claude-sonnet-5", "Claude Sonnet 5", "anthropic-vertex", "vertex-token"],
])("surfaces structured %s streaming refusals for %s", async (id, name, provider, apiKey) => {
@@ -1424,6 +1426,47 @@ describe("Anthropic provider", () => {
expect(capturedPayload).not.toHaveProperty("temperature");
});
it("uses mandatory adaptive thinking and default sampling for Claude Mythos 5", async () => {
let capturedPayload: unknown;
const stream = streamSimpleAnthropic(
makeAnthropicModel({
id: "prod-mythos",
name: "Production Claude",
provider: "microsoft-foundry",
params: { canonicalModelId: "claude-mythos-5" },
reasoning: false,
baseUrl: "https://example.services.ai.azure.com/anthropic",
maxTokens: 128_000,
}),
{
messages: [{ role: "user", content: "hello", timestamp: 0 }],
},
{
apiKey: "sk-ant-provider",
reasoning: "off",
temperature: 0.2,
onPayload: (payload) => {
capturedPayload = {
...(payload as Record<string, unknown>),
top_p: 0.9,
top_k: 40,
};
return capturedPayload;
},
},
);
await stream.result();
expect(capturedPayload).toMatchObject({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "low" },
});
expect(capturedPayload).not.toHaveProperty("temperature");
expect(capturedPayload).not.toHaveProperty("top_p");
expect(capturedPayload).not.toHaveProperty("top_k");
});
it("preserves native max effort for Claude Mythos Preview", async () => {
let capturedPayload: unknown;
const stream = streamSimpleAnthropic(
@@ -1546,6 +1589,41 @@ describe("Anthropic provider", () => {
expect((capturedPayload as { output_config?: unknown }).output_config).toBeUndefined();
});
it.each(["claude-opus-4-8", "claude-mythos-preview"])(
"restores default sampling for %s after payload hooks",
async (modelId) => {
let capturedPayload: unknown;
const stream = streamSimpleAnthropic(
makeAnthropicModel({
id: modelId,
name: modelId,
maxTokens: 128_000,
}),
{ messages: [{ role: "user", content: "hello", timestamp: 0 }] },
{
apiKey: "sk-ant-provider",
reasoning: "high",
temperature: 0.2,
onPayload: (payload) => {
capturedPayload = {
...(payload as Record<string, unknown>),
temperature: 0.2,
top_p: 0.9,
top_k: 40,
};
return capturedPayload;
},
},
);
await stream.result();
expect(capturedPayload).not.toHaveProperty("temperature");
expect(capturedPayload).not.toHaveProperty("top_p");
expect(capturedPayload).not.toHaveProperty("top_k");
},
);
it.each([
{
id: "prod-primary",
+15 -9
View File
@@ -46,6 +46,7 @@ import {
usesFoundryBearerAuth,
} from "./anthropic-auth-headers.js";
import {
applyClaudeRequestContract,
prepareClaudeSonnet5RequestContext,
resolveClaudeNativeThinkingLevelMap,
resolveClaudeSonnet5ModelIdentity,
@@ -575,6 +576,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
if (nextParams !== undefined) {
params = nextParams as MessageCreateParamsStreaming;
}
applyClaudeRequestContract(params as unknown as Record<string, unknown>, model);
const requestOptions = {
...(options?.signal ? { signal: options.signal } : {}),
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
@@ -1014,14 +1016,18 @@ export const streamSimpleAnthropic: StreamFunction<
toolChoice: options?.toolChoice,
};
const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model);
const fable5 = usesClaudeFable5MessagesContract(model);
if (options?.reasoning === "off" && !mandatoryAdaptiveThinking) {
return streamAnthropic(model, context, {
...base,
thinkingEnabled: false,
} satisfies AnthropicOptions);
}
const reasoning = options?.reasoning === "off" ? (fable5 ? "low" : "high") : options?.reasoning;
const reasoning =
options?.reasoning === "off"
? mandatoryAdaptiveThinking
? "low"
: "high"
: options?.reasoning;
if (resolveClaudeSonnet5ModelIdentity(model)) {
return streamAnthropic(model, context, {
...base,
@@ -1239,8 +1245,8 @@ function buildParams(
params: MessageCreateParamsStreaming;
toolProjection?: AnthropicToolProjection;
} {
const fable5 = usesClaudeFable5MessagesContract(model);
const replayThinkingEnabled = fable5 || options?.thinkingEnabled === true;
const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model);
const replayThinkingEnabled = mandatoryAdaptiveThinking || options?.thinkingEnabled === true;
const { cacheControl } = getCacheControl(model, options?.cacheRetention);
const system = buildAnthropicSystemBlocks(context.systemPrompt, isOAuthTokenResult, cacheControl);
const compat = context.tools ? getAnthropicCompat(model) : undefined;
@@ -1304,18 +1310,18 @@ function buildParams(
params.tools = tools;
}
// Configure thinking mode: always-on adaptive (Fable 5), adaptive (Opus
// 4.6+ and Sonnet 4.6),
// Configure thinking mode: always-on adaptive (Fable 5 and Mythos 5),
// adaptive (Opus 4.6+ and Sonnet 4.6),
// budget-based (older models), or explicitly disabled.
if (fable5 || model.reasoning || supportsAdaptiveThinking(model)) {
if (fable5 || options?.thinkingEnabled) {
if (mandatoryAdaptiveThinking || model.reasoning || supportsAdaptiveThinking(model)) {
if (mandatoryAdaptiveThinking || options?.thinkingEnabled) {
// Default to "summarized" so Opus 4.7+ and Mythos Preview behave like
// older Claude 4 models (whose API default is also "summarized").
const display: AnthropicThinkingDisplay = options?.thinkingDisplay ?? "summarized";
if (supportsAdaptiveThinking(model)) {
// Adaptive thinking: Claude decides when and how much to think.
params.thinking = { type: "adaptive", display };
const effort = options?.effort ?? (fable5 ? "high" : undefined);
const effort = options?.effort ?? (mandatoryAdaptiveThinking ? "high" : undefined);
if (effort) {
// The Anthropic SDK types can lag newly supported effort values such as "xhigh".
params.output_config =
@@ -65,6 +65,26 @@ export function resolveClaudeFable5ModelIdentity(ref: ClaudeModelRef): string |
return normalized.slice((match.index ?? 0) + (match[0].startsWith("-") ? 1 : 0));
}
/** Resolve Claude Mythos 5 through direct ids, cloud ids, or deployment metadata. */
export function resolveClaudeMythos5ModelIdentity(ref: ClaudeModelRef): string | undefined {
const normalized = resolveClaudeModelIdentity(ref);
const match = /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.exec(normalized);
if (!match) {
return undefined;
}
return normalized.slice((match.index ?? 0) + (match[0].startsWith("-") ? 1 : 0));
}
/** Return whether a Claude model requires adaptive thinking instead of manual budgets. */
export function requiresClaudeMandatoryAdaptiveThinking(ref: ClaudeModelRef): boolean {
const modelId = resolveClaudeModelIdentity(ref);
return (
resolveClaudeFable5ModelIdentity(ref) !== undefined ||
resolveClaudeMythos5ModelIdentity(ref) !== undefined ||
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId)
);
}
/** Resolve Claude Sonnet 5 through direct ids, cloud ids, or deployment metadata. */
export function resolveClaudeSonnet5ModelIdentity(ref: ClaudeModelRef): string | undefined {
const normalized = resolveClaudeModelIdentity(ref);
@@ -78,7 +98,7 @@ export function resolveClaudeSonnet5ModelIdentity(ref: ClaudeModelRef): string |
/** Return whether a Claude model supports adaptive thinking. */
export function supportsClaudeAdaptiveThinking(ref: ClaudeModelRef): boolean {
const modelId = resolveClaudeModelIdentity(ref);
return /(?:^|-)claude-(?:fable-5|mythos-preview|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test(
return /(?:^|-)claude-(?:fable-5|mythos-(?:5|preview)|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test(
modelId,
);
}
@@ -86,13 +106,24 @@ export function supportsClaudeAdaptiveThinking(ref: ClaudeModelRef): boolean {
/** Return whether a Claude model supports native max effort. */
export function supportsClaudeNativeMaxEffort(ref: ClaudeModelRef): boolean {
const modelId = resolveClaudeModelIdentity(ref);
return /(?:^|-)claude-(?:fable-5|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test(modelId);
return /(?:^|-)claude-(?:fable-5|mythos-5|opus-4-(?:6|7|8)|sonnet-(?:5|4-6))(?=$|[^a-z0-9])/.test(
modelId,
);
}
/** Return whether a Claude model supports native xhigh effort. */
export function supportsClaudeNativeXhighEffort(ref: ClaudeModelRef): boolean {
const modelId = resolveClaudeModelIdentity(ref);
return /(?:^|-)claude-(?:fable-5|opus-4-(?:7|8)|sonnet-5)(?=$|[^a-z0-9])/.test(modelId);
return /(?:^|-)claude-(?:fable-5|mythos-5|opus-4-(?:7|8)|sonnet-5)(?=$|[^a-z0-9])/.test(modelId);
}
/** Return whether a Claude model rejects caller-selected sampling parameters. */
export function requiresClaudeDefaultSampling(ref: ClaudeModelRef): boolean {
const modelId = resolveClaudeModelIdentity(ref);
return (
supportsClaudeNativeXhighEffort(ref) ||
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId)
);
}
/**
@@ -3519,6 +3519,42 @@ describe("anthropic transport stream", () => {
expect(result.responseModel).toBe("claude-fable-5");
});
it("uses mandatory adaptive thinking and default sampling for Claude Mythos 5 transport runs", async () => {
const model = makeAnthropicTransportModel({
id: "prod-mythos",
name: "Production Claude",
provider: "microsoft-foundry",
params: { canonicalModelId: "claude-mythos-5" },
reasoning: false,
baseUrl: "https://example.services.ai.azure.com/anthropic",
maxTokens: 128_000,
});
await runTransportStream(
model,
{
messages: [{ role: "user", content: "Think." }],
} as AnthropicStreamContext,
{
apiKey: "sk-ant-api",
reasoning: "off",
temperature: 0.2,
onPayload: (payload) => ({
...(payload as Record<string, unknown>),
top_p: 0.9,
top_k: 40,
}),
} as AnthropicStreamOptions,
);
const payload = latestAnthropicRequest().payload;
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "low" });
expect(payload).not.toHaveProperty("temperature");
expect(payload).not.toHaveProperty("top_p");
expect(payload).not.toHaveProperty("top_k");
});
it("uses adaptive thinking for canonical Claude Mythos Preview transport aliases", async () => {
const model = makeAnthropicTransportModel({
id: "prod-mythos-preview",
@@ -3574,6 +3610,36 @@ describe("anthropic transport stream", () => {
expect(payload.output_config).toEqual({ effort: "high" });
});
it.each(["claude-opus-4-8", "claude-mythos-preview"])(
"restores default sampling for %s transport requests after payload hooks",
async (modelId) => {
await runTransportStream(
makeAnthropicTransportModel({
id: modelId,
name: modelId,
maxTokens: 128_000,
}),
{ messages: [{ role: "user", content: "hello" }] } as AnthropicStreamContext,
{
apiKey: "sk-ant-api",
reasoning: "high",
temperature: 0.2,
onPayload: (payload) => ({
...(payload as Record<string, unknown>),
temperature: 0.2,
top_p: 0.9,
top_k: 40,
}),
} as AnthropicStreamOptions,
);
const payload = latestAnthropicRequest().payload;
expect(payload).not.toHaveProperty("temperature");
expect(payload).not.toHaveProperty("top_p");
expect(payload).not.toHaveProperty("top_k");
},
);
it("maps Claude Fable 5 transport thinking levels to adaptive effort", async () => {
const model = makeAnthropicTransportModel({
id: "claude-fable-5",
+8 -11
View File
@@ -2,6 +2,7 @@ import {
ANTHROPIC_OMITTED_REASONING_TEXT,
ANTHROPIC_SERVER_SIDE_FALLBACK_BETA,
CLAUDE_FABLE_5_FALLBACK_MODEL_COST,
applyClaudeRequestContract,
applyAnthropicFallbackBoundary,
buildAnthropicServerSideFallbacks,
defaultsClaudeAdaptiveThinking,
@@ -979,8 +980,8 @@ function buildAnthropicParams(
params: Record<string, unknown>;
toolProjection?: AnthropicToolProjection;
} {
const fable5 = usesClaudeFable5MessagesContract(model);
const replayThinkingEnabled = fable5 || options?.thinkingEnabled === true;
const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model);
const replayThinkingEnabled = mandatoryAdaptiveThinking || options?.thinkingEnabled === true;
const maxTokens = resolveAnthropicMessagesMaxTokens({
modelMaxTokens: model.maxTokens,
requestedMaxTokens: options?.maxTokens,
@@ -1060,8 +1061,8 @@ function buildAnthropicParams(
params.tools = convertedTools.tools;
}
}
if (fable5 || model.reasoning || supportsAdaptiveThinking(model)) {
if (fable5 || options?.thinkingEnabled) {
if (mandatoryAdaptiveThinking || model.reasoning || supportsAdaptiveThinking(model)) {
if (mandatoryAdaptiveThinking || options?.thinkingEnabled) {
if (supportsAdaptiveThinking(model)) {
// Default display to "summarized" so Opus 4.7+/Fable 5 return a thinking
// summary like older Claude 4 models — mirrors the provider path
@@ -1070,7 +1071,7 @@ function buildAnthropicParams(
// blank (the live agent transport previously sent this for opus-4-8).
const display: AnthropicThinkingDisplay = options?.thinkingDisplay ?? "summarized";
params.thinking = { type: "adaptive", display };
const effort = options?.effort ?? (fable5 ? "high" : undefined);
const effort = options?.effort ?? (mandatoryAdaptiveThinking ? "high" : undefined);
if (effort) {
params.output_config = { effort };
}
@@ -1121,13 +1122,8 @@ function resolveAnthropicTransportOptions(
const reasoningModelMaxTokens =
resolvePositiveAnthropicMaxTokens(model.maxTokens) ?? baseMaxTokens;
const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model);
const fable5 = usesClaudeFable5MessagesContract(model);
const reasoning =
options?.reasoning === "off" && mandatoryAdaptiveThinking
? fable5
? "low"
: "high"
: options?.reasoning;
options?.reasoning === "off" && mandatoryAdaptiveThinking ? "low" : options?.reasoning;
const resolved: AnthropicTransportOptions = {
temperature: options?.temperature,
stop: options?.stop,
@@ -1229,6 +1225,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
if (nextParams !== undefined) {
params = nextParams as Record<string, unknown>;
}
applyClaudeRequestContract(params, model);
const anthropicStream = client.messages.stream(
{ ...params, stream: true },
transportOptions.signal ? { signal: transportOptions.signal } : undefined,
+7
View File
@@ -44,6 +44,7 @@ const ANTHROPIC_GA_1M_MODEL_PREFIXES = [
export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576;
export const ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS = 1_000_000;
export const ANTHROPIC_FABLE_CONTEXT_TOKENS = 1_000_000;
export const ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS = 1_000_000;
export const ANTHROPIC_SONNET_5_CONTEXT_TOKENS = 1_000_000;
type ConfiguredContextTokens = {
@@ -160,6 +161,12 @@ export function resolveAnthropicFixedContextWindow(
) {
return ANTHROPIC_FABLE_CONTEXT_TOKENS;
}
if (
(provider === "anthropic" || provider === "anthropic-vertex") &&
/^claude-mythos-5(?=$|[^a-z0-9])/.test(modelId)
) {
return ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS;
}
if (resolveClaudeSonnet5ModelIdentity({ id: modelId })) {
return ANTHROPIC_SONNET_5_CONTEXT_TOKENS;
}
+19
View File
@@ -11,6 +11,7 @@ import {
import {
ANTHROPIC_CONTEXT_1M_TOKENS,
ANTHROPIC_FABLE_CONTEXT_TOKENS,
ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS,
ANTHROPIC_SONNET_5_CONTEXT_TOKENS,
ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS,
applyConfiguredContextWindows,
@@ -419,6 +420,8 @@ describe("resolveContextTokensForModel", () => {
it.each([
["anthropic", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS],
["anthropic-vertex", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS],
["anthropic", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS],
["anthropic-vertex", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS],
["anthropic", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
["anthropic-vertex", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
["claude-cli", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
@@ -449,6 +452,17 @@ describe("resolveContextTokensForModel", () => {
expect(result).toBe(200_000);
});
it("does not give mythos-5 context window to claude-mythos-50", () => {
const result = resolveContextTokensForModel({
provider: "anthropic",
model: "claude-mythos-50",
fallbackContextTokens: 200_000,
allowAsyncLoad: false,
});
expect(result).toBe(200_000);
});
it.each([
["anthropic", "claude-fable-5"],
["anthropic-vertex", "claude-fable-5"],
@@ -545,6 +559,11 @@ describe("resolveContextTokensForModel", () => {
it.each([
["anthropic", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS],
["anthropic-vertex", "claude-fable-5", ANTHROPIC_FABLE_CONTEXT_TOKENS],
["anthropic", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS],
["anthropic-vertex", "claude-mythos-5", ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS],
["anthropic", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
["anthropic-vertex", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
["claude-cli", "claude-sonnet-5", ANTHROPIC_SONNET_5_CONTEXT_TOKENS],
["anthropic", "claude-sonnet-4-6", ANTHROPIC_CONTEXT_1M_TOKENS],
["anthropic-vertex", "claude-sonnet-4-6", ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS],
])(
+1
View File
@@ -30,6 +30,7 @@ import { normalizeProviderId } from "./model-selection.js";
export {
ANTHROPIC_CONTEXT_1M_TOKENS,
ANTHROPIC_FABLE_CONTEXT_TOKENS,
ANTHROPIC_MYTHOS_5_CONTEXT_TOKENS,
ANTHROPIC_SONNET_5_CONTEXT_TOKENS,
ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS,
} from "./context-resolution.js";
+23 -2
View File
@@ -60,6 +60,18 @@ describe("transformTransportMessages synthetic tool-result policy", () => {
source: { provider: "anthropic", model: "claude-fable-5" },
target: { provider: "anthropic-vertex", model: "claude-opus-4-8" },
},
{
source: { provider: "anthropic", model: "claude-mythos-5" },
target: { provider: "anthropic", model: "claude-opus-4-8" },
},
{
source: { provider: "anthropic", model: "claude-fable-5" },
target: { provider: "anthropic-vertex", model: "claude-mythos-5" },
},
{
source: { provider: "anthropic", model: "claude-mythos-5" },
target: { provider: "anthropic", model: "claude-fable-5" },
},
{
source: { provider: "anthropic", model: "claude-sonnet-4-6" },
target: { provider: "anthropic", model: "claude-fable-5" },
@@ -100,7 +112,7 @@ describe("transformTransportMessages synthetic tool-result policy", () => {
canonicalModelId: "claude-fable-5",
},
},
])("drops model-bound thinking for Fable switches", ({ source, target }) => {
])("drops model-bound thinking for Fable/Mythos switches", ({ source, target }) => {
const result = transformTransportMessages(
[
{
@@ -185,8 +197,17 @@ describe("transformTransportMessages synthetic tool-result policy", () => {
targetModel: "prod-primary",
targetCanonicalModelId: "claude-fable-5",
},
{
sourceProvider: "anthropic",
sourceModel: "claude-mythos-5",
sourceResponseModel: undefined,
targetProvider: "anthropic-vertex",
targetApi: "anthropic-messages" as const,
targetModel: "claude-mythos-5",
targetCanonicalModelId: undefined,
},
])(
"preserves Fable thinking across compatible Anthropic transports",
"preserves Fable/Mythos thinking across compatible Anthropic transports",
({
sourceProvider,
sourceModel,
+28 -8
View File
@@ -9,8 +9,10 @@ import {
OPENAI_COMPATIBLE_REPLAY_HOOKS,
PASSTHROUGH_GEMINI_REPLAY_HOOKS,
resolveClaudeFable5ModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
resolveClaudeThinkingProfile,
requiresClaudeDefaultSampling,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
@@ -51,9 +53,24 @@ describe("Claude model contracts", () => {
);
});
it("recognizes Claude Mythos 5 as mandatory adaptive with native max effort", () => {
expect(resolveClaudeMythos5ModelIdentity({ id: "us.anthropic.claude-mythos-5-v1:0" })).toBe(
"claude-mythos-5-v1:0",
);
expect(supportsClaudeAdaptiveThinking({ id: "claude-mythos-5" })).toBe(true);
expect(supportsClaudeNativeMaxEffort({ id: "claude-mythos-5" })).toBe(true);
expect(supportsClaudeNativeXhighEffort({ id: "anthropic.claude-mythos-5" })).toBe(true);
expect(requiresClaudeDefaultSampling({ id: "claude-mythos-5" })).toBe(true);
});
it("does not classify later numeric model versions as supported aliases", () => {
expect(supportsClaudeAdaptiveThinking({ id: "claude-sonnet-4-60" })).toBe(false);
expect(supportsClaudeAdaptiveThinking({ id: "claude-sonnet-50" })).toBe(false);
expect(supportsClaudeAdaptiveThinking({ id: "claude-mythos-50" })).toBe(false);
expect(supportsClaudeNativeXhighEffort({ id: "claude-opus-4-80" })).toBe(false);
expect(requiresClaudeDefaultSampling({ id: "claude-opus-4-8" })).toBe(true);
expect(requiresClaudeDefaultSampling({ id: "claude-mythos-preview" })).toBe(true);
expect(requiresClaudeDefaultSampling({ id: "claude-opus-4-6" })).toBe(false);
expect(readLevelIds(resolveClaudeThinkingProfile("claude-opus-4-80"))).toEqual([
"off",
"minimal",
@@ -318,14 +335,17 @@ describe("resolveClaudeThinkingProfile", () => {
expectLevelIdsInclude(profile, ["off", "xhigh", "adaptive", "max"]);
});
it("exposes Fable 5's always-adaptive profile to Claude providers", () => {
const profile = resolveClaudeThinkingProfile("claude-fable-5");
expectFields(profile, {
defaultLevel: "high",
preserveWhenCatalogReasoningFalse: true,
});
expectLevelIdsInclude(profile, ["xhigh", "adaptive", "max"]);
});
it.each(["claude-fable-5", "claude-mythos-5"])(
"exposes %s's mandatory-adaptive profile to Claude providers",
(modelId) => {
const profile = resolveClaudeThinkingProfile(modelId);
expectFields(profile, {
defaultLevel: "high",
preserveWhenCatalogReasoningFalse: true,
});
expectLevelIdsInclude(profile, ["xhigh", "adaptive", "max"]);
},
);
it("does not match longer unrelated Claude ids by prefix only", () => {
expect(resolveClaudeThinkingProfile("vendor/claude-fable-500").defaultLevel).toBeUndefined();
+3
View File
@@ -29,8 +29,11 @@ export type {
export {
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeNativeThinkingLevelMap,
resolveClaudeSonnet5ModelIdentity,
requiresClaudeDefaultSampling,
requiresClaudeMandatoryAdaptiveThinking,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
+2 -1
View File
@@ -6,6 +6,7 @@ import {
CLAUDE_SONNET_5_THINKING_PROFILE,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
resolveClaudeMythos5ModelIdentity,
resolveClaudeSonnet5ModelIdentity,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeXhighEffort,
@@ -38,7 +39,7 @@ export function resolveClaudeThinkingProfile(
): ProviderThinkingProfile {
const ref = { id: modelId, params };
const canonicalModelId = resolveClaudeModelIdentity(ref);
if (resolveClaudeFable5ModelIdentity(ref)) {
if (resolveClaudeFable5ModelIdentity(ref) || resolveClaudeMythos5ModelIdentity(ref)) {
return CLAUDE_FABLE_5_THINKING_PROFILE;
}
if (resolveClaudeSonnet5ModelIdentity(ref)) {