From 1046716b6d3d45f30d5efad42becbd440d6187ee Mon Sep 17 00:00:00 2001 From: tavren27 <348969968+tavren27@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:57:51 -0800 Subject: [PATCH] fix(anthropic)!: sync the beta header allowlist with what anthropic, vertex_ai, and azure_ai accept (#49409) * fix(anthropic): sync the beta header allowlist with what anthropic, vertex_ai, and azure_ai accept * test(e2e): send the azure_ai beta check without the code execution tool * test(anthropic): require a vertex_ai and azure_ai decision for every anthropic beta * fix(anthropic): forward the betas Claude Code sends that anthropic, vertex_ai, and azure_ai accept * test(e2e): send Vertex's header-only betas in the beta acceptance case * fix(anthropic): keep forwarding Vertex computer-use betas and point beta tests at platforms that still lack them A live probe found each Claude 4.5 and 4.6 model on Vertex needs one of the two computer-use betas for the computer tool, so dropping both broke it. The e2e acceptance test leaves those two out of its all-at-once Vertex call, and the integration and unit tests that relied on a beta being dropped now use one the platform still lacks. * test(anthropic): point unsupported-beta tests at betas the platforms still drop The Azure AI handler test used claude-code-20250219 as the beta Azure drops, and the Rust parse test used it as an unknown value. This PR forwards it on Azure AI and maps it, so both now use values that stay dropped or unknown. * fix(anthropic): add computer-use-2025-11-24 for the computer_20251124 tool The computer_20251124 tool fell through to the default computer-use-2024-10-22 beta, which the provider rejects for that tool version. Map it to computer-use-2025-11-24, which the beta map now forwards on anthropic and azure_ai * fix(anthropic): send no computer use beta for an unknown computer tool version An unknown computer tool version fell back to computer-use-2024-10-22, which the anthropic and azure_ai columns now forward. It now gets no beta, as every platform effectively sent before. Every computer tool version also skips defer_loading and allowed_callers, computer_20251124 included * test(azure_ai): read the branch's beta map in the auto-injected beta wire test * fix(anthropic): keep defer_loading and allowed_callers on the computer_20251124 chat tool * test(anthropic): read the branch's beta map in the azure_ai safeguards beta wire test --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> --- .../src/providers/anthropic/beta.rs | 293 +++++++++++++----- .../crates/llms/src/anthropic/beta_headers.rs | 5 +- litellm/anthropic_beta_headers_config.json | 175 ++++++++++- litellm/llms/anthropic/common_utils.py | 20 +- .../test_messages_beta_headers_e2e.py | 74 +++-- .../test_anthropic_safeguards_beta_wire.py | 27 +- .../test_beta_gated_fields_wire.py | 19 +- .../test_azure_ai_claude_messages_wire.py | 29 +- ...vertex_anthropic_inline_tools_beta_wire.py | 2 +- .../test_anthropic_chat_transformation.py | 102 +++++- .../custom_httpx/test_llm_http_handler.py | 2 +- ...partner_models_anthropic_transformation.py | 38 +-- .../test_anthropic_beta_headers_filtering.py | 67 +++- 13 files changed, 674 insertions(+), 179 deletions(-) diff --git a/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs b/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs index 01ed023999d..46c14a21c59 100644 --- a/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs +++ b/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs @@ -124,12 +124,86 @@ pub enum AnthropicBeta { ToolSearchTool20251019, #[strum(serialize = "inline-tools-2026-09-15")] InlineTools20260915, + #[strum(serialize = "afk-mode-2026-01-31")] + AfkMode20260131, + #[strum(serialize = "agent-memory-2026-07-22")] + AgentMemory20260722, + #[strum(serialize = "analytics-usage-cost-2026-08-24")] + AnalyticsUsageCost20260824, + #[strum(serialize = "cache-diagnosis-2026-04-07")] + CacheDiagnosis20260407, + #[strum(serialize = "ce-plugins-2026-09-01")] + CePlugins20260901, + #[strum(serialize = "ce-user-management-2026-07-13")] + CeUserManagement20260713, + #[strum(serialize = "code-execution-2025-05-22")] + CodeExecution20250522, + #[strum(serialize = "computer-use-2024-10-22")] + ComputerUse20241022, + #[strum(serialize = "dev-full-thinking-2025-05-14")] + DevFullThinking20250514, + #[strum(serialize = "dreaming-2026-04-21")] + Dreaming20260421, + #[strum(serialize = "experimental-claude-tag-2026-10-21")] + ExperimentalClaudeTag20261021, + #[strum(serialize = "extended-cache-ttl-2025-04-11")] + ExtendedCacheTtl20250411, + #[strum(serialize = "fallback-credit-2026-06-01")] + FallbackCredit20260601, + #[strum(serialize = "fallback-credit-2026-07-01")] + FallbackCredit20260701, + #[strum(serialize = "managed-agents-2026-04-01")] + ManagedAgents20260401, + #[strum(serialize = "mcp-client-2026-09-15")] + McpClient20260915, + #[strum(serialize = "mcp-tunnels-2026-06-22")] + McpTunnels20260622, + #[strum(serialize = "message-batches-2024-09-24")] + MessageBatches20240924, + #[strum(serialize = "message-threads-2026-08-12")] + MessageThreads20260812, + #[strum(serialize = "mid-conversation-system-clear-at-2026-08-21")] + MidConversationSystemClearAt20260821, + #[strum(serialize = "model-context-window-exceeded-2025-08-26")] + ModelContextWindowExceeded20250826, + #[strum(serialize = "output-300k-2026-03-24")] + Output300k20260324, + #[strum(serialize = "pdfs-2024-09-25")] + Pdfs20240925, + #[strum(serialize = "prompt-caching-2024-07-31")] + PromptCaching20240731, + #[strum(serialize = "redact-thinking-2026-02-12")] + RedactThinking20260212, + #[strum(serialize = "server-side-fallback-2026-06-01")] + ServerSideFallback20260601, + #[strum(serialize = "server-side-fallback-2026-07-01")] + ServerSideFallback20260701, + #[strum(serialize = "spend-limit-reads-2026-09-26")] + SpendLimitReads20260926, + #[strum(serialize = "structured-outputs-2025-12-15")] + StructuredOutputs20251215, + #[strum(serialize = "task-budgets-2026-03-13")] + TaskBudgets20260313, + #[strum(serialize = "telemetry-destinations-2026-08-11")] + TelemetryDestinations20260811, + #[strum(serialize = "thinking-token-count-2026-05-13")] + ThinkingTokenCount20260513, + #[strum(serialize = "token-counting-2024-11-01")] + TokenCounting20241101, + #[strum(serialize = "user-profiles-2026-03-24")] + UserProfiles20260324, + #[strum(serialize = "user-profiles-2026-08-18")] + UserProfiles20260818, + #[strum(serialize = "user-profiles-2026-09-04")] + UserProfiles20260904, + #[strum(serialize = "webhooks-2026-10-07")] + Webhooks20261007, #[strum(default, transparent)] Other(String), } impl AnthropicBeta { - pub const KNOWN: [Self; 40] = [ + pub const KNOWN: [Self; 77] = [ Self::Oauth20250420, Self::WebFetch20250910, Self::WebSearch20250305, @@ -170,6 +244,43 @@ impl AnthropicBeta { Self::ToolExamples20251029, Self::ToolSearchTool20251019, Self::InlineTools20260915, + Self::AfkMode20260131, + Self::AgentMemory20260722, + Self::AnalyticsUsageCost20260824, + Self::CacheDiagnosis20260407, + Self::CePlugins20260901, + Self::CeUserManagement20260713, + Self::CodeExecution20250522, + Self::ComputerUse20241022, + Self::DevFullThinking20250514, + Self::Dreaming20260421, + Self::ExperimentalClaudeTag20261021, + Self::ExtendedCacheTtl20250411, + Self::FallbackCredit20260601, + Self::FallbackCredit20260701, + Self::ManagedAgents20260401, + Self::McpClient20260915, + Self::McpTunnels20260622, + Self::MessageBatches20240924, + Self::MessageThreads20260812, + Self::MidConversationSystemClearAt20260821, + Self::ModelContextWindowExceeded20250826, + Self::Output300k20260324, + Self::Pdfs20240925, + Self::PromptCaching20240731, + Self::RedactThinking20260212, + Self::ServerSideFallback20260601, + Self::ServerSideFallback20260701, + Self::SpendLimitReads20260926, + Self::StructuredOutputs20251215, + Self::TaskBudgets20260313, + Self::TelemetryDestinations20260811, + Self::ThinkingTokenCount20260513, + Self::TokenCounting20241101, + Self::UserProfiles20260324, + Self::UserProfiles20260818, + Self::UserProfiles20260904, + Self::Webhooks20261007, ]; pub fn as_str(&self) -> &str { @@ -195,10 +306,9 @@ impl AnthropicBeta { fn rejected_by(&self) -> &'static [BetaProvider] { match self { - Self::AdvancedToolUse20251120 => { + Self::AdvancedToolUse20251120 | Self::ToolSearchTool20251019 => { &[BetaProvider::BedrockConverse, BetaProvider::Databricks] } - Self::ContextManagement20250627 => &[BetaProvider::BedrockConverse], Self::AdvisorTool20260301 => &[ BetaProvider::AzureAi, BetaProvider::BedrockConverse, @@ -207,98 +317,99 @@ impl AnthropicBeta { BetaProvider::VertexAi, BetaProvider::Databricks, ], - Self::Compact20260904 => &[ + Self::AfkMode20260131 + | Self::CePlugins20260901 + | Self::CeUserManagement20260713 + | Self::Compact20260904 + | Self::Dreaming20260421 + | Self::ExperimentalClaudeTag20261021 + | Self::ExtendedCacheTtl20250411 + | Self::FallbackCredit20260601 + | Self::FallbackCredit20260701 + | Self::InlineTools20260915 + | Self::ManagedAgents20260401 + | Self::McpTunnels20260622 + | Self::MidConversationSystemClearAt20260821 + | Self::ModelContextWindowExceeded20250826 + | Self::Oauth20250420 + | Self::Pdfs20240925 + | Self::PromptCaching20240731 + | Self::PromptCachingScope20260105 + | Self::RedactThinking20260212 + | Self::ServerSideFallback20260601 + | Self::SpendLimitReads20260926 + | Self::StructuredOutputs20251215 + | Self::TaskBudgets20260313 + | Self::TelemetryDestinations20260811 + | Self::ThinkingTokenCount20260513 + | Self::TokenCounting20241101 + | Self::UserProfiles20260324 + | Self::UserProfiles20260818 + | Self::UserProfiles20260904 => &[ BetaProvider::BedrockConverse, BetaProvider::Bedrock, BetaProvider::BedrockMantle, BetaProvider::Databricks, ], - Self::Bash20241022 + Self::AgentMemory20260722 + | Self::Bash20241022 | Self::Bash20250124 + | Self::DevFullThinking20250514 | Self::McpServers20251204 | Self::StructuredOutput20240301 | Self::TextEditor20241022 | Self::TextEditor20250124 => BetaProvider::VARIANTS, - Self::ClaudeCode20250219 | Self::ToolExamples20251029 => &[ - BetaProvider::Anthropic, - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::Bedrock, - BetaProvider::VertexAi, - BetaProvider::Databricks, - ], - Self::CodeExecution20250825 + Self::AnalyticsUsageCost20260824 + | Self::CacheDiagnosis20260407 + | Self::CodeExecution20250522 + | Self::CodeExecution20250825 + | Self::ComputerUse20241022 + | Self::FastMode20260201 | Self::FilesApi20250414 | Self::McpClient20250404 | Self::McpClient20251120 - | Self::PromptCachingScope20260105 + | Self::McpClient20260915 + | Self::MessageBatches20240924 + | Self::MessageThreads20260812 + | Self::Output300k20260324 + | Self::ServerSideFallback20260701 | Self::Skills20251002 - | Self::WebFetch20250910 => &[ + | Self::WebFetch20250910 + | Self::Webhooks20261007 => &[ BetaProvider::BedrockConverse, BetaProvider::Bedrock, BetaProvider::BedrockMantle, BetaProvider::VertexAi, BetaProvider::Databricks, ], - Self::Compact20260112 => &[BetaProvider::AzureAi, BetaProvider::BedrockConverse], + Self::ClaudeCode20250219 + | Self::PerTurnControl20260701 + | Self::ToolExamples20251029 => &[ + BetaProvider::BedrockConverse, + BetaProvider::Bedrock, + BetaProvider::Databricks, + ], + Self::Compact20260112 + | Self::ContextManagement20250627 + | Self::DangerousToolUse20260903 => &[BetaProvider::BedrockConverse], Self::ComputerUse20250124 | Self::ComputerUse20251124 | Self::Context1m20250807 + | Self::Effort20251124 + | Self::FineGrainedToolStreaming20250514 | Self::ThinkingBindingControls20260801 | Self::ThinkingDisplayUpdates20260818 => &[], - Self::DangerousToolUse20260903 => &[BetaProvider::BedrockConverse], - Self::Effort20251124 => &[BetaProvider::VertexAi], - Self::FastMode20260201 | Self::Oauth20250420 => &[ - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::Bedrock, - BetaProvider::BedrockMantle, - BetaProvider::VertexAi, - BetaProvider::Databricks, - ], - Self::FineGrainedToolStreaming20250514 => { - &[BetaProvider::AzureAi, BetaProvider::VertexAi] - } - Self::InterleavedThinking20250514 | Self::WebSearch20250305 => { - &[BetaProvider::BedrockConverse, BetaProvider::Bedrock] - } - Self::MidConversationOutputConfig20260701 => &[ - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::BedrockMantle, - BetaProvider::VertexAi, - BetaProvider::Databricks, - ], - Self::MidConversationToolChanges20260701 => &[ + Self::InterleavedThinking20250514 + | Self::Output128k20250219 + | Self::TokenEfficientTools20250219 + | Self::WebSearch20250305 => &[BetaProvider::BedrockConverse, BetaProvider::Bedrock], + Self::MidConversationOutputConfig20260701 + | Self::MidConversationToolChanges20260701 => &[ BetaProvider::BedrockConverse, BetaProvider::BedrockMantle, BetaProvider::Databricks, ], - Self::Output128k20250219 | Self::TokenEfficientTools20250219 => &[ - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::Bedrock, - BetaProvider::VertexAi, - ], - Self::PerTurnControl20260701 => &[ - BetaProvider::BedrockConverse, - BetaProvider::Bedrock, - BetaProvider::Databricks, - ], - Self::StructuredOutputs20251113 => &[BetaProvider::Bedrock, BetaProvider::VertexAi], - Self::ToolSearchTool20251019 => &[ - BetaProvider::Anthropic, - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::Databricks, - ], - Self::InlineTools20260915 => &[ - BetaProvider::AzureAi, - BetaProvider::BedrockConverse, - BetaProvider::Bedrock, - BetaProvider::BedrockMantle, - BetaProvider::Databricks, - ], + Self::StructuredOutputs20251113 => &[BetaProvider::Bedrock], Self::Other(_) => BetaProvider::VARIANTS, } } @@ -451,7 +562,44 @@ mod tests { AnthropicBeta::TokenEfficientTools20250219, AnthropicBeta::ToolExamples20251029, AnthropicBeta::ToolSearchTool20251019, - AnthropicBeta::InlineTools20260915 + AnthropicBeta::InlineTools20260915, + AnthropicBeta::AfkMode20260131, + AnthropicBeta::AgentMemory20260722, + AnthropicBeta::AnalyticsUsageCost20260824, + AnthropicBeta::CacheDiagnosis20260407, + AnthropicBeta::CePlugins20260901, + AnthropicBeta::CeUserManagement20260713, + AnthropicBeta::CodeExecution20250522, + AnthropicBeta::ComputerUse20241022, + AnthropicBeta::DevFullThinking20250514, + AnthropicBeta::Dreaming20260421, + AnthropicBeta::ExperimentalClaudeTag20261021, + AnthropicBeta::ExtendedCacheTtl20250411, + AnthropicBeta::FallbackCredit20260601, + AnthropicBeta::FallbackCredit20260701, + AnthropicBeta::ManagedAgents20260401, + AnthropicBeta::McpClient20260915, + AnthropicBeta::McpTunnels20260622, + AnthropicBeta::MessageBatches20240924, + AnthropicBeta::MessageThreads20260812, + AnthropicBeta::MidConversationSystemClearAt20260821, + AnthropicBeta::ModelContextWindowExceeded20250826, + AnthropicBeta::Output300k20260324, + AnthropicBeta::Pdfs20240925, + AnthropicBeta::PromptCaching20240731, + AnthropicBeta::RedactThinking20260212, + AnthropicBeta::ServerSideFallback20260601, + AnthropicBeta::ServerSideFallback20260701, + AnthropicBeta::SpendLimitReads20260926, + AnthropicBeta::StructuredOutputs20251215, + AnthropicBeta::TaskBudgets20260313, + AnthropicBeta::TelemetryDestinations20260811, + AnthropicBeta::ThinkingTokenCount20260513, + AnthropicBeta::TokenCounting20241101, + AnthropicBeta::UserProfiles20260324, + AnthropicBeta::UserProfiles20260818, + AnthropicBeta::UserProfiles20260904, + AnthropicBeta::Webhooks20261007 )] beta: AnthropicBeta, ) { @@ -551,7 +699,7 @@ mod tests { BetaProvider::Anthropic, Some(AnthropicBeta::Oauth20250420) )] - #[case::rejected("effort-2025-11-24", BetaProvider::VertexAi, None)] + #[case::rejected("skills-2025-10-02", BetaProvider::VertexAi, None)] #[case::unknown("example-beta-2099-01-01", BetaProvider::Anthropic, None)] fn on_resolves_known_spellings_held_as_other( #[case] raw: &str, @@ -580,12 +728,11 @@ mod tests { #[test] fn unknown_values_are_kept_verbatim() { - let parsed: AnthropicBeta = "claude-code-20250219".parse().unwrap(); - assert_eq!( - parsed, - AnthropicBeta::Other("claude-code-20250219".to_string()) + let parsed: AnthropicBeta = "example-beta-2099-01-01".parse().unwrap(); + assert!( + matches!(&parsed, AnthropicBeta::Other(value) if value == "example-beta-2099-01-01") ); - assert_eq!(parsed.to_string(), "claude-code-20250219"); + assert_eq!(parsed.to_string(), "example-beta-2099-01-01"); } #[test] diff --git a/litellm-rust/crates/llms/src/anthropic/beta_headers.rs b/litellm-rust/crates/llms/src/anthropic/beta_headers.rs index a0843621043..7842b478dce 100644 --- a/litellm-rust/crates/llms/src/anthropic/beta_headers.rs +++ b/litellm-rust/crates/llms/src/anthropic/beta_headers.rs @@ -96,7 +96,10 @@ mod tests { } #[rstest] - #[case::azure_ai(BetaProvider::AzureAi, "fast-mode-2026-02-01,example-beta-2099-01-01")] + #[case::azure_ai( + BetaProvider::AzureAi, + "advisor-tool-2026-03-01,example-beta-2099-01-01" + )] #[case::anthropic(BetaProvider::Anthropic, "bash_20241022")] #[case::vertex_ai(BetaProvider::VertexAi, " , ")] fn filter_removes_header_when_nothing_survives( diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index 253074b0e1f..bdfddb48522 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -37,7 +37,47 @@ "mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01", "thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18", "mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01", - "inline-tools-2026-09-15": "inline-tools-2026-09-15" + "inline-tools-2026-09-15": "inline-tools-2026-09-15", + "agent-memory-2026-07-22": null, + "analytics-usage-cost-2026-08-24": "analytics-usage-cost-2026-08-24", + "cache-diagnosis-2026-04-07": "cache-diagnosis-2026-04-07", + "ce-plugins-2026-09-01": "ce-plugins-2026-09-01", + "ce-user-management-2026-07-13": "ce-user-management-2026-07-13", + "claude-code-20250219": "claude-code-20250219", + "code-execution-2025-05-22": "code-execution-2025-05-22", + "computer-use-2024-10-22": "computer-use-2024-10-22", + "dev-full-thinking-2025-05-14": null, + "dreaming-2026-04-21": "dreaming-2026-04-21", + "experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21", + "extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11", + "fallback-credit-2026-06-01": "fallback-credit-2026-06-01", + "fallback-credit-2026-07-01": "fallback-credit-2026-07-01", + "managed-agents-2026-04-01": "managed-agents-2026-04-01", + "mcp-client-2026-09-15": "mcp-client-2026-09-15", + "mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22", + "message-batches-2024-09-24": "message-batches-2024-09-24", + "message-threads-2026-08-12": "message-threads-2026-08-12", + "mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21", + "model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26", + "output-300k-2026-03-24": "output-300k-2026-03-24", + "pdfs-2024-09-25": "pdfs-2024-09-25", + "prompt-caching-2024-07-31": "prompt-caching-2024-07-31", + "server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01", + "server-side-fallback-2026-07-01": "server-side-fallback-2026-07-01", + "spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26", + "task-budgets-2026-03-13": "task-budgets-2026-03-13", + "telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11", + "thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13", + "token-counting-2024-11-01": "token-counting-2024-11-01", + "tool-examples-2025-10-29": "tool-examples-2025-10-29", + "tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19", + "user-profiles-2026-03-24": "user-profiles-2026-03-24", + "user-profiles-2026-08-18": "user-profiles-2026-08-18", + "user-profiles-2026-09-04": "user-profiles-2026-09-04", + "webhooks-2026-10-07": "webhooks-2026-10-07", + "afk-mode-2026-01-31": "afk-mode-2026-01-31", + "redact-thinking-2026-02-12": "redact-thinking-2026-02-12", + "structured-outputs-2025-12-15": "structured-outputs-2025-12-15" }, "azure_ai": { "advisor-tool-2026-03-01": null, @@ -45,23 +85,21 @@ "bash_20241022": null, "bash_20250124": null, "code-execution-2025-08-25": "code-execution-2025-08-25", - "compact-2026-01-12": null, - "compact-2026-09-04": "compact-2026-09-04", + "compact-2026-01-12": "compact-2026-01-12", "computer-use-2025-01-24": "computer-use-2025-01-24", "computer-use-2025-11-24": "computer-use-2025-11-24", "context-1m-2025-08-07": "context-1m-2025-08-07", "context-management-2025-06-27": "context-management-2025-06-27", "dangerous-tool-use-2026-09-03": "dangerous-tool-use-2026-09-03", "effort-2025-11-24": "effort-2025-11-24", - "fast-mode-2026-02-01": null, + "fast-mode-2026-02-01": "fast-mode-2026-02-01", "files-api-2025-04-14": "files-api-2025-04-14", - "fine-grained-tool-streaming-2025-05-14": null, + "fine-grained-tool-streaming-2025-05-14": "fine-grained-tool-streaming-2025-05-14", "interleaved-thinking-2025-05-14": "interleaved-thinking-2025-05-14", "mcp-client-2025-11-20": "mcp-client-2025-11-20", "mcp-client-2025-04-04": "mcp-client-2025-04-04", "mcp-servers-2025-12-04": null, - "mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01", - "output-128k-2025-02-19": null, + "output-128k-2025-02-19": "output-128k-2025-02-19", "structured-output-2024-03-01": null, "per-turn-control-2026-07-01": "per-turn-control-2026-07-01", "prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05", @@ -71,9 +109,54 @@ "text_editor_20250124": null, "thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01", "thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18", - "token-efficient-tools-2025-02-19": null, + "token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19", "web-fetch-2025-09-10": "web-fetch-2025-09-10", - "web-search-2025-03-05": "web-search-2025-03-05" + "web-search-2025-03-05": "web-search-2025-03-05", + "agent-memory-2026-07-22": null, + "analytics-usage-cost-2026-08-24": "analytics-usage-cost-2026-08-24", + "cache-diagnosis-2026-04-07": "cache-diagnosis-2026-04-07", + "ce-plugins-2026-09-01": "ce-plugins-2026-09-01", + "ce-user-management-2026-07-13": "ce-user-management-2026-07-13", + "claude-code-20250219": "claude-code-20250219", + "code-execution-2025-05-22": "code-execution-2025-05-22", + "compact-2026-09-04": "compact-2026-09-04", + "computer-use-2024-10-22": "computer-use-2024-10-22", + "dev-full-thinking-2025-05-14": null, + "dreaming-2026-04-21": "dreaming-2026-04-21", + "experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21", + "extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11", + "fallback-credit-2026-06-01": "fallback-credit-2026-06-01", + "fallback-credit-2026-07-01": "fallback-credit-2026-07-01", + "inline-tools-2026-09-15": "inline-tools-2026-09-15", + "managed-agents-2026-04-01": "managed-agents-2026-04-01", + "mcp-client-2026-09-15": "mcp-client-2026-09-15", + "mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22", + "message-batches-2024-09-24": "message-batches-2024-09-24", + "message-threads-2026-08-12": "message-threads-2026-08-12", + "mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01", + "mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21", + "mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01", + "model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26", + "oauth-2025-04-20": "oauth-2025-04-20", + "output-300k-2026-03-24": "output-300k-2026-03-24", + "pdfs-2024-09-25": "pdfs-2024-09-25", + "prompt-caching-2024-07-31": "prompt-caching-2024-07-31", + "server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01", + "server-side-fallback-2026-07-01": "server-side-fallback-2026-07-01", + "spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26", + "task-budgets-2026-03-13": "task-budgets-2026-03-13", + "telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11", + "thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13", + "token-counting-2024-11-01": "token-counting-2024-11-01", + "tool-examples-2025-10-29": "tool-examples-2025-10-29", + "tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19", + "user-profiles-2026-03-24": "user-profiles-2026-03-24", + "user-profiles-2026-08-18": "user-profiles-2026-08-18", + "user-profiles-2026-09-04": "user-profiles-2026-09-04", + "webhooks-2026-10-07": "webhooks-2026-10-07", + "afk-mode-2026-01-31": "afk-mode-2026-01-31", + "redact-thinking-2026-02-12": "redact-thinking-2026-02-12", + "structured-outputs-2025-12-15": "structured-outputs-2025-12-15" }, "bedrock_converse": { "advisor-tool-2026-03-01": null, @@ -196,34 +279,92 @@ "context-1m-2025-08-07": "context-1m-2025-08-07", "context-management-2025-06-27": "context-management-2025-06-27", "dangerous-tool-use-2026-09-03": "dangerous-tool-use-2026-09-03", - "effort-2025-11-24": null, + "effort-2025-11-24": "effort-2025-11-24", "fast-mode-2026-02-01": null, "files-api-2025-04-14": null, - "fine-grained-tool-streaming-2025-05-14": null, + "fine-grained-tool-streaming-2025-05-14": "fine-grained-tool-streaming-2025-05-14", "interleaved-thinking-2025-05-14": "interleaved-thinking-2025-05-14", "mcp-client-2025-11-20": null, "mcp-client-2025-04-04": null, "mcp-servers-2025-12-04": null, - "mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01", - "output-128k-2025-02-19": null, + "output-128k-2025-02-19": "output-128k-2025-02-19", "structured-output-2024-03-01": null, "per-turn-control-2026-07-01": "per-turn-control-2026-07-01", "prompt-caching-scope-2026-01-05": null, "skills-2025-10-02": null, - "structured-outputs-2025-11-13": null, + "structured-outputs-2025-11-13": "structured-outputs-2025-11-13", "text_editor_20241022": null, "text_editor_20250124": null, "thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01", "thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18", - "token-efficient-tools-2025-02-19": null, + "token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19", "tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19", "web-fetch-2025-09-10": null, "web-search-2025-03-05": "web-search-2025-03-05", - "inline-tools-2026-09-15": "inline-tools-2026-09-15" + "inline-tools-2026-09-15": "inline-tools-2026-09-15", + "agent-memory-2026-07-22": null, + "analytics-usage-cost-2026-08-24": null, + "cache-diagnosis-2026-04-07": null, + "ce-plugins-2026-09-01": null, + "ce-user-management-2026-07-13": null, + "claude-code-20250219": "claude-code-20250219", + "code-execution-2025-05-22": null, + "compact-2026-09-04": null, + "computer-use-2024-10-22": null, + "dev-full-thinking-2025-05-14": null, + "dreaming-2026-04-21": null, + "experimental-claude-tag-2026-10-21": null, + "extended-cache-ttl-2025-04-11": null, + "fallback-credit-2026-06-01": "fallback-credit-2026-06-01", + "fallback-credit-2026-07-01": "fallback-credit-2026-07-01", + "managed-agents-2026-04-01": null, + "mcp-client-2026-09-15": null, + "mcp-tunnels-2026-06-22": null, + "message-batches-2024-09-24": null, + "message-threads-2026-08-12": null, + "mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01", + "mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21", + "mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01", + "model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26", + "oauth-2025-04-20": null, + "output-300k-2026-03-24": null, + "pdfs-2024-09-25": "pdfs-2024-09-25", + "prompt-caching-2024-07-31": null, + "server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01", + "server-side-fallback-2026-07-01": null, + "spend-limit-reads-2026-09-26": null, + "task-budgets-2026-03-13": "task-budgets-2026-03-13", + "telemetry-destinations-2026-08-11": null, + "thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13", + "token-counting-2024-11-01": "token-counting-2024-11-01", + "tool-examples-2025-10-29": "tool-examples-2025-10-29", + "user-profiles-2026-03-24": null, + "user-profiles-2026-08-18": null, + "user-profiles-2026-09-04": null, + "webhooks-2026-10-07": null, + "afk-mode-2026-01-31": "afk-mode-2026-01-31", + "redact-thinking-2026-02-12": null, + "structured-outputs-2025-12-15": "structured-outputs-2025-12-15" }, "vertex_ai_header_only": { + "ce-plugins-2026-09-01": "ce-plugins-2026-09-01", + "ce-user-management-2026-07-13": "ce-user-management-2026-07-13", "compact-2026-09-04": "compact-2026-09-04", - "inline-tools-2026-09-15": "inline-tools-2026-09-15" + "dreaming-2026-04-21": "dreaming-2026-04-21", + "experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21", + "extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11", + "inline-tools-2026-09-15": "inline-tools-2026-09-15", + "managed-agents-2026-04-01": "managed-agents-2026-04-01", + "mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22", + "oauth-2025-04-20": "oauth-2025-04-20", + "prompt-caching-2024-07-31": "prompt-caching-2024-07-31", + "prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05", + "redact-thinking-2026-02-12": "redact-thinking-2026-02-12", + "spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26", + "telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11", + "user-profiles-2026-03-24": "user-profiles-2026-03-24", + "user-profiles-2026-08-18": "user-profiles-2026-08-18", + "user-profiles-2026-09-04": "user-profiles-2026-09-04" }, "databricks": { "advisor-tool-2026-03-01": null, diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 4f5dc678bae..c9df903cbd6 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1297,7 +1297,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): return None return anthropic_beta_header.split(",") - def get_computer_tool_beta_header(self, computer_tool_version: str) -> str: + def get_computer_tool_beta_header(self, computer_tool_version: str) -> str | None: """ Get the appropriate beta header for a given computer tool version. @@ -1305,16 +1305,14 @@ class AnthropicModelInfo(BaseLLMModelInfo): computer_tool_version: The computer tool version (e.g., 'computer_20250124', 'computer_20241022') Returns: - The corresponding beta header string + The corresponding beta header string, or None for a version with no known beta """ computer_tool_beta_mapping: Final = { + "computer_20251124": "computer-use-2025-11-24", "computer_20250124": "computer-use-2025-01-24", "computer_20241022": "computer-use-2024-10-22", } - return computer_tool_beta_mapping.get( - computer_tool_version, - "computer-use-2024-10-22", # Default fallback - ) + return computer_tool_beta_mapping.get(computer_tool_version) def get_anthropic_beta_list( self, @@ -1346,9 +1344,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): if effort_used: betas.append(ANTHROPIC_EFFORT_BETA_HEADER) # effort-2025-11-24 - if computer_tool_used: - beta_header: Final = self.get_computer_tool_beta_header(computer_tool_used) - betas.append(beta_header) + if computer_tool_used and (computer_tool_beta := self.get_computer_tool_beta_header(computer_tool_used)): + betas.append(computer_tool_beta) # Anthropic no longer requires the prompt-caching beta header # Prompt caching now works automatically when cache_control is used in messages @@ -1437,9 +1434,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): # Anthropic no longer requires the prompt-caching beta header # Prompt caching now works automatically when cache_control is used in messages # Reference: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching - if computer_tool_used: - beta_header: Final = self.get_computer_tool_beta_header(computer_tool_used) - betas.add(beta_header) + if computer_tool_used and (computer_tool_beta := self.get_computer_tool_beta_header(computer_tool_used)): + betas.add(computer_tool_beta) # if pdf_used: # betas.add("pdfs-2024-09-25") if file_id_used: diff --git a/tests/e2e/llm_translation/test_messages_beta_headers_e2e.py b/tests/e2e/llm_translation/test_messages_beta_headers_e2e.py index 91a31201b0f..59035f8f6b6 100644 --- a/tests/e2e/llm_translation/test_messages_beta_headers_e2e.py +++ b/tests/e2e/llm_translation/test_messages_beta_headers_e2e.py @@ -15,48 +15,85 @@ pytestmark = pytest.mark.e2e _BETA_CONFIG: Final = files("litellm").joinpath("anthropic_beta_headers_config.json") _ANTHROPIC_MODEL: Final = "anthropic/claude-sonnet-5-5" +_VERTEX_MODEL: Final = "vertex_ai/claude-sonnet-5-5" +_VERTEX_LOCATION: Final = "global" +_AZURE_AI_MODEL: Final = "azure_ai/claude-fable-5-1" _BEDROCK_MODEL_ID: Final = "us.anthropic.claude-fable-5-1" _BETA_CONFIG_FILE: Final = TypeAdapter(dict[str, JsonValue]) _BETA_MAPPING: Final = TypeAdapter(dict[str, str | None]) _CODE_EXECUTION: Final[ToolUnionParam] = {"type": "code_execution_20250825", "name": "code_execution"} +# A live Vertex probe on 2026-10-31: each Claude 4.5 and 4.6 model takes one of these and Claude 5.5 takes neither +_VERTEX_PER_MODEL_BETAS: Final = frozenset({"computer-use-2025-01-24", "computer-use-2025-11-24"}) +_NO_PER_MODEL_BETAS: Final[frozenset[str]] = frozenset() -def _mapped_beta_headers(provider: str) -> str: - mapping: Final = _BETA_MAPPING.validate_python(_BETA_CONFIG_FILE.validate_json(_BETA_CONFIG.read_text())[provider]) - return ",".join(name for name, value in mapping.items() if value is not None) +def _mapped_beta_headers(provider: str, per_model: frozenset[str]) -> str: + config: Final = _BETA_CONFIG_FILE.validate_json(_BETA_CONFIG.read_text()) + mapping: Final = _BETA_MAPPING.validate_python(config[provider]) + header_only: Final = _BETA_MAPPING.validate_python(config.get(f"{provider}_header_only", {})) + forwarded: Final = {name for name, value in {**mapping, **header_only}.items() if value is not None} + assert per_model <= forwarded, per_model - forwarded + return ",".join(sorted(forwarded - per_model)) def _deployment(backend: str) -> LiteLLMParamsBody: - if backend.startswith("anthropic/"): - return LiteLLMParamsBody(model=backend, api_key="os.environ/ANTHROPIC_API_KEY") - return LiteLLMParamsBody( - model=backend, - aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", - aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", - aws_region_name="us-east-1", - ) + match backend.split("/", 1)[0]: + case "anthropic": + return LiteLLMParamsBody(model=backend, api_key="os.environ/ANTHROPIC_API_KEY") + case "vertex_ai": + return LiteLLMParamsBody( + model=backend, + vertex_project="os.environ/VERTEXAI_PROJECT", + vertex_location=_VERTEX_LOCATION, + vertex_credentials="os.environ/VERTEXAI_CREDENTIALS", + ) + case "azure_ai": + return LiteLLMParamsBody( + model=backend, + api_base="os.environ/AZURE_AI_API_BASE", + api_key="os.environ/AZURE_AI_API_KEY", + ) + case _: + return LiteLLMParamsBody( + model=backend, + aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", + aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", + aws_region_name="us-east-1", + ) class TestAnthropicBetaHeaders: @pytest.mark.parametrize( - ("backend", "beta_provider", "tools"), + ("backend", "beta_provider", "tools", "per_model"), [ - pytest.param(_ANTHROPIC_MODEL, "anthropic", [_CODE_EXECUTION], id="anthropic"), - pytest.param(f"bedrock/invoke/{_BEDROCK_MODEL_ID}", "bedrock", [], id="bedrock-invoke"), - pytest.param(f"bedrock/converse/{_BEDROCK_MODEL_ID}", "bedrock_converse", [], id="bedrock-converse"), + pytest.param(_ANTHROPIC_MODEL, "anthropic", [_CODE_EXECUTION], _NO_PER_MODEL_BETAS, id="anthropic"), + pytest.param(_VERTEX_MODEL, "vertex_ai", [], _VERTEX_PER_MODEL_BETAS, id="vertex-ai"), + pytest.param(_AZURE_AI_MODEL, "azure_ai", [], _NO_PER_MODEL_BETAS, id="azure-ai"), + pytest.param( + f"bedrock/invoke/{_BEDROCK_MODEL_ID}", "bedrock", [], _NO_PER_MODEL_BETAS, id="bedrock-invoke" + ), + pytest.param( + f"bedrock/converse/{_BEDROCK_MODEL_ID}", + "bedrock_converse", + [], + _NO_PER_MODEL_BETAS, + id="bedrock-converse", + ), ], ) @pytest.mark.covers( "llm.messages.anthropic.basic.nonstream.works", "llm.messages.bedrock_invoke.basic.nonstream.works", "llm.messages.bedrock_converse.basic.nonstream.works", + "llm.messages.vertex.basic.nonstream.works", + "llm.messages.azure_foundry.basic.nonstream.works", ) @meta( Subject( domain=Domain.LLM_TRANSLATION, route=Route.MESSAGES, - providers=(Provider.ANTHROPIC, Provider.BEDROCK), - models=(_ANTHROPIC_MODEL, _BEDROCK_MODEL_ID), + providers=(Provider.ANTHROPIC, Provider.BEDROCK, Provider.VERTEX_AI, Provider.AZURE_AI), + models=(_ANTHROPIC_MODEL, _BEDROCK_MODEL_ID, _VERTEX_MODEL, _AZURE_AI_MODEL), mode=Mode.NONSTREAM, ) ) @@ -68,6 +105,7 @@ class TestAnthropicBetaHeaders: backend: str, beta_provider: str, tools: list[ToolUnionParam], + per_model: frozenset[str], ) -> None: model: Final = f"e2e-messages-beta-{unique_marker()}" model_id: Final = proxy.create_model(model, _deployment(backend)) @@ -78,7 +116,7 @@ class TestAnthropicBetaHeaders: max_tokens=64, messages=[{"role": "user", "content": "Say 'hello' and nothing else"}], tools=tools, - extra_headers={"anthropic-beta": _mapped_beta_headers(beta_provider)}, + extra_headers={"anthropic-beta": _mapped_beta_headers(beta_provider, per_model)}, extra_body=NO_PROXY_CACHE, ) diff --git a/tests/integration/messages_endpoint/providers/anthropic/test_anthropic_safeguards_beta_wire.py b/tests/integration/messages_endpoint/providers/anthropic/test_anthropic_safeguards_beta_wire.py index 3ac1bdb0334..a1c5f6964bf 100644 --- a/tests/integration/messages_endpoint/providers/anthropic/test_anthropic_safeguards_beta_wire.py +++ b/tests/integration/messages_endpoint/providers/anthropic/test_anthropic_safeguards_beta_wire.py @@ -3,7 +3,7 @@ import json import re import time import uuid -from collections.abc import Callable, Mapping +from collections.abc import Callable, Iterator, Mapping from dataclasses import dataclass from pathlib import Path from types import MappingProxyType @@ -13,7 +13,7 @@ import anthropic import httpx import openai import pytest -from integration._support.client import Gateway, eventually, object_value +from integration._support.client import Gateway, eventually, gateway_from_environment, object_value from integration._support.database import read_rows from integration._support.process import owned_proxy from integration._support.vertex import service_account_json @@ -417,14 +417,31 @@ def test_a_deployment_dropping_context_management_sends_neither_the_field_nor_it _spend_row(f"msg_{marker}") +@pytest.fixture(scope="module") +def local_beta_map_gateway(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Gateway]: + with ( + gateway_from_environment() as shared, + owned_proxy( + shared, + tmp_path_factory.mktemp("safeguards-local-beta-map"), + {"LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS": "true"}, + ) as owned, + ): + yield owned + + @pytest.mark.parametrize("row", tuple(_FOUNDRY_SHAPES)) -def test_azure_ai_messages_forward_the_mapped_client_betas_beside_the_derived_one(gateway: Gateway, row: str) -> None: +def test_azure_ai_messages_forward_the_mapped_client_betas_beside_the_derived_one( + local_beta_map_gateway: Gateway, row: str +) -> None: case: Final = _FOUNDRY_SHAPES[row] marker: Final = uuid.uuid4().hex - with wire_server(anthropic_peer) as wire, gateway.scenario() as scenario: + with wire_server(anthropic_peer) as wire, local_beta_map_gateway.scenario() as scenario: model: Final = scenario.model(model=_FOUNDRY_MODEL, api_base=wire.url, api_key=_FOUNDRY_KEY) response: Final = _post( - gateway, messages_body(model, marker, safeguards=case.sent, stream=case.stream), beta=case.client_beta + local_beta_map_gateway, + messages_body(model, marker, safeguards=case.sent, stream=case.stream), + beta=case.client_beta, ) if case.stream: _assert_streamed(response, marker) diff --git a/tests/integration/messages_endpoint/test_beta_gated_fields_wire.py b/tests/integration/messages_endpoint/test_beta_gated_fields_wire.py index 26ad9f20897..6d36003dbfa 100644 --- a/tests/integration/messages_endpoint/test_beta_gated_fields_wire.py +++ b/tests/integration/messages_endpoint/test_beta_gated_fields_wire.py @@ -15,8 +15,8 @@ from pydantic import JsonValue, TypeAdapter pytestmark: Final = pytest.mark.shares_worker_state ANTHROPIC_MODEL: Final = "beta-gated-anthropic" -FOUNDRY_MODEL: Final = "beta-gated-foundry" -FOUNDRY_DROP_PARAMS_MODEL: Final = "beta-gated-foundry-drop-params" +DATABRICKS_MODEL: Final = "beta-gated-databricks" +DATABRICKS_DROP_PARAMS_MODEL: Final = "beta-gated-databricks-drop-params" CLEAR_AT_BETA: Final = "mid-conversation-system-clear-at-2026-08-21" _JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) _NONCE: Final = re.compile(rb"nonce-([0-9a-f]{32})") @@ -71,9 +71,16 @@ def write_config(directory: Path, wire_url: str) -> Path: { "model_list": [ deployment(ANTHROPIC_MODEL, "anthropic/claude-opus-4-8", f"{wire_url}/anthropic"), - deployment(FOUNDRY_MODEL, "azure_ai/claude-opus-4-8", f"{wire_url}/foundry"), deployment( - FOUNDRY_DROP_PARAMS_MODEL, "azure_ai/claude-opus-4-8", f"{wire_url}/foundry", drop_params=True + DATABRICKS_MODEL, + "databricks/databricks-claude-opus-4-8", + f"{wire_url}/databricks/serving-endpoints", + ), + deployment( + DATABRICKS_DROP_PARAMS_MODEL, + "databricks/databricks-claude-opus-4-8", + f"{wire_url}/databricks/serving-endpoints", + drop_params=True, ), ], "general_settings": { @@ -150,7 +157,7 @@ def system_message(body: bytes) -> dict[str, JsonValue]: def test_a_drop_params_leg_whose_row_lacks_the_beta_sends_neither_the_beta_nor_clear_at(rig: Rig) -> None: request_nonce: Final = nonce() - status, raw = rig.send(FOUNDRY_DROP_PARAMS_MODEL, request_nonce) + status, raw = rig.send(DATABRICKS_DROP_PARAMS_MODEL, request_nonce) assert reply_text(status, raw) == f"answered {request_nonce.removeprefix('nonce-')}" upstream: Final = rig.received(request_nonce) assert CLEAR_AT_BETA not in beta_header(upstream.headers), upstream.headers @@ -159,7 +166,7 @@ def test_a_drop_params_leg_whose_row_lacks_the_beta_sends_neither_the_beta_nor_c def test_a_leg_whose_row_lacks_the_beta_forwards_clear_at_as_sent_without_drop_params(rig: Rig) -> None: request_nonce: Final = nonce() - status, raw = rig.send(FOUNDRY_MODEL, request_nonce) + status, raw = rig.send(DATABRICKS_MODEL, request_nonce) assert reply_text(status, raw) == f"answered {request_nonce.removeprefix('nonce-')}" upstream: Final = rig.received(request_nonce) assert CLEAR_AT_BETA not in beta_header(upstream.headers), upstream.headers diff --git a/tests/integration/providers/test_azure_ai_claude_messages_wire.py b/tests/integration/providers/test_azure_ai_claude_messages_wire.py index c150de7520b..d39c6d2d2c1 100644 --- a/tests/integration/providers/test_azure_ai_claude_messages_wire.py +++ b/tests/integration/providers/test_azure_ai_claude_messages_wire.py @@ -1,10 +1,12 @@ +from collections.abc import Iterator from typing import Final import anthropic import pytest from anthropic.types import Message from integration._support import claude_code as cc -from integration._support.client import Gateway +from integration._support.client import Gateway, gateway_from_environment +from integration._support.process import owned_proxy from integration._support.wire import Reply, Request, wire_server from pydantic import JsonValue, TypeAdapter @@ -144,8 +146,23 @@ def test_azure_ai_claude_messages_preserves_target_auth_beta_and_typed_response( assert _JSON_OBJECT.validate_json(provider_request.body) == expected_provider_body, provider_request.body +@pytest.fixture(scope="module") +def local_beta_map_gateway(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Gateway]: + with ( + gateway_from_environment() as shared, + owned_proxy( + shared, + tmp_path_factory.mktemp("azure-ai-local-beta-map"), + {"LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS": "true"}, + ) as owned, + ): + yield owned + + @pytest.mark.parametrize("stream", [False, True], ids=["non_stream", "stream"]) -def test_azure_ai_claude_messages_filters_auto_injected_betas_by_azure_support(gateway: Gateway, stream: bool) -> None: +def test_azure_ai_claude_messages_forwards_the_auto_injected_betas_azure_accepts( + local_beta_map_gateway: Gateway, stream: bool +) -> None: identity: Final = f"msg_azure_claude_beta_filter_{stream}" api_path: Final = "/anthropic/v1/messages" reply_body: Final = cc.message_reply(identity, _BACKEND, _CONTENT, _USAGE) @@ -172,7 +189,7 @@ def test_azure_ai_claude_messages_filters_auto_injected_betas_by_azure_support(g "context_management": context_management, } expected_provider_body: Final = {**request_body_template, "model": _BACKEND} - expected_beta: Final = "structured-outputs-2025-11-13" + expected_beta: Final = "compact-2026-01-12,structured-outputs-2025-11-13" def respond(request: Request) -> Reply: assert request.method == "POST" and request.target == api_path, request.target @@ -185,7 +202,7 @@ def test_azure_ai_claude_messages_filters_auto_injected_betas_by_azure_support(g return Reply(content_type="text/event-stream", chunks=stream_reply) return Reply(body=reply_body) - with wire_server(respond) as wire, gateway.scenario() as scenario: + with wire_server(respond) as wire, local_beta_map_gateway.scenario() as scenario: model: Final = scenario.model( model=f"azure_ai/{_BACKEND}", api_base=wire.url, @@ -193,8 +210,8 @@ def test_azure_ai_claude_messages_filters_auto_injected_betas_by_azure_support(g num_retries=0, ) with anthropic.Anthropic( - api_key=gateway.key, - base_url=str(gateway.client.base_url), + api_key=local_beta_map_gateway.key, + base_url=str(local_beta_map_gateway.client.base_url), max_retries=0, ) as client: if stream: diff --git a/tests/integration/providers/test_vertex_anthropic_inline_tools_beta_wire.py b/tests/integration/providers/test_vertex_anthropic_inline_tools_beta_wire.py index 3175f2ec075..00f861678cb 100644 --- a/tests/integration/providers/test_vertex_anthropic_inline_tools_beta_wire.py +++ b/tests/integration/providers/test_vertex_anthropic_inline_tools_beta_wire.py @@ -25,7 +25,7 @@ from pydantic import JsonValue, TypeAdapter _INLINE_TOOLS: Final = "inline-tools-2026-09-15" _THINKING: Final = "interleaved-thinking-2025-05-14" -_VERTEX_UNSUPPORTED: Final = "effort-2025-11-24" +_VERTEX_UNSUPPORTED: Final = "skills-2025-10-02" _UNKNOWN: Final = "not-a-beta-2099-01-01" _BACKEND: Final = "claude-opus-5-5" _PROJECT: Final = "scripted-project" diff --git a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py index 636e23168cf..63a304ab77d 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -6788,6 +6788,34 @@ def test_eager_input_streaming_not_set_on_computer_use_tool(): assert "eager_input_streaming" not in mapped_tool +def _computer_tool_with_defer_loading_and_allowed_callers(tool_type: str) -> dict[str, object]: + return { + "type": tool_type, + "function": {"name": "computer", "parameters": {"display_width_px": 1024, "display_height_px": 768}}, + "defer_loading": True, + "allowed_callers": ["code_execution_20250825"], + } + + +@pytest.mark.parametrize("tool_type", ["computer_20241022", "computer_20250124"]) +def test_legacy_computer_tools_drop_defer_loading_and_allowed_callers(tool_type: str): + mapped_tool, _ = AnthropicConfig().map_tool_helper(_computer_tool_with_defer_loading_and_allowed_callers(tool_type)) + + assert mapped_tool["type"] == tool_type + assert "defer_loading" not in mapped_tool + assert "allowed_callers" not in mapped_tool + + +def test_computer_20251124_keeps_defer_loading_and_allowed_callers(): + mapped_tool, _ = AnthropicConfig().map_tool_helper( + _computer_tool_with_defer_loading_and_allowed_callers("computer_20251124") + ) + + assert mapped_tool["type"] == "computer_20251124" + assert mapped_tool["defer_loading"] is True + assert mapped_tool["allowed_callers"] == ["code_execution_20250825"] + + def test_eager_input_streaming_reaches_anthropic_request_tools(): result = AnthropicConfig().map_openai_params( non_default_params={"tools": [_eager_chat_tool(eager_input_streaming=True)], "stream": True}, @@ -7520,10 +7548,10 @@ def test_process_anthropic_headers_with_no_matching_headers(): @pytest.mark.parametrize( "computer_tool_used, prompt_caching_set, expected_beta_header", [ - (True, False, True), - (False, True, False), - (True, True, True), - (False, False, False), + ("computer_20250124", False, True), + (None, True, False), + ("computer_20250124", True, True), + (None, False, False), ], ) def test_anthropic_beta_header(computer_tool_used, prompt_caching_set, expected_beta_header): @@ -9233,6 +9261,7 @@ async def test_litellm_anthropic_prompt_caching_system( } +@pytest.mark.usefixtures("local_beta_headers_config") def test_completion_forwards_caller_anthropic_beta_header_with_computer_tool(respx_mock: respx.MockRouter): route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").mock( return_value=httpx.Response( @@ -9271,7 +9300,7 @@ def test_completion_forwards_caller_anthropic_beta_header_with_computer_tool(res sent_betas: Final = frozenset( beta.strip() for beta in route.calls.last.request.headers["anthropic-beta"].split(",") ) - assert sent_betas == frozenset({"computer-use-2025-01-24"}) + assert sent_betas == frozenset({"computer-use-2024-10-22", "computer-use-2025-01-24"}) assert json.loads(route.calls.last.request.content)["tools"] == [ { "type": "computer_20241022", @@ -9283,6 +9312,69 @@ def test_completion_forwards_caller_anthropic_beta_header_with_computer_tool(res ] +# Tool-to-beta pairs from Anthropic's computer use docs; api.anthropic.com answered 400 to +# computer_20251124 without computer-use-2025-11-24 and 200 with it on 2026-10-31 +@pytest.mark.usefixtures("local_beta_headers_config") +@pytest.mark.parametrize( + ("tool_type", "expected_beta"), + [ + ("computer_20241022", "computer-use-2024-10-22"), + ("computer_20250124", "computer-use-2025-01-24"), + ("computer_20251124", "computer-use-2025-11-24"), + ], +) +def test_completion_adds_the_beta_matching_the_computer_tool_version( + respx_mock: respx.MockRouter, tool_type: str, expected_beta: str +): + route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").mock( + return_value=httpx.Response(200, json=_anthropic_message_body([{"type": "text", "text": "hi"}], "end_turn")) + ) + + litellm.completion( + model="anthropic/claude-opus-5", + api_key="mock_api_key", + messages=[{"role": "user", "content": "Say hi"}], + tools=[ + { + "type": tool_type, + "function": { + "name": "computer", + "parameters": {"display_height_px": 768, "display_width_px": 1024}, + }, + } + ], + ) + + sent_betas: Final = frozenset( + beta.strip() for beta in route.calls.last.request.headers["anthropic-beta"].split(",") + ) + assert sent_betas == frozenset({expected_beta}) + + +@pytest.mark.usefixtures("local_beta_headers_config") +def test_completion_sends_no_computer_use_beta_for_an_unknown_computer_tool_version(respx_mock: respx.MockRouter): + route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").mock( + return_value=httpx.Response(200, json=_anthropic_message_body([{"type": "text", "text": "hi"}], "end_turn")) + ) + + litellm.completion( + model="anthropic/claude-opus-5", + api_key="mock_api_key", + messages=[{"role": "user", "content": "Say hi"}], + tools=[ + { + "type": "computer_20991231", + "function": { + "name": "computer", + "parameters": {"display_height_px": 768, "display_width_px": 1024}, + }, + } + ], + ) + + assert "anthropic-beta" not in route.calls.last.request.headers + + def _anthropic_message_body(content: list[dict[str, object]], stop_reason: str) -> dict[str, object]: return { "id": "msg_01XrAv7gc5tQNDuoADra7vB4", diff --git a/tests/unit/llms/custom_httpx/test_llm_http_handler.py b/tests/unit/llms/custom_httpx/test_llm_http_handler.py index a8f6772a76e..464ff01d16b 100644 --- a/tests/unit/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/unit/llms/custom_httpx/test_llm_http_handler.py @@ -1668,7 +1668,7 @@ _BOTH_FOUNDRY_BETAS: Final = ("context-1m-2025-08-07,context-management-2025-06- pytest.param( None, {"anthropic-beta": "context-1m-2025-08-07"}, - "claude-code-20250219", + "mcp-servers-2025-12-04", ("context-1m-2025-08-07",), id="unsupported-client-beta-is-dropped-and-deployment-beta-kept", ), diff --git a/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py index 0a582c95749..2be6794186e 100644 --- a/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py +++ b/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py @@ -640,36 +640,16 @@ async def test_vertex_ai_anthropic_prompt_caching_does_not_add_beta_header( assert "anthropic_beta" not in json.loads(request.content) -def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_header(): - """ - Test that remove_unsupported_beta correctly filters out prompt-caching-scope-2026-01-05 - from the anthropic-beta headers. - """ +def test_vertex_ai_partner_models_anthropic_forwards_prompt_caching_scope_beta_header( + local_beta_headers_config: None, +) -> None: + """A live Vertex rawPredict probe on 2026-10-28 accepted prompt-caching-scope-2026-01-05, so it goes upstream + while a beta missing from the vertex_ai column is still dropped.""" + headers: Final = update_headers_with_filtered_beta( + {"anthropic-beta": "other-feature,prompt-caching-scope-2026-01-05,web-search-2025-03-05"}, "vertex_ai" + ) - # This beta header should be removed - PROMPT_CACHING_BETA_HEADER = "prompt-caching-scope-2026-01-05" - headers = { - "anthropic-beta": f"other-feature,{PROMPT_CACHING_BETA_HEADER},web-search-2025-03-05" - } - - headers = update_headers_with_filtered_beta(headers, "vertex_ai") - - beta_header = headers.get("anthropic-beta") - assert PROMPT_CACHING_BETA_HEADER not in ( - beta_header or "" - ), f"{PROMPT_CACHING_BETA_HEADER} should be filtered out" - assert "other-feature" not in ( - beta_header or "" - ), "Other non-excluded beta headers should remain" - assert "web-search-2025-03-05" in ( - beta_header or "" - ), "Other non-excluded beta headers should remain" - # If prompt-caching was the only value, header should be removed completely - headers2 = {"anthropic-beta": PROMPT_CACHING_BETA_HEADER} - headers2 = update_headers_with_filtered_beta(headers2, "vertex_ai") - assert ( - "anthropic-beta" not in headers2 - ), "Header should be removed if no supported values remain" + assert headers == {"anthropic-beta": "prompt-caching-scope-2026-01-05,web-search-2025-03-05"} def test_vertex_ai_anthropic_output_config_effort_only_forwarded(): diff --git a/tests/unit/test_anthropic_beta_headers_filtering.py b/tests/unit/test_anthropic_beta_headers_filtering.py index c8fef95cf98..c33f207da74 100644 --- a/tests/unit/test_anthropic_beta_headers_filtering.py +++ b/tests/unit/test_anthropic_beta_headers_filtering.py @@ -10,10 +10,11 @@ This test validates: import json import os -from typing import Dict, Final, List +from typing import Dict, Final, List, get_args from unittest.mock import AsyncMock, MagicMock, patch import pytest +from anthropic.types.anthropic_beta_param import AnthropicBetaParam import litellm from litellm.anthropic_beta_headers_manager import ( @@ -25,6 +26,13 @@ from litellm.anthropic_beta_headers_manager import ( ) +def _literal_strings(annotation: object) -> frozenset[str]: + args: Final = get_args(annotation) + if not args: + return frozenset({annotation}) if isinstance(annotation, str) else frozenset() + return frozenset[str]().union(*(_literal_strings(arg) for arg in args)) + + class TestAnthropicBetaHeadersFiltering: """Test beta header filtering and mapping for all providers.""" @@ -50,21 +58,24 @@ class TestAnthropicBetaHeadersFiltering: """Get all beta headers from the anthropic provider config.""" return list(self.config.get("anthropic", {}).keys()) + def get_header_column(self, provider: str) -> Dict[str, str | None]: + return {**self.config.get(provider, {}), **self.config.get(f"{provider}_header_only", {})} + def get_supported_headers(self, provider: str) -> List[str]: """Get headers with non-null values for a provider.""" - provider_config = self.config.get(provider, {}) + provider_config = self.get_header_column(provider) return [ header for header, value in provider_config.items() if value is not None ] def get_unsupported_headers(self, provider: str) -> List[str]: """Get headers with null values for a provider.""" - provider_config = self.config.get(provider, {}) + provider_config = self.get_header_column(provider) return [header for header, value in provider_config.items() if value is None] def get_mapped_headers(self, provider: str) -> Dict[str, str]: """Get mapping of input headers to provider-specific headers.""" - provider_config = self.config.get(provider, {}) + provider_config = self.get_header_column(provider) return { header: value for header, value in provider_config.items() @@ -407,7 +418,15 @@ class TestAnthropicBetaHeadersFiltering: test_case["expected"] in filtered ), f"Header '{test_case['input']}' should be mapped to '{test_case['expected']}' for {test_case['provider']}, but got: {filtered}" - @pytest.mark.parametrize("beta", ["compact-2026-09-04", "inline-tools-2026-09-15"]) + @pytest.mark.parametrize( + "beta", + [ + "compact-2026-09-04", + "inline-tools-2026-09-15", + "prompt-caching-scope-2026-01-05", + "redact-thinking-2026-02-12", + ], + ) def test_vertex_ai_header_only_beta_is_forwarded_as_the_header_and_dropped_from_the_body( self, beta: str ) -> None: @@ -509,6 +528,44 @@ class TestAnthropicBetaHeadersFiltering: assert filtered == ["inline-tools-2026-09-15"] + @pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai"]) + def test_task_budgets_and_computer_use_betas_reach_every_platform(self, provider: str) -> None: + """Live probes on 2026-10-28 found all three platforms accept task-budgets-2026-03-13. On 2026-10-31 Vertex + ran the computer tool only with the beta each model takes (computer-use-2025-01-24 on Claude Sonnet 4.5 and + Haiku 4.5, computer-use-2025-11-24 on Claude Opus 4.5 and the 4.6 models) and answered 400 to the other, so + both reach Vertex and the model decides, the same as calling Vertex directly.""" + sent: Final = frozenset(("computer-use-2025-01-24", "computer-use-2025-11-24", "task-budgets-2026-03-13")) + + headers: Final = update_headers_with_filtered_beta({"anthropic-beta": ",".join(sorted(sent))}, provider) + + assert frozenset(headers["anthropic-beta"].split(",")) == sent + + @pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai"]) + def test_claude_code_betas_reach_the_probed_platforms(self, provider: str) -> None: + """Claude Code 2.1.299 sends these betas outside the anthropic SDK's list, and live probes on + 2026-10-28 found Anthropic, Vertex, and Azure AI Foundry accept each of them as the anthropic-beta header.""" + claude_code_betas: Final = frozenset( + ("afk-mode-2026-01-31", "redact-thinking-2026-02-12", "structured-outputs-2025-12-15") + ) + + headers: Final = update_headers_with_filtered_beta({"anthropic-beta": ",".join(claude_code_betas)}, provider) + + assert frozenset(headers["anthropic-beta"].split(",")) == claude_code_betas + + @pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai"]) + def test_every_anthropic_sdk_beta_has_a_decision_for_the_probed_platforms(self, provider: str) -> None: + sdk_betas: Final = _literal_strings(AnthropicBetaParam) + provider_column: Final = GetAnthropicBetaHeadersConfig.load_local_beta_headers_config()[provider] + + assert sdk_betas + assert sdk_betas - provider_column.keys() == frozenset() + + @pytest.mark.parametrize("provider", ["azure_ai", "vertex_ai"]) + def test_every_anthropic_beta_has_a_decision_for_vertex_ai_and_azure_ai(self, provider: str) -> None: + config: Final = GetAnthropicBetaHeadersConfig.load_local_beta_headers_config() + + assert config["anthropic"].keys() - config[provider].keys() == set() + def test_null_value_headers_filtered(self): """Test that headers with null values are always filtered out.""" for provider in [