mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(models): add Claude Mythos 5 support (#101238)
This commit is contained in:
@@ -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": {},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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"))) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
])(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -29,8 +29,11 @@ export type {
|
||||
export {
|
||||
resolveClaudeFable5ModelIdentity,
|
||||
resolveClaudeModelIdentity,
|
||||
resolveClaudeMythos5ModelIdentity,
|
||||
resolveClaudeNativeThinkingLevelMap,
|
||||
resolveClaudeSonnet5ModelIdentity,
|
||||
requiresClaudeDefaultSampling,
|
||||
requiresClaudeMandatoryAdaptiveThinking,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
supportsClaudeNativeMaxEffort,
|
||||
supportsClaudeNativeXhighEffort,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user