fix(anthropic): require Mythos adaptive thinking

This commit is contained in:
Vincent Koc
2026-06-11 11:01:07 +09:00
parent 4a06e4c773
commit 7acedeaf11
5 changed files with 84 additions and 6 deletions

View File

@@ -308,6 +308,7 @@ describe("anthropic transport stream", () => {
} as AnthropicStreamContext,
{
apiKey: "sk-ant-api",
toolChoice: { type: "tool", name: "read_file" },
} as AnthropicStreamOptions,
);
@@ -2121,6 +2122,7 @@ describe("anthropic transport stream", () => {
const payload = latestAnthropicRequest().payload;
expect(payload.thinking).toEqual({ type: "adaptive" });
expect(payload.output_config).toEqual({ effort: "high" });
expect(payload.tool_choice).toEqual({ type: "auto" });
});
it("does not infer adaptive thinking from forward-compatible effort maps", async () => {
@@ -2302,6 +2304,33 @@ describe("anthropic transport stream", () => {
expect(payload.output_config).toEqual({ effort: "high" });
});
it("uses mandatory adaptive thinking for canonical Claude Mythos Preview transport aliases", async () => {
const model = makeAnthropicTransportModel({
id: "prod-mythos-preview",
name: "Production Claude",
provider: "microsoft-foundry",
params: { canonicalModelId: "claude-mythos-preview" },
reasoning: false,
baseUrl: "https://example.services.ai.azure.com/anthropic",
maxTokens: 128_000,
});
guardedFetchMock.mockResolvedValueOnce(createSseResponse());
await runTransportStream(
model,
{
messages: [{ role: "user", content: "Think." }],
} as AnthropicStreamContext,
{
apiKey: "sk-ant-api",
} as AnthropicStreamOptions,
);
const payload = latestAnthropicRequest().payload;
expect(payload.thinking).toEqual({ type: "adaptive" });
expect(payload.output_config).toEqual({ effort: "high" });
});
it("maps Claude Fable 5 transport thinking levels to adaptive effort", async () => {
const model = makeAnthropicTransportModel({
id: "claude-fable-5",

View File

@@ -17,6 +17,7 @@ import type {
import { parseStreamingJson } from "../llm/utils/json-parse.js";
import {
resolveClaudeNativeThinkingLevelMap,
requiresClaudeAdaptiveThinking,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
@@ -141,7 +142,7 @@ function normalizeAnthropicToolChoice(
toolChoice: AnthropicTransportOptions["toolChoice"],
) {
if (
usesClaudeFable5MessagesContract(model) &&
requiresClaudeAdaptiveThinking(model) &&
(toolChoice === "any" || (typeof toolChoice === "object" && toolChoice.type === "tool"))
) {
return { type: "auto" as const };
@@ -1025,7 +1026,7 @@ function resolveAnthropicTransportOptions(
reasoning: options?.reasoning,
};
if (!options?.reasoning) {
resolved.thinkingEnabled = usesClaudeFable5MessagesContract(model);
resolved.thinkingEnabled = requiresClaudeAdaptiveThinking(model);
if (resolved.thinkingEnabled) {
resolved.effort = "high";
}

View File

@@ -537,6 +537,38 @@ describe("Anthropic provider", () => {
});
});
it("uses mandatory adaptive thinking for Foundry Mythos Preview", async () => {
let capturedPayload: unknown;
const stream = streamSimpleAnthropic(
makeAnthropicModel({
id: "prod-mythos-preview",
name: "Production Claude",
provider: "microsoft-foundry",
params: { canonicalModelId: "claude-mythos-preview" },
reasoning: false,
}),
{
messages: [{ role: "user", content: "hello", timestamp: 0 }],
},
{
apiKey: "sk-ant-provider",
toolChoice: "any",
onPayload: (payload) => {
capturedPayload = payload;
throw new Error("stop before network");
},
},
);
await stream.result();
expect(capturedPayload).toMatchObject({
thinking: { type: "adaptive" },
output_config: { effort: "high" },
tool_choice: { type: "auto" },
});
});
it("uses adaptive high effort for Foundry Mythos Preview without native max metadata", async () => {
let capturedPayload: unknown;
const stream = streamSimpleAnthropic(

View File

@@ -14,6 +14,7 @@ import {
} from "../../agents/system-prompt-cache-boundary.js";
import {
resolveClaudeNativeThinkingLevelMap,
requiresClaudeAdaptiveThinking,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
@@ -802,7 +803,7 @@ function normalizeAnthropicToolChoice(
toolChoice: AnthropicOptions["toolChoice"],
) {
if (
usesClaudeFable5MessagesContract(model) &&
requiresClaudeAdaptiveThinking(model) &&
(toolChoice === "any" || (typeof toolChoice === "object" && toolChoice.type === "tool"))
) {
return { type: "auto" as const };
@@ -875,11 +876,11 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS
const base = buildBaseOptions(model, options, apiKey);
if (!options?.reasoning) {
const fable5 = usesClaudeFable5MessagesContract(model);
const mandatoryAdaptiveThinking = requiresClaudeAdaptiveThinking(model);
return streamAnthropic(model, context, {
...base,
thinkingEnabled: fable5,
...(fable5 ? { effort: "high" as const } : {}),
thinkingEnabled: mandatoryAdaptiveThinking,
...(mandatoryAdaptiveThinking ? { effort: "high" as const } : {}),
} satisfies AnthropicOptions);
}

View File

@@ -49,6 +49,21 @@ export function usesClaudeFable5MessagesContract(model: {
);
}
export function requiresClaudeAdaptiveThinking(model: {
id?: string;
params?: Record<string, unknown>;
api?: string;
}): boolean {
if (normalizeApi(model.api) !== "anthropic-messages") {
return false;
}
const modelId = resolveClaudeModelIdentity(model);
return (
resolveClaudeFable5ModelIdentity(model) !== undefined ||
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(modelId)
);
}
function resolveReplayFableIdentity(ref: ReplayModelRef): string | undefined {
if (normalizeApi(ref.api) !== "anthropic-messages") {
return undefined;