diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 3d546ac0d7f..ca0c7abbdb8 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -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", diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index 4812b028b23..f8887511c5f 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -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"; } diff --git a/src/llm/providers/anthropic.test.ts b/src/llm/providers/anthropic.test.ts index 4e1b3e96f7d..a07f14d3092 100644 --- a/src/llm/providers/anthropic.test.ts +++ b/src/llm/providers/anthropic.test.ts @@ -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( diff --git a/src/llm/providers/anthropic.ts b/src/llm/providers/anthropic.ts index 71d759edc42..25d56bd933a 100644 --- a/src/llm/providers/anthropic.ts +++ b/src/llm/providers/anthropic.ts @@ -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); } diff --git a/src/shared/anthropic-model-contract.ts b/src/shared/anthropic-model-contract.ts index 700c8832a10..749681bf054 100644 --- a/src/shared/anthropic-model-contract.ts +++ b/src/shared/anthropic-model-contract.ts @@ -49,6 +49,21 @@ export function usesClaudeFable5MessagesContract(model: { ); } +export function requiresClaudeAdaptiveThinking(model: { + id?: string; + params?: Record; + 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;