mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
feat(rust): add the Anthropic beta header policy (#45419)
* feat(rust): add the Anthropic beta header policy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): resolve known betas held as Other in AnthropicBeta::on Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(rust): use named rstest cases for Other beta resolution Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs(rust): make the rstest named-case rule explicit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(rust): move BetaPolicy tests to the public API test crate Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
56bafc29cf
commit
9505af0639
6 changed files with 519 additions and 3 deletions
|
|
@ -17,6 +17,9 @@ For string-valued enums and their Serde conversions, follow [.agents/skills/rust
|
|||
|
||||
Use [`#[rstest]`](https://docs.rs/rstest/latest/rstest/attr.rstest.html) for new and updated tests and [`#[fixture]`](https://docs.rs/rstest/latest/rstest/attr.fixture.html) for reusable setup, injected through typed test arguments. Express input variations as named `#[case::name(...)]` cases instead of loops or duplicated tests so each failure identifies its case. Keep behavior assertions in the test body and fixtures focused on setup. Use the workspace `rstest` dependency
|
||||
|
||||
- Never loop over inputs (`for`, `.iter().for_each`, `.all`) inside a test body; give each input its own `#[case::name(...)]`, or use `#[values(...)]` for a cross product
|
||||
- Exception: a test pinning a Rust table against a repo-owned data file (for example `include_str!` of a JSON config) may iterate that file's entries
|
||||
|
||||
## Error definitions
|
||||
|
||||
- A crate's errors live in `src/error.rs`, defined with `thiserror`, and re-exported from `lib.rs`
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ These links describe the provider-specific wire types in this directory. Shared
|
|||
|
||||
## anthropic.rs
|
||||
|
||||
`AnthropicBeta` and `BetaSet` represent the `anthropic-beta` header values and their wire spelling
|
||||
`AnthropicBeta`, `BetaSet` and `BetaProvider` represent the `anthropic-beta` header values, their
|
||||
wire spelling and per-host support
|
||||
|
||||
- https://platform.claude.com/docs/en/api/beta-headers.md
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,34 @@ use std::{
|
|||
str::FromStr,
|
||||
};
|
||||
|
||||
/// A provider column of `litellm/anthropic_beta_headers_config.json`: which betas a host accepts
|
||||
/// and under which name.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, Hash, strum::AsRefStr, strum::Display, strum::EnumString,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum BetaProvider {
|
||||
Anthropic,
|
||||
AzureAi,
|
||||
BedrockConverse,
|
||||
Bedrock,
|
||||
BedrockMantle,
|
||||
VertexAi,
|
||||
Databricks,
|
||||
}
|
||||
|
||||
impl BetaProvider {
|
||||
pub const ALL: &[Self] = &[
|
||||
Self::Anthropic,
|
||||
Self::AzureAi,
|
||||
Self::BedrockConverse,
|
||||
Self::Bedrock,
|
||||
Self::BedrockMantle,
|
||||
Self::VertexAi,
|
||||
Self::Databricks,
|
||||
];
|
||||
}
|
||||
|
||||
/// One value of the `anthropic-beta` header. Equality, ordering and hashing follow the wire
|
||||
/// string, so a value parsed from a caller's header never disagrees with the matching variant.
|
||||
#[derive(Clone, Debug, strum::AsRefStr, strum::Display, strum::EnumString)]
|
||||
|
|
@ -35,12 +63,66 @@ pub enum AnthropicBeta {
|
|||
PerTurnControl20260701,
|
||||
#[strum(serialize = "dangerous-tool-use-2026-09-03")]
|
||||
DangerousToolUse20260903,
|
||||
#[strum(serialize = "bash_20241022")]
|
||||
Bash20241022,
|
||||
#[strum(serialize = "bash_20250124")]
|
||||
Bash20250124,
|
||||
#[strum(serialize = "claude-code-20250219")]
|
||||
ClaudeCode20250219,
|
||||
#[strum(serialize = "code-execution-2025-08-25")]
|
||||
CodeExecution20250825,
|
||||
#[strum(serialize = "computer-use-2025-01-24")]
|
||||
ComputerUse20250124,
|
||||
#[strum(serialize = "computer-use-2025-11-24")]
|
||||
ComputerUse20251124,
|
||||
#[strum(serialize = "context-1m-2025-08-07")]
|
||||
Context1m20250807,
|
||||
#[strum(serialize = "effort-2025-11-24")]
|
||||
Effort20251124,
|
||||
#[strum(serialize = "files-api-2025-04-14")]
|
||||
FilesApi20250414,
|
||||
#[strum(serialize = "fine-grained-tool-streaming-2025-05-14")]
|
||||
FineGrainedToolStreaming20250514,
|
||||
#[strum(serialize = "interleaved-thinking-2025-05-14")]
|
||||
InterleavedThinking20250514,
|
||||
#[strum(serialize = "mcp-client-2025-04-04")]
|
||||
McpClient20250404,
|
||||
#[strum(serialize = "mcp-client-2025-11-20")]
|
||||
McpClient20251120,
|
||||
#[strum(serialize = "mcp-servers-2025-12-04")]
|
||||
McpServers20251204,
|
||||
#[strum(serialize = "mid-conversation-output-config-2026-07-01")]
|
||||
MidConversationOutputConfig20260701,
|
||||
#[strum(serialize = "mid-conversation-tool-changes-2026-07-01")]
|
||||
MidConversationToolChanges20260701,
|
||||
#[strum(serialize = "output-128k-2025-02-19")]
|
||||
Output128k20250219,
|
||||
#[strum(serialize = "prompt-caching-scope-2026-01-05")]
|
||||
PromptCachingScope20260105,
|
||||
#[strum(serialize = "skills-2025-10-02")]
|
||||
Skills20251002,
|
||||
#[strum(serialize = "structured-output-2024-03-01")]
|
||||
StructuredOutput20240301,
|
||||
#[strum(serialize = "text_editor_20241022")]
|
||||
TextEditor20241022,
|
||||
#[strum(serialize = "text_editor_20250124")]
|
||||
TextEditor20250124,
|
||||
#[strum(serialize = "thinking-binding-controls-2026-08-01")]
|
||||
ThinkingBindingControls20260801,
|
||||
#[strum(serialize = "thinking-display-updates-2026-08-18")]
|
||||
ThinkingDisplayUpdates20260818,
|
||||
#[strum(serialize = "token-efficient-tools-2025-02-19")]
|
||||
TokenEfficientTools20250219,
|
||||
#[strum(serialize = "tool-examples-2025-10-29")]
|
||||
ToolExamples20251029,
|
||||
#[strum(serialize = "tool-search-tool-2025-10-19")]
|
||||
ToolSearchTool20251019,
|
||||
#[strum(default, transparent)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl AnthropicBeta {
|
||||
pub const KNOWN: [Self; 12] = [
|
||||
pub const KNOWN: [Self; 39] = [
|
||||
Self::Oauth20250420,
|
||||
Self::WebFetch20250910,
|
||||
Self::WebSearch20250305,
|
||||
|
|
@ -53,11 +135,145 @@ impl AnthropicBeta {
|
|||
Self::AdvisorTool20260301,
|
||||
Self::PerTurnControl20260701,
|
||||
Self::DangerousToolUse20260903,
|
||||
Self::Bash20241022,
|
||||
Self::Bash20250124,
|
||||
Self::ClaudeCode20250219,
|
||||
Self::CodeExecution20250825,
|
||||
Self::ComputerUse20250124,
|
||||
Self::ComputerUse20251124,
|
||||
Self::Context1m20250807,
|
||||
Self::Effort20251124,
|
||||
Self::FilesApi20250414,
|
||||
Self::FineGrainedToolStreaming20250514,
|
||||
Self::InterleavedThinking20250514,
|
||||
Self::McpClient20250404,
|
||||
Self::McpClient20251120,
|
||||
Self::McpServers20251204,
|
||||
Self::MidConversationOutputConfig20260701,
|
||||
Self::MidConversationToolChanges20260701,
|
||||
Self::Output128k20250219,
|
||||
Self::PromptCachingScope20260105,
|
||||
Self::Skills20251002,
|
||||
Self::StructuredOutput20240301,
|
||||
Self::TextEditor20241022,
|
||||
Self::TextEditor20250124,
|
||||
Self::ThinkingBindingControls20260801,
|
||||
Self::ThinkingDisplayUpdates20260818,
|
||||
Self::TokenEfficientTools20250219,
|
||||
Self::ToolExamples20251029,
|
||||
Self::ToolSearchTool20251019,
|
||||
];
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub fn on(&self, provider: BetaProvider) -> Option<Self> {
|
||||
let resolved = match self {
|
||||
Self::Other(raw) => raw.parse().unwrap(),
|
||||
_ => self.clone(),
|
||||
};
|
||||
if resolved.rejected_by().contains(&provider) {
|
||||
return None;
|
||||
}
|
||||
Some(match (resolved, provider) {
|
||||
(
|
||||
Self::AdvancedToolUse20251120,
|
||||
BetaProvider::Bedrock | BetaProvider::BedrockMantle | BetaProvider::VertexAi,
|
||||
) => Self::ToolSearchTool20251019,
|
||||
(beta, _) => beta,
|
||||
})
|
||||
}
|
||||
|
||||
fn rejected_by(&self) -> &'static [BetaProvider] {
|
||||
match self {
|
||||
Self::AdvancedToolUse20251120 | Self::ContextManagement20250627 => {
|
||||
&[BetaProvider::BedrockConverse]
|
||||
}
|
||||
Self::AdvisorTool20260301 | Self::Compact20260904 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Bash20241022
|
||||
| Self::Bash20250124
|
||||
| Self::McpServers20251204
|
||||
| Self::StructuredOutput20240301
|
||||
| Self::TextEditor20241022
|
||||
| Self::TextEditor20250124 => BetaProvider::ALL,
|
||||
Self::ClaudeCode20250219 | Self::ToolExamples20251029 => &[
|
||||
BetaProvider::Anthropic,
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::CodeExecution20250825
|
||||
| Self::FilesApi20250414
|
||||
| Self::McpClient20250404
|
||||
| Self::McpClient20251120
|
||||
| Self::PromptCachingScope20260105
|
||||
| Self::Skills20251002
|
||||
| Self::WebFetch20250910 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
],
|
||||
Self::Compact20260112 => &[BetaProvider::AzureAi, BetaProvider::BedrockConverse],
|
||||
Self::ComputerUse20250124 | Self::ComputerUse20251124 | Self::Context1m20250807 => &[],
|
||||
Self::DangerousToolUse20260903 => {
|
||||
&[BetaProvider::BedrockConverse, BetaProvider::Databricks]
|
||||
}
|
||||
Self::Effort20251124 => &[BetaProvider::VertexAi],
|
||||
Self::FastMode20260201 | Self::Oauth20250420 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
],
|
||||
Self::FineGrainedToolStreaming20250514 => {
|
||||
&[BetaProvider::AzureAi, BetaProvider::VertexAi]
|
||||
}
|
||||
Self::InterleavedThinking20250514 | Self::WebSearch20250305 => {
|
||||
&[BetaProvider::BedrockConverse, BetaProvider::Bedrock]
|
||||
}
|
||||
Self::MidConversationOutputConfig20260701
|
||||
| Self::MidConversationToolChanges20260701 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
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::ThinkingBindingControls20260801 => &[BetaProvider::AzureAi],
|
||||
Self::ThinkingDisplayUpdates20260818 => &[BetaProvider::Databricks],
|
||||
Self::ToolSearchTool20251019 => &[
|
||||
BetaProvider::Anthropic,
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Other(_) => BetaProvider::ALL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnthropicBeta {
|
||||
|
|
@ -149,10 +365,20 @@ impl fmt::Display for BetaSet {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn beta_headers_config() -> IndexMap<String, serde_json::Value> {
|
||||
serde_json::from_str(include_str!(
|
||||
"../../../../../litellm/anthropic_beta_headers_config.json"
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn set(header: &str) -> BetaSet {
|
||||
header.parse().unwrap_or_else(|never| match never {})
|
||||
}
|
||||
|
|
@ -171,7 +397,34 @@ mod tests {
|
|||
AnthropicBeta::FastMode20260201,
|
||||
AnthropicBeta::AdvisorTool20260301,
|
||||
AnthropicBeta::PerTurnControl20260701,
|
||||
AnthropicBeta::DangerousToolUse20260903
|
||||
AnthropicBeta::DangerousToolUse20260903,
|
||||
AnthropicBeta::Bash20241022,
|
||||
AnthropicBeta::Bash20250124,
|
||||
AnthropicBeta::ClaudeCode20250219,
|
||||
AnthropicBeta::CodeExecution20250825,
|
||||
AnthropicBeta::ComputerUse20250124,
|
||||
AnthropicBeta::ComputerUse20251124,
|
||||
AnthropicBeta::Context1m20250807,
|
||||
AnthropicBeta::Effort20251124,
|
||||
AnthropicBeta::FilesApi20250414,
|
||||
AnthropicBeta::FineGrainedToolStreaming20250514,
|
||||
AnthropicBeta::InterleavedThinking20250514,
|
||||
AnthropicBeta::McpClient20250404,
|
||||
AnthropicBeta::McpClient20251120,
|
||||
AnthropicBeta::McpServers20251204,
|
||||
AnthropicBeta::MidConversationOutputConfig20260701,
|
||||
AnthropicBeta::MidConversationToolChanges20260701,
|
||||
AnthropicBeta::Output128k20250219,
|
||||
AnthropicBeta::PromptCachingScope20260105,
|
||||
AnthropicBeta::Skills20251002,
|
||||
AnthropicBeta::StructuredOutput20240301,
|
||||
AnthropicBeta::TextEditor20241022,
|
||||
AnthropicBeta::TextEditor20250124,
|
||||
AnthropicBeta::ThinkingBindingControls20260801,
|
||||
AnthropicBeta::ThinkingDisplayUpdates20260818,
|
||||
AnthropicBeta::TokenEfficientTools20250219,
|
||||
AnthropicBeta::ToolExamples20251029,
|
||||
AnthropicBeta::ToolSearchTool20251019
|
||||
)]
|
||||
beta: AnthropicBeta,
|
||||
) {
|
||||
|
|
@ -181,6 +434,117 @@ mod tests {
|
|||
assert!(AnthropicBeta::KNOWN.contains(&beta));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn known_betas_are_exactly_the_config_keys() {
|
||||
let config = beta_headers_config();
|
||||
let config_keys: BTreeSet<String> = config
|
||||
.iter()
|
||||
.filter(|(provider, _)| provider.as_str() != "description")
|
||||
.flat_map(|(_, column)| {
|
||||
column
|
||||
.as_object()
|
||||
.into_iter()
|
||||
.flat_map(|values| values.keys().cloned())
|
||||
})
|
||||
.collect();
|
||||
let known_keys: BTreeSet<String> = AnthropicBeta::KNOWN
|
||||
.iter()
|
||||
.map(|beta| beta.as_str().to_string())
|
||||
.collect();
|
||||
assert_eq!(known_keys, config_keys);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::anthropic(BetaProvider::Anthropic)]
|
||||
#[case::azure_ai(BetaProvider::AzureAi)]
|
||||
#[case::bedrock_converse(BetaProvider::BedrockConverse)]
|
||||
#[case::bedrock(BetaProvider::Bedrock)]
|
||||
#[case::bedrock_mantle(BetaProvider::BedrockMantle)]
|
||||
#[case::vertex_ai(BetaProvider::VertexAi)]
|
||||
#[case::databricks(BetaProvider::Databricks)]
|
||||
fn provider_columns_match_beta_provider_all(#[case] provider: BetaProvider) {
|
||||
let config = beta_headers_config();
|
||||
let config_columns: Vec<String> = config
|
||||
.keys()
|
||||
.filter(|column| column.as_str() != "description")
|
||||
.cloned()
|
||||
.collect();
|
||||
let beta_provider_columns: Vec<String> =
|
||||
BetaProvider::ALL.iter().map(ToString::to_string).collect();
|
||||
assert_eq!(config_columns, beta_provider_columns);
|
||||
assert_eq!(
|
||||
provider.to_string().parse::<BetaProvider>().unwrap(),
|
||||
provider
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn on_matches_every_config_cell() {
|
||||
let config = beta_headers_config();
|
||||
for provider in BetaProvider::ALL {
|
||||
for beta in &AnthropicBeta::KNOWN {
|
||||
let expected = config
|
||||
.get(provider.as_ref())
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|column| column.get(beta.as_str()))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string);
|
||||
let actual = beta.on(*provider).map(|name| name.to_string());
|
||||
assert_eq!(actual, expected, "provider {provider}, beta {beta}");
|
||||
if matches!(beta, AnthropicBeta::AdvancedToolUse20251120)
|
||||
&& matches!(
|
||||
provider,
|
||||
BetaProvider::Bedrock
|
||||
| BetaProvider::BedrockMantle
|
||||
| BetaProvider::VertexAi
|
||||
)
|
||||
{
|
||||
let parsed: AnthropicBeta = actual.as_deref().unwrap().parse().unwrap();
|
||||
assert!(AnthropicBeta::KNOWN.contains(&parsed));
|
||||
assert!(!matches!(parsed, AnthropicBeta::Other(_)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::renamed(
|
||||
"advanced-tool-use-2025-11-20",
|
||||
BetaProvider::Bedrock,
|
||||
Some(AnthropicBeta::ToolSearchTool20251019)
|
||||
)]
|
||||
#[case::kept(
|
||||
"oauth-2025-04-20",
|
||||
BetaProvider::Anthropic,
|
||||
Some(AnthropicBeta::Oauth20250420)
|
||||
)]
|
||||
#[case::rejected("effort-2025-11-24", BetaProvider::VertexAi, None)]
|
||||
#[case::unknown("example-beta-2099-01-01", BetaProvider::Anthropic, None)]
|
||||
fn on_resolves_known_spellings_held_as_other(
|
||||
#[case] raw: &str,
|
||||
#[case] provider: BetaProvider,
|
||||
#[case] expected: Option<AnthropicBeta>,
|
||||
) {
|
||||
let actual = AnthropicBeta::Other(raw.to_string()).on(provider);
|
||||
assert_eq!(actual, expected);
|
||||
assert!(!matches!(actual, Some(AnthropicBeta::Other(_))));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::anthropic(BetaProvider::Anthropic)]
|
||||
#[case::azure_ai(BetaProvider::AzureAi)]
|
||||
#[case::bedrock_converse(BetaProvider::BedrockConverse)]
|
||||
#[case::bedrock(BetaProvider::Bedrock)]
|
||||
#[case::bedrock_mantle(BetaProvider::BedrockMantle)]
|
||||
#[case::vertex_ai(BetaProvider::VertexAi)]
|
||||
#[case::databricks(BetaProvider::Databricks)]
|
||||
fn unknown_beta_is_rejected_by_every_provider(#[case] provider: BetaProvider) {
|
||||
assert_eq!(
|
||||
AnthropicBeta::Other("example-beta-2099-01-01".into()).on(provider),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_values_are_kept_verbatim() {
|
||||
let parsed: AnthropicBeta = "claude-code-20250219".parse().unwrap();
|
||||
|
|
|
|||
36
litellm-rust/crates/llms/src/anthropic/beta_headers.rs
Normal file
36
litellm-rust/crates/llms/src/anthropic/beta_headers.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use litellm_http::request::{with_header, without_headers};
|
||||
use litellm_llms_types::providers::anthropic::{BetaProvider, BetaSet};
|
||||
|
||||
use crate::{anthropic::common_utils::existing_betas, base_llm::auth::Headers};
|
||||
|
||||
const BETA_HEADER: &str = "anthropic-beta";
|
||||
|
||||
/// How the `anthropic-beta` header is treated before a request leaves for a host.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BetaPolicy {
|
||||
/// Send the header untouched and let the host decide.
|
||||
Forward,
|
||||
/// Keep only the betas the provider accepts, under the names it expects.
|
||||
Filter(BetaProvider),
|
||||
/// Remove every `anthropic-beta` header.
|
||||
Drop,
|
||||
}
|
||||
|
||||
impl BetaPolicy {
|
||||
pub fn apply(self, headers: Headers) -> Headers {
|
||||
let provider = match self {
|
||||
Self::Forward => return headers,
|
||||
Self::Drop => return without_headers(headers, &[BETA_HEADER]),
|
||||
Self::Filter(provider) => provider,
|
||||
};
|
||||
let accepted: BetaSet = existing_betas(&headers)
|
||||
.iter()
|
||||
.filter_map(|beta| beta.on(provider))
|
||||
.collect();
|
||||
let headers = without_headers(headers, &[BETA_HEADER]);
|
||||
if accepted.is_empty() {
|
||||
return headers;
|
||||
}
|
||||
with_header(headers, BETA_HEADER, accepted.to_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod beta_headers;
|
||||
pub mod common_utils;
|
||||
|
||||
pub mod batches;
|
||||
|
|
|
|||
111
litellm-rust/crates/llms/tests/anthropic_beta_headers.rs
Normal file
111
litellm-rust/crates/llms/tests/anthropic_beta_headers.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use litellm_llms::anthropic::beta_headers::BetaPolicy;
|
||||
use litellm_llms_types::providers::anthropic::BetaProvider;
|
||||
use rstest::rstest;
|
||||
|
||||
fn headers(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(name, value)| (name.to_string(), value.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn forward_keeps_headers_as_is() {
|
||||
let input = headers(&[
|
||||
(
|
||||
"Anthropic-Beta",
|
||||
"example-beta-2099-01-01,web-search-2025-03-05",
|
||||
),
|
||||
("x-api-key", "k"),
|
||||
]);
|
||||
assert_eq!(BetaPolicy::Forward.apply(input.clone()), input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn drop_removes_every_casing() {
|
||||
assert_eq!(
|
||||
BetaPolicy::Drop.apply(headers(&[
|
||||
("anthropic-beta", "a"),
|
||||
("x-api-key", "k"),
|
||||
("ANTHROPIC-BETA", "b"),
|
||||
])),
|
||||
headers(&[("x-api-key", "k")])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::bedrock(BetaProvider::Bedrock, Some("tool-search-tool-2025-10-19"))]
|
||||
#[case::bedrock_mantle(BetaProvider::BedrockMantle, Some("tool-search-tool-2025-10-19"))]
|
||||
#[case::vertex_ai(BetaProvider::VertexAi, Some("tool-search-tool-2025-10-19"))]
|
||||
#[case::anthropic(BetaProvider::Anthropic, Some("advanced-tool-use-2025-11-20"))]
|
||||
#[case::azure_ai(BetaProvider::AzureAi, Some("advanced-tool-use-2025-11-20"))]
|
||||
#[case::databricks(BetaProvider::Databricks, Some("advanced-tool-use-2025-11-20"))]
|
||||
#[case::bedrock_converse(BetaProvider::BedrockConverse, None)]
|
||||
fn filter_renames_per_host(#[case] provider: BetaProvider, #[case] expected_beta: Option<&str>) {
|
||||
let expected = match expected_beta {
|
||||
Some(beta) => headers(&[("x-api-key", "k"), ("anthropic-beta", beta)]),
|
||||
None => headers(&[("x-api-key", "k")]),
|
||||
};
|
||||
assert_eq!(
|
||||
BetaPolicy::Filter(provider).apply(headers(&[
|
||||
("Anthropic-Beta", "advanced-tool-use-2025-11-20"),
|
||||
("x-api-key", "k"),
|
||||
])),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::azure_ai(BetaProvider::AzureAi, "fast-mode-2026-02-01,example-beta-2099-01-01")]
|
||||
#[case::anthropic(BetaProvider::Anthropic, "bash_20241022")]
|
||||
#[case::vertex_ai(BetaProvider::VertexAi, " , ")]
|
||||
fn filter_removes_header_when_nothing_survives(
|
||||
#[case] provider: BetaProvider,
|
||||
#[case] beta_value: &str,
|
||||
) {
|
||||
assert_eq!(
|
||||
BetaPolicy::Filter(provider).apply(headers(&[
|
||||
("x-api-key", "k"),
|
||||
("anthropic-beta", beta_value)
|
||||
])),
|
||||
headers(&[("x-api-key", "k")])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::anthropic(
|
||||
BetaProvider::Anthropic,
|
||||
"web-search-2025-03-05",
|
||||
"oauth-2025-04-20,web-search-2025-03-05,example-beta-2099-01-01",
|
||||
"oauth-2025-04-20,web-search-2025-03-05"
|
||||
)]
|
||||
#[case::vertex_ai_renames_and_deduplicates(
|
||||
BetaProvider::VertexAi,
|
||||
"advanced-tool-use-2025-11-20",
|
||||
"tool-search-tool-2025-10-19",
|
||||
"tool-search-tool-2025-10-19"
|
||||
)]
|
||||
fn filter_deduplicates_and_sorts(
|
||||
#[case] provider: BetaProvider,
|
||||
#[case] first_beta: &str,
|
||||
#[case] second_beta: &str,
|
||||
#[case] expected_beta: &str,
|
||||
) {
|
||||
assert_eq!(
|
||||
BetaPolicy::Filter(provider).apply(headers(&[
|
||||
("ANTHROPIC-BETA", first_beta),
|
||||
("x-api-key", "k"),
|
||||
("anthropic-beta", second_beta),
|
||||
])),
|
||||
headers(&[("x-api-key", "k"), ("anthropic-beta", expected_beta)])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::forward(BetaPolicy::Forward)]
|
||||
#[case::drop(BetaPolicy::Drop)]
|
||||
#[case::filter(BetaPolicy::Filter(BetaProvider::Bedrock))]
|
||||
fn headers_without_a_beta_are_untouched(#[case] policy: BetaPolicy) {
|
||||
let input = headers(&[("x-api-key", "k"), ("anthropic-version", "2023")]);
|
||||
assert_eq!(policy.apply(input.clone()), input);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue