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 [