fix(anthropic): override custom_llm_provider in provider config subclasses so capability probes use the right namespace

This commit is contained in:
mateo-berri 2026-07-11 12:11:17 -07:00
parent ae08bcc4ee
commit abc38935fa
11 changed files with 192 additions and 3 deletions

View file

@ -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,

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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