mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(anthropic): override custom_llm_provider in provider config subclasses so capability probes use the right namespace
This commit is contained in:
parent
ae08bcc4ee
commit
abc38935fa
11 changed files with 192 additions and 3 deletions
|
|
@ -359,16 +359,40 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
value = litellm.model_cost.get(model, {}).get(key)
|
||||
return value if isinstance(value, bool) else None
|
||||
|
||||
@staticmethod
|
||||
def _get_provider_resolved_capability(model: str, key: str, custom_llm_provider: str) -> Optional[bool]:
|
||||
"""Resolve boolean capability ``key`` for ``model`` under the caller's provider.
|
||||
|
||||
Returns the flag when the provider-aware lookup resolves ``model`` to an
|
||||
entry (or fallback rule) that sets it explicitly, and ``None`` when the
|
||||
model does not resolve under that provider or the resolved entry has no
|
||||
opinion on ``key``.
|
||||
"""
|
||||
from litellm.utils import _get_model_info_helper
|
||||
|
||||
try:
|
||||
resolved_model, resolved_provider, _, _ = litellm.get_llm_provider(
|
||||
model=model, custom_llm_provider=custom_llm_provider
|
||||
)
|
||||
value = _get_model_info_helper(model=resolved_model, custom_llm_provider=resolved_provider).get(key)
|
||||
except Exception: # noqa: BLE001 # _get_model_info_helper raises bare Exception for unmapped models
|
||||
return None
|
||||
return value if isinstance(value, bool) else None
|
||||
|
||||
@staticmethod
|
||||
def _supports_model_capability(model: str, key: str, custom_llm_provider: str) -> bool:
|
||||
"""Check a boolean capability ``key`` in the model map under the caller's provider.
|
||||
|
||||
The provider-aware lookup makes exact provider-namespaced entries (e.g. the
|
||||
Bedrock ``global.anthropic.*`` ids) authoritative; the raw model-map walk
|
||||
remains as a provider-less backstop for alias forms the lookup misses.
|
||||
The provider-aware lookup is authoritative when it resolves an explicit flag,
|
||||
so ``key: false`` on the provider-namespaced entry wins over every fallback.
|
||||
Otherwise ``_supports_factory``'s provider-level fallbacks and the raw
|
||||
model-map walk remain as backstops for alias forms the lookup misses.
|
||||
"""
|
||||
from litellm.utils import _supports_factory
|
||||
|
||||
resolved = AnthropicModelInfo._get_provider_resolved_capability(model, key, custom_llm_provider)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
try:
|
||||
if _supports_factory(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
and Azure endpoint format.
|
||||
"""
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "azure_ai"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,10 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
if key != "self" and value is not None:
|
||||
setattr(self.__class__, key, value)
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "databricks"
|
||||
|
||||
@classmethod
|
||||
def get_config(cls):
|
||||
return super().get_config()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ class GithubCopilotAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
super().__init__()
|
||||
self.authenticator = Authenticator()
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "github_copilot"
|
||||
|
||||
def handles_web_search_natively(self) -> bool:
|
||||
"""
|
||||
Copilot's /v1/messages endpoint does not execute ``web_search`` tools, so
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ class JSONProviderAnthropicMessagesConfig(OpenAILikeAnthropicMessagesConfig):
|
|||
super().__init__()
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return self._provider.slug
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ from ..output_params_utils import sanitize_vertex_anthropic_output_params
|
|||
|
||||
|
||||
class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, VertexBase):
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "vertex_ai"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -331,3 +331,59 @@ class TestProviderConfigManagerAzureAnthropicMessages:
|
|||
)
|
||||
|
||||
assert config is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_model_cost_map(monkeypatch):
|
||||
"""Force the bundled backup cost map so capability flags match this branch."""
|
||||
import litellm
|
||||
|
||||
original = litellm.model_cost
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
litellm.get_model_info.cache_clear()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
litellm.model_cost = original
|
||||
litellm.get_model_info.cache_clear()
|
||||
|
||||
|
||||
def test_messages_thinking_shape_follows_exact_azure_entry_flag(local_model_cost_map, monkeypatch):
|
||||
"""The Azure messages config must probe capabilities under ``azure_ai`` so an
|
||||
operator setting ``supports_adaptive_thinking: false`` on the exact
|
||||
``azure_ai/claude-opus-4-8`` entry beats the unmodified ``anthropic`` entry.
|
||||
With the inherited ``"anthropic"`` provider default the flip was ignored and
|
||||
the transform kept emitting ``thinking.type='adaptive'``."""
|
||||
import litellm
|
||||
|
||||
config = AzureAnthropicMessagesConfig()
|
||||
|
||||
def transform():
|
||||
return config.transform_anthropic_messages_request(
|
||||
model="claude-opus-4-8",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_optional_request_params={
|
||||
"max_tokens": 4096,
|
||||
"reasoning_effort": "medium",
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
result = transform()
|
||||
assert result.get("thinking") == {"type": "adaptive"}
|
||||
assert result.get("output_config") == {"effort": "medium"}
|
||||
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost["azure_ai/claude-opus-4-8"], "supports_adaptive_thinking", False
|
||||
)
|
||||
litellm.get_model_info.cache_clear()
|
||||
assert litellm.model_cost["claude-opus-4-8"]["supports_adaptive_thinking"] is True
|
||||
|
||||
flipped = transform()
|
||||
thinking = flipped.get("thinking")
|
||||
assert isinstance(thinking, dict)
|
||||
assert thinking.get("type") == "enabled"
|
||||
assert isinstance(thinking.get("budget_tokens"), int)
|
||||
assert "output_config" not in flipped
|
||||
|
|
|
|||
|
|
@ -416,3 +416,10 @@ def test_transform_request_keeps_parallel_tool_calls_for_claude():
|
|||
)["messages"]
|
||||
|
||||
assert len([m for m in result if m.get("role") == "assistant"]) == 1
|
||||
|
||||
|
||||
def test_databricks_config_probes_capabilities_under_databricks_namespace():
|
||||
"""Inherited AnthropicConfig capability probes read ``self.custom_llm_provider``;
|
||||
without this override they probed the ``anthropic`` cost-map namespace and
|
||||
ignored the exact ``databricks/databricks-claude-*`` entries."""
|
||||
assert DatabricksConfig().custom_llm_provider == "databricks"
|
||||
|
|
|
|||
|
|
@ -326,3 +326,11 @@ def test_github_copilot_config_does_not_handle_web_search_natively():
|
|||
|
||||
assert GithubCopilotAnthropicMessagesConfig().handles_web_search_natively() is False
|
||||
assert AnthropicMessagesConfig().handles_web_search_natively() is True
|
||||
|
||||
|
||||
def test_github_copilot_messages_config_probes_capabilities_under_copilot_namespace():
|
||||
"""Capability probes in the shared pass-through helpers read
|
||||
``self.custom_llm_provider``; without this override they probed the
|
||||
``anthropic`` namespace and ignored the exact ``github_copilot/claude-*``
|
||||
cost-map entries."""
|
||||
assert GithubCopilotAnthropicMessagesConfig().custom_llm_provider == "github_copilot"
|
||||
|
|
|
|||
|
|
@ -299,3 +299,21 @@ def test_anthropic_beta_survives_provider_filter_on_passthrough_path(config):
|
|||
|
||||
stripped = update_headers_with_filtered_beta(headers=dict(headers), provider="openai")
|
||||
assert "anthropic-beta" not in stripped
|
||||
|
||||
|
||||
def test_json_provider_messages_config_probes_capabilities_under_provider_slug():
|
||||
"""Capability probes in the shared pass-through helpers read
|
||||
``self.custom_llm_provider``. The JSON-provider config knows its slug, so it
|
||||
must expose it; the generic OpenAI-like config has no class-level namespace
|
||||
and keeps the inherited ``anthropic`` default."""
|
||||
from litellm.llms.openai_like.json_loader import SimpleProviderConfig
|
||||
from litellm.llms.openai_like.messages.transformation import (
|
||||
JSONProviderAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
provider = SimpleProviderConfig(
|
||||
slug="exampleprovider",
|
||||
data={"base_url": "https://api.example.com/v1", "api_key_env": "EXAMPLE_API_KEY"},
|
||||
)
|
||||
assert JSONProviderAnthropicMessagesConfig(provider).custom_llm_provider == "exampleprovider"
|
||||
assert OpenAILikeAnthropicMessagesConfig().custom_llm_provider == "anthropic"
|
||||
|
|
|
|||
|
|
@ -509,3 +509,59 @@ def test_vertex_claude_completion_does_not_mutate_shared_extra_headers():
|
|||
assert (
|
||||
shared_extra_headers == {}
|
||||
), "extra_headers must not be mutated by completion()"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_model_cost_map(monkeypatch):
|
||||
"""Force the bundled backup cost map so capability flags match this branch."""
|
||||
import litellm
|
||||
|
||||
original = litellm.model_cost
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
litellm.get_model_info.cache_clear()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
litellm.model_cost = original
|
||||
litellm.get_model_info.cache_clear()
|
||||
|
||||
|
||||
def test_messages_thinking_shape_follows_exact_vertex_entry_flag(local_model_cost_map, monkeypatch):
|
||||
"""The Vertex messages config must probe capabilities under ``vertex_ai`` so an
|
||||
operator setting ``supports_adaptive_thinking: false`` on the exact
|
||||
``vertex_ai/claude-opus-4-8`` entry beats the unmodified ``anthropic`` entry.
|
||||
With the inherited ``"anthropic"`` provider default the flip was ignored and
|
||||
the transform kept emitting ``thinking.type='adaptive'``."""
|
||||
import litellm
|
||||
|
||||
config = VertexAIPartnerModelsAnthropicMessagesConfig()
|
||||
|
||||
def transform():
|
||||
return config.transform_anthropic_messages_request(
|
||||
model="claude-opus-4-8",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_optional_request_params={
|
||||
"max_tokens": 4096,
|
||||
"reasoning_effort": "medium",
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
result = transform()
|
||||
assert result.get("thinking") == {"type": "adaptive"}
|
||||
assert result.get("output_config") == {"effort": "medium"}
|
||||
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost["vertex_ai/claude-opus-4-8"], "supports_adaptive_thinking", False
|
||||
)
|
||||
litellm.get_model_info.cache_clear()
|
||||
assert litellm.model_cost["claude-opus-4-8"]["supports_adaptive_thinking"] is True
|
||||
|
||||
flipped = transform()
|
||||
thinking = flipped.get("thinking")
|
||||
assert isinstance(thinking, dict)
|
||||
assert thinking.get("type") == "enabled"
|
||||
assert isinstance(thinking.get("budget_tokens"), int)
|
||||
assert "output_config" not in flipped
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue