mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
test(anthropic): fold deployment-row tests into the filtering file and mirror the new beta in the Rust table
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
a524926eba
commit
07c4d4d7f1
4 changed files with 145 additions and 221 deletions
|
|
@ -98,6 +98,8 @@ pub enum AnthropicBeta {
|
|||
McpServers20251204,
|
||||
#[strum(serialize = "mid-conversation-output-config-2026-07-01")]
|
||||
MidConversationOutputConfig20260701,
|
||||
#[strum(serialize = "mid-conversation-system-clear-at-2026-08-21")]
|
||||
MidConversationSystemClearAt20260821,
|
||||
#[strum(serialize = "mid-conversation-tool-changes-2026-07-01")]
|
||||
MidConversationToolChanges20260701,
|
||||
#[strum(serialize = "output-128k-2025-02-19")]
|
||||
|
|
@ -129,7 +131,7 @@ pub enum AnthropicBeta {
|
|||
}
|
||||
|
||||
impl AnthropicBeta {
|
||||
pub const KNOWN: [Self; 40] = [
|
||||
pub const KNOWN: [Self; 41] = [
|
||||
Self::Oauth20250420,
|
||||
Self::WebFetch20250910,
|
||||
Self::WebSearch20250305,
|
||||
|
|
@ -157,6 +159,7 @@ impl AnthropicBeta {
|
|||
Self::McpClient20251120,
|
||||
Self::McpServers20251204,
|
||||
Self::MidConversationOutputConfig20260701,
|
||||
Self::MidConversationSystemClearAt20260821,
|
||||
Self::MidConversationToolChanges20260701,
|
||||
Self::Output128k20250219,
|
||||
Self::PromptCachingScope20260105,
|
||||
|
|
@ -257,10 +260,14 @@ impl AnthropicBeta {
|
|||
}
|
||||
Self::MidConversationOutputConfig20260701
|
||||
| Self::MidConversationToolChanges20260701 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::MidConversationSystemClearAt20260821 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Output128k20250219 | Self::TokenEfficientTools20250219 => &[
|
||||
|
|
@ -429,6 +436,7 @@ mod tests {
|
|||
AnthropicBeta::McpClient20251120,
|
||||
AnthropicBeta::McpServers20251204,
|
||||
AnthropicBeta::MidConversationOutputConfig20260701,
|
||||
AnthropicBeta::MidConversationSystemClearAt20260821,
|
||||
AnthropicBeta::MidConversationToolChanges20260701,
|
||||
AnthropicBeta::Output128k20250219,
|
||||
AnthropicBeta::PromptCachingScope20260105,
|
||||
|
|
|
|||
|
|
@ -5726,8 +5726,8 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro
|
|||
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
|
||||
anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None
|
||||
foundry_api_base = "https://my-resource.services.ai.azure.com/anthropic"
|
||||
upstream_response = httpx.Response(
|
||||
foundry_api_base: Final = "https://my-resource.services.ai.azure.com/anthropic"
|
||||
upstream_response: Final = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "msg_123",
|
||||
|
|
@ -5740,9 +5740,9 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro
|
|||
},
|
||||
request=httpx.Request("POST", f"{foundry_api_base}/v1/messages"),
|
||||
)
|
||||
mock_client = AsyncMock(spec=AsyncHTTPHandler)
|
||||
mock_client: Final = AsyncMock(spec=AsyncHTTPHandler)
|
||||
mock_client.post = AsyncMock(return_value=upstream_response)
|
||||
mock_logging_obj = Mock()
|
||||
mock_logging_obj: Final = Mock()
|
||||
mock_logging_obj.model_call_details = {}
|
||||
mock_logging_obj.stream = False
|
||||
mock_logging_obj.dynamic_success_callbacks = None
|
||||
|
|
@ -5761,5 +5761,5 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro
|
|||
kwargs={"extra_headers": {"anthropic-beta": "advisor-tool-2026-03-01,thinking-binding-controls-2026-08-01"}},
|
||||
)
|
||||
|
||||
sent_headers = mock_client.post.call_args.kwargs["headers"]
|
||||
sent_headers: Final = mock_client.post.call_args.kwargs["headers"]
|
||||
assert sent_headers["anthropic-beta"] == "thinking-binding-controls-2026-08-01"
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
"""The beta allowlist row is picked per deployment: an ``anthropic`` deployment on a Foundry host gets ``azure_ai``."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm import anthropic_beta_headers_manager
|
||||
from litellm.anthropic_beta_headers_manager import (
|
||||
is_foundry_api_base,
|
||||
resolve_beta_provider,
|
||||
update_headers_with_filtered_beta,
|
||||
update_request_with_filtered_beta,
|
||||
)
|
||||
|
||||
FOUNDRY_API_BASE: Final = "https://my-resource.services.ai.azure.com/anthropic"
|
||||
DIRECT_API_BASE: Final = "https://api.anthropic.com"
|
||||
DIRECT_ONLY_BETA: Final = "advisor-tool-2026-03-01"
|
||||
PRESERVED_THINKING_BETAS: Final = (
|
||||
"thinking-binding-controls-2026-08-01",
|
||||
"mid-conversation-system-clear-at-2026-08-21",
|
||||
"mid-conversation-tool-changes-2026-07-01",
|
||||
"mid-conversation-output-config-2026-07-01",
|
||||
"thinking-display-updates-2026-08-18",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _bundled_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
|
||||
anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None
|
||||
|
||||
|
||||
def _bundled_rows() -> dict[str, dict[str, str | None]]:
|
||||
with open(os.path.join(os.path.dirname(litellm.__file__), "anthropic_beta_headers_config.json")) as f:
|
||||
rows: dict[str, dict[str, str | None]] = json.load(f)
|
||||
return rows
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_base", "expected"),
|
||||
(
|
||||
(FOUNDRY_API_BASE, True),
|
||||
("https://my-resource.openai.azure.com", True),
|
||||
("my-resource.services.ai.azure.com/anthropic", True),
|
||||
(DIRECT_API_BASE, False),
|
||||
("https://services.ai.azure.com.example.com", False),
|
||||
("", False),
|
||||
(None, False),
|
||||
),
|
||||
)
|
||||
def test_is_foundry_api_base_matches_only_foundry_hosts(api_base: str | None, expected: bool) -> None:
|
||||
assert is_foundry_api_base(api_base) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("provider", "api_base", "expected"),
|
||||
(
|
||||
("anthropic", FOUNDRY_API_BASE, "azure_ai"),
|
||||
("anthropic", DIRECT_API_BASE, "anthropic"),
|
||||
("anthropic", None, "anthropic"),
|
||||
("vertex_ai", FOUNDRY_API_BASE, "vertex_ai"),
|
||||
("bedrock_converse", None, "bedrock_converse"),
|
||||
),
|
||||
)
|
||||
def test_resolve_beta_provider_picks_the_row_per_deployment(provider: str, api_base: str | None, expected: str) -> None:
|
||||
assert resolve_beta_provider(provider, api_base) == expected
|
||||
|
||||
|
||||
def test_foundry_deployment_headers_follow_the_azure_ai_row() -> None:
|
||||
headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers=headers, provider="anthropic", api_base=FOUNDRY_API_BASE
|
||||
)
|
||||
assert result == {"anthropic-beta": "thinking-binding-controls-2026-08-01"}
|
||||
|
||||
|
||||
def test_direct_deployment_headers_keep_every_anthropic_row_beta() -> None:
|
||||
headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers=headers, provider="anthropic", api_base=DIRECT_API_BASE
|
||||
)
|
||||
assert result == {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
|
||||
|
||||
def test_foundry_deployment_request_filters_header_and_body_betas_together() -> None:
|
||||
headers: dict[str, str]
|
||||
data: dict[str, object]
|
||||
headers, data = update_request_with_filtered_beta(
|
||||
headers={"anthropic-beta": DIRECT_ONLY_BETA},
|
||||
request_data={"anthropic_beta": [DIRECT_ONLY_BETA, "thinking-binding-controls-2026-08-01"], "max_tokens": 8},
|
||||
provider="anthropic",
|
||||
api_base=FOUNDRY_API_BASE,
|
||||
)
|
||||
assert headers == {}
|
||||
assert data == {"anthropic_beta": ["thinking-binding-controls-2026-08-01"], "max_tokens": 8}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ("anthropic", "azure_ai", "vertex_ai"))
|
||||
@pytest.mark.parametrize("beta", PRESERVED_THINKING_BETAS)
|
||||
def test_live_verified_platforms_forward_every_preserved_thinking_beta(provider: str, beta: str) -> None:
|
||||
assert _bundled_rows()[provider][beta] == beta
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ("bedrock", "bedrock_converse", "bedrock_mantle", "databricks"))
|
||||
def test_unverified_platforms_hold_back_the_system_clear_beta(provider: str) -> None:
|
||||
assert _bundled_rows()[provider]["mid-conversation-system-clear-at-2026-08-21"] is None
|
||||
assert (
|
||||
update_headers_with_filtered_beta(
|
||||
headers={"anthropic-beta": "mid-conversation-system-clear-at-2026-08-21"}, provider=provider
|
||||
)
|
||||
== {}
|
||||
)
|
||||
|
|
@ -16,7 +16,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm import anthropic_beta_headers_manager
|
||||
from litellm.anthropic_beta_headers_manager import (
|
||||
resolve_beta_provider,
|
||||
is_foundry_api_base,
|
||||
GetAnthropicBetaHeadersConfig,
|
||||
filter_and_transform_beta_headers,
|
||||
get_beta_headers_config,
|
||||
|
|
@ -53,9 +56,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
def get_supported_headers(self, provider: str) -> List[str]:
|
||||
"""Get headers with non-null values for a provider."""
|
||||
provider_config = self.config.get(provider, {})
|
||||
return [
|
||||
header for header, value in provider_config.items() if value is not None
|
||||
]
|
||||
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."""
|
||||
|
|
@ -65,11 +66,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
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, {})
|
||||
return {
|
||||
header: value
|
||||
for header, value in provider_config.items()
|
||||
if value is not None
|
||||
}
|
||||
return {header: value for header, value in provider_config.items() if value is not None}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider",
|
||||
|
|
@ -82,24 +79,20 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
unsupported_headers = self.get_unsupported_headers(provider)
|
||||
mapped_headers = self.get_mapped_headers(provider)
|
||||
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=all_headers, provider=provider
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=all_headers, provider=provider)
|
||||
assert ("compact-2026-09-04" in filtered) is (provider == "anthropic")
|
||||
|
||||
for header in unsupported_headers:
|
||||
assert (
|
||||
header not in filtered
|
||||
), f"Unsupported header '{header}' should be filtered out for {provider}"
|
||||
assert (
|
||||
mapped_headers.get(header) not in filtered
|
||||
), f"Mapped value of unsupported header '{header}' should not appear for {provider}"
|
||||
assert header not in filtered, f"Unsupported header '{header}' should be filtered out for {provider}"
|
||||
assert mapped_headers.get(header) not in filtered, (
|
||||
f"Mapped value of unsupported header '{header}' should not appear for {provider}"
|
||||
)
|
||||
|
||||
for header in supported_headers:
|
||||
expected_mapped = mapped_headers[header]
|
||||
assert (
|
||||
expected_mapped in filtered
|
||||
), f"Supported header '{header}' should be mapped to '{expected_mapped}' for {provider}"
|
||||
assert expected_mapped in filtered, (
|
||||
f"Supported header '{header}' should be mapped to '{expected_mapped}' for {provider}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider",
|
||||
|
|
@ -114,20 +107,14 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
]
|
||||
all_headers = self.get_all_beta_headers() + unknown_headers
|
||||
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=all_headers, provider=provider
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=all_headers, provider=provider)
|
||||
|
||||
for unknown in unknown_headers:
|
||||
assert (
|
||||
unknown not in filtered
|
||||
), f"Unknown header '{unknown}' should be filtered out for {provider}"
|
||||
assert unknown not in filtered, f"Unknown header '{unknown}' should be filtered out for {provider}"
|
||||
|
||||
def test_update_request_with_filtered_beta_vertex_ai(self):
|
||||
"""Test combined filtering for both HTTP headers and request body betas."""
|
||||
headers = {
|
||||
"anthropic-beta": "files-api-2025-04-14,context-management-2025-06-27,code-execution-2025-05-22"
|
||||
}
|
||||
headers = {"anthropic-beta": "files-api-2025-04-14,context-management-2025-06-27,code-execution-2025-05-22"}
|
||||
request_data = {
|
||||
"anthropic_beta": [
|
||||
"files-api-2025-04-14",
|
||||
|
|
@ -143,9 +130,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
)
|
||||
|
||||
assert filtered_headers.get("anthropic-beta") == "context-management-2025-06-27"
|
||||
assert filtered_request_data.get("anthropic_beta") == [
|
||||
"context-management-2025-06-27"
|
||||
]
|
||||
assert filtered_request_data.get("anthropic_beta") == ["context-management-2025-06-27"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_messages_http_headers_filtering(self):
|
||||
|
|
@ -153,9 +138,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
all_headers = self.get_all_beta_headers()
|
||||
unsupported = self.get_unsupported_headers("anthropic")
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.get_async_httpx_client"
|
||||
) as mock_client_factory:
|
||||
with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
|
|
@ -191,9 +174,9 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
if beta_header:
|
||||
beta_values = [b.strip() for b in beta_header.split(",")]
|
||||
for unsupported_header in unsupported:
|
||||
assert (
|
||||
unsupported_header not in beta_values
|
||||
), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Anthropic"
|
||||
assert unsupported_header not in beta_values, (
|
||||
f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Anthropic"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_azure_ai_messages_http_headers_filtering(self):
|
||||
|
|
@ -201,9 +184,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
all_headers = self.get_all_beta_headers()
|
||||
unsupported = self.get_unsupported_headers("azure_ai")
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.get_async_httpx_client"
|
||||
) as mock_client_factory:
|
||||
with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
|
|
@ -241,9 +222,9 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
if beta_header:
|
||||
beta_values = [b.strip() for b in beta_header.split(",")]
|
||||
for unsupported_header in unsupported:
|
||||
assert (
|
||||
unsupported_header not in beta_values
|
||||
), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Azure AI"
|
||||
assert unsupported_header not in beta_values, (
|
||||
f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Azure AI"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_converse_headers_and_body_filtering(self):
|
||||
|
|
@ -256,9 +237,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"output": {
|
||||
"message": {"role": "assistant", "content": [{"text": "Hello"}]}
|
||||
},
|
||||
"output": {"message": {"role": "assistant", "content": [{"text": "Hello"}]}},
|
||||
"stopReason": "end_turn",
|
||||
"usage": {"inputTokens": 10, "outputTokens": 20},
|
||||
}
|
||||
|
|
@ -290,27 +269,25 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
if beta_header:
|
||||
beta_values = [b.strip() for b in beta_header.split(",")]
|
||||
for unsupported_header in unsupported:
|
||||
assert (
|
||||
unsupported_header not in beta_values
|
||||
), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Bedrock Converse"
|
||||
assert unsupported_header not in beta_values, (
|
||||
f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Bedrock Converse"
|
||||
)
|
||||
|
||||
data = call_kwargs.get("data")
|
||||
if data:
|
||||
body = json.loads(data)
|
||||
body_beta = body.get("additionalModelRequestFields", {}).get(
|
||||
"anthropic_beta", []
|
||||
)
|
||||
body_beta = body.get("additionalModelRequestFields", {}).get("anthropic_beta", [])
|
||||
|
||||
for unsupported_header in unsupported:
|
||||
assert (
|
||||
unsupported_header not in body_beta
|
||||
), f"Unsupported header '{unsupported_header}' should not be in request body for Bedrock Converse"
|
||||
assert unsupported_header not in body_beta, (
|
||||
f"Unsupported header '{unsupported_header}' should not be in request body for Bedrock Converse"
|
||||
)
|
||||
|
||||
for header, mapped_value in mapped_headers.items():
|
||||
if header in all_headers and mapped_value in body_beta:
|
||||
assert (
|
||||
mapped_value in body_beta
|
||||
), f"Supported header '{header}' should be mapped to '{mapped_value}' in request body for Bedrock Converse"
|
||||
assert mapped_value in body_beta, (
|
||||
f"Supported header '{header}' should be mapped to '{mapped_value}' in request body for Bedrock Converse"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vertex_ai_messages_http_headers_filtering(self):
|
||||
|
|
@ -318,9 +295,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
all_headers = self.get_all_beta_headers()
|
||||
unsupported = self.get_unsupported_headers("vertex_ai")
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.get_async_httpx_client"
|
||||
) as mock_client_factory:
|
||||
with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
|
|
@ -338,9 +313,7 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
mock_client.post = AsyncMock(return_value=mock_response)
|
||||
mock_client_factory.return_value = mock_client
|
||||
|
||||
with patch(
|
||||
"litellm.llms.vertex_ai.vertex_llm_base.VertexBase._ensure_access_token"
|
||||
) as mock_token:
|
||||
with patch("litellm.llms.vertex_ai.vertex_llm_base.VertexBase._ensure_access_token") as mock_token:
|
||||
mock_token.return_value = ("test-token", "test-project")
|
||||
|
||||
try:
|
||||
|
|
@ -363,9 +336,9 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
if beta_header:
|
||||
beta_values = [b.strip() for b in beta_header.split(",")]
|
||||
for unsupported_header in unsupported:
|
||||
assert (
|
||||
unsupported_header not in beta_values
|
||||
), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Vertex AI"
|
||||
assert unsupported_header not in beta_values, (
|
||||
f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Vertex AI"
|
||||
)
|
||||
|
||||
def test_header_mapping_correctness(self):
|
||||
"""Test that headers are mapped correctly for providers with transformations."""
|
||||
|
|
@ -402,17 +375,15 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
beta_headers=[test_case["input"]], provider=test_case["provider"]
|
||||
)
|
||||
|
||||
assert (
|
||||
test_case["expected"] in filtered
|
||||
), f"Header '{test_case['input']}' should be mapped to '{test_case['expected']}' for {test_case['provider']}, but got: {filtered}"
|
||||
assert test_case["expected"] in filtered, (
|
||||
f"Header '{test_case['input']}' should be mapped to '{test_case['expected']}' for {test_case['provider']}, but got: {filtered}"
|
||||
)
|
||||
|
||||
def test_filter_and_transform_beta_headers_vertex_ai_keeps_compact(self):
|
||||
"""Vertex AI supports compact context edits, so the compact beta header
|
||||
must be forwarded instead of stripped (it was previously mapped to null,
|
||||
which broke compact_20260112 context edits over /v1/messages)."""
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=["compact-2026-01-12"], provider="vertex_ai"
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=["compact-2026-01-12"], provider="vertex_ai")
|
||||
|
||||
assert filtered == ["compact-2026-01-12"]
|
||||
|
||||
|
|
@ -486,13 +457,11 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
unsupported = self.get_unsupported_headers(provider)
|
||||
|
||||
if unsupported:
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=unsupported, provider=provider
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=unsupported, provider=provider)
|
||||
|
||||
assert (
|
||||
len(filtered) == 0
|
||||
), f"All null-value headers should be filtered out for {provider}, but got: {filtered}"
|
||||
assert len(filtered) == 0, (
|
||||
f"All null-value headers should be filtered out for {provider}, but got: {filtered}"
|
||||
)
|
||||
|
||||
def test_empty_headers_list(self):
|
||||
"""Test that empty headers list returns empty result."""
|
||||
|
|
@ -503,13 +472,9 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
"bedrock",
|
||||
"vertex_ai",
|
||||
]:
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=[], provider=provider
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=[], provider=provider)
|
||||
|
||||
assert (
|
||||
len(filtered) == 0
|
||||
), f"Empty headers list should return empty result for {provider}"
|
||||
assert len(filtered) == 0, f"Empty headers list should return empty result for {provider}"
|
||||
|
||||
def test_mixed_supported_and_unsupported_headers(self):
|
||||
"""Test filtering with a mix of supported, unsupported, and unknown headers."""
|
||||
|
|
@ -529,20 +494,12 @@ class TestAnthropicBetaHeadersFiltering:
|
|||
|
||||
test_headers = [supported[0]] + [unsupported[0]] + ["unknown-header-123"]
|
||||
|
||||
filtered = filter_and_transform_beta_headers(
|
||||
beta_headers=test_headers, provider=provider
|
||||
)
|
||||
filtered = filter_and_transform_beta_headers(beta_headers=test_headers, provider=provider)
|
||||
|
||||
expected_mapped = mapped_headers[supported[0]]
|
||||
assert (
|
||||
expected_mapped in filtered
|
||||
), f"Supported header should be in result for {provider}"
|
||||
assert (
|
||||
unsupported[0] not in filtered
|
||||
), f"Unsupported header should not be in result for {provider}"
|
||||
assert (
|
||||
"unknown-header-123" not in filtered
|
||||
), f"Unknown header should not be in result for {provider}"
|
||||
assert expected_mapped in filtered, f"Supported header should be in result for {provider}"
|
||||
assert unsupported[0] not in filtered, f"Unsupported header should not be in result for {provider}"
|
||||
assert "unknown-header-123" not in filtered, f"Unknown header should not be in result for {provider}"
|
||||
|
||||
@pytest.mark.parametrize("provider", ["anthropic", "bedrock", "bedrock_mantle", "vertex_ai"])
|
||||
def test_blank_anthropic_beta_header_is_removed(self, provider):
|
||||
|
|
@ -570,3 +527,77 @@ def test_local_flags_load_bundled_beta_headers_without_fetching(monkeypatch, fla
|
|||
config = get_beta_headers_config(url="https://example.test/anthropic_beta_headers_config.json")
|
||||
remote_get.assert_not_called()
|
||||
assert config == GetAnthropicBetaHeadersConfig.load_local_beta_headers_config()
|
||||
|
||||
|
||||
FOUNDRY_API_BASE: Final = "https://my-resource.services.ai.azure.com/anthropic"
|
||||
DIRECT_API_BASE: Final = "https://api.anthropic.com"
|
||||
DIRECT_ONLY_BETA: Final = "advisor-tool-2026-03-01"
|
||||
PRESERVED_THINKING_BETAS: Final = (
|
||||
"thinking-binding-controls-2026-08-01",
|
||||
"mid-conversation-system-clear-at-2026-08-21",
|
||||
"mid-conversation-tool-changes-2026-07-01",
|
||||
"mid-conversation-output-config-2026-07-01",
|
||||
"thinking-display-updates-2026-08-18",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bundled_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
|
||||
anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None
|
||||
|
||||
|
||||
def test_foundry_deployment_headers_follow_the_azure_ai_row(bundled_config: None) -> None:
|
||||
headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers=headers, provider="anthropic", api_base=FOUNDRY_API_BASE
|
||||
)
|
||||
assert result == {"anthropic-beta": "thinking-binding-controls-2026-08-01"}
|
||||
|
||||
|
||||
def test_direct_deployment_headers_keep_every_anthropic_row_beta(bundled_config: None) -> None:
|
||||
headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers=headers, provider="anthropic", api_base=DIRECT_API_BASE
|
||||
)
|
||||
assert result == {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"}
|
||||
|
||||
|
||||
def test_foundry_deployment_request_filters_header_and_body_betas_together(bundled_config: None) -> None:
|
||||
headers: dict[str, str]
|
||||
data: dict[str, object]
|
||||
headers, data = update_request_with_filtered_beta(
|
||||
headers={"anthropic-beta": DIRECT_ONLY_BETA},
|
||||
request_data={"anthropic_beta": [DIRECT_ONLY_BETA, "thinking-binding-controls-2026-08-01"], "max_tokens": 8},
|
||||
provider="anthropic",
|
||||
api_base=FOUNDRY_API_BASE,
|
||||
)
|
||||
assert headers == {}
|
||||
assert data == {"anthropic_beta": ["thinking-binding-controls-2026-08-01"], "max_tokens": 8}
|
||||
|
||||
|
||||
PRESERVED_THINKING_BETAS: Final = (
|
||||
"thinking-binding-controls-2026-08-01",
|
||||
"mid-conversation-system-clear-at-2026-08-21",
|
||||
"mid-conversation-tool-changes-2026-07-01",
|
||||
"mid-conversation-output-config-2026-07-01",
|
||||
"thinking-display-updates-2026-08-18",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai"])
|
||||
def test_live_verified_platforms_forward_every_preserved_thinking_beta(bundled_config: None, provider: str) -> None:
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers={"anthropic-beta": ",".join(PRESERVED_THINKING_BETAS)}, provider=provider
|
||||
)
|
||||
assert sorted(result["anthropic-beta"].split(",")) == sorted(PRESERVED_THINKING_BETAS), result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai", "bedrock", "bedrock_converse"])
|
||||
def test_each_row_forwards_exactly_its_verified_preserved_thinking_cells(bundled_config: None, provider: str) -> None:
|
||||
row: Final[dict[str, str | None]] = GetAnthropicBetaHeadersConfig.load_local_beta_headers_config()[provider]
|
||||
forwarded: Final = tuple(beta for beta in PRESERVED_THINKING_BETAS if row.get(beta) is not None)
|
||||
result: dict[str, str] = update_headers_with_filtered_beta(
|
||||
headers={"anthropic-beta": ",".join(PRESERVED_THINKING_BETAS)}, provider=provider
|
||||
)
|
||||
assert sorted(result["anthropic-beta"].split(",")) == sorted(forwarded), (provider, result)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue