fix(anthropic): forward service_tier on chat completions to Anthropic and Azure (#46461)

* fix(anthropic): forward service_tier on chat completions to Anthropic and Azure

Chat completions calls to Claude with service_tier were rejected client-side
(or dropped with drop_params), so the tier never reached Anthropic. Map auto
and standard_only through, send OpenAI's default as standard_only, and raise
a 400 with a drop_params hint for values Anthropic has no equivalent for.
Bedrock Invoke and Vertex reject the field upstream, so they keep rejecting it.

* fix(anthropic): keep the caller's service_tier value out of the drop warning log
This commit is contained in:
ryan-crabbe-berri 2026-10-14 10:34:00 -07:00 • committed by GitHub
parent 4f3cb48dc1
commit b5aa827067
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 0 deletions

View file

@ -294,6 +294,17 @@ DROP_UNSUPPORTED_SPEED_WARNING: Final = (
"Dropping unsupported `speed` for model=%s (drop_params=True). Fast mode is only supported on select Opus models."
)
SERVICE_TIER_PROVIDERS: Final = frozenset({"anthropic", "azure_ai"})
OPENAI_TO_ANTHROPIC_SERVICE_TIER: Final = MappingProxyType(
{"auto": "auto", "default": "standard_only", "standard_only": "standard_only"}
)
DROP_UNSUPPORTED_SERVICE_TIER_WARNING: Final = (
"Dropping unsupported `service_tier` for model=%s (drop_params=True). "
"Anthropic only accepts 'auto' and 'standard_only'."
)
class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"""
@ -500,6 +511,23 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
)
optional_params.pop("speed", None)
@staticmethod
def _map_service_tier(value: str, model: str, drop_params: bool) -> str | None:
anthropic_tier: Final = OPENAI_TO_ANTHROPIC_SERVICE_TIER.get(value)
if anthropic_tier is not None:
return anthropic_tier
if not (litellm.drop_params or drop_params):
raise litellm.utils.UnsupportedParamsError(
message=(
f"{model} does not support service_tier={value!r}. Anthropic accepts 'auto', "
"'standard_only', or OpenAI's 'default' (sent as 'standard_only'). "
"To drop unsupported params, set `litellm.drop_params = True`."
),
status_code=400,
)
litellm.verbose_logger.warning(DROP_UNSUPPORTED_SERVICE_TIER_WARNING, model)
return None
@classmethod
def maybe_drop_speed_param(
cls,
@ -573,6 +601,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
params.append("thinking")
params.append("reasoning_effort")
if self._resolved_provider in SERVICE_TIER_PROVIDERS:
params.append("service_tier")
return params
@staticmethod
@ -1720,6 +1751,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
elif param == "cache_control" and isinstance(value, dict):
# Pass through top-level cache_control for automatic prompt caching
optional_params["cache_control"] = value
elif (
param == "service_tier"
and isinstance(value, str)
and self._resolved_provider in SERVICE_TIER_PROVIDERS
and (anthropic_tier := AnthropicConfig._map_service_tier(value, model, drop_params)) is not None
):
optional_params["service_tier"] = anthropic_tier # rebind-ok: in-place fill is this method's contract
## handle thinking tokens
self.update_optional_params_with_thinking_tokens(

View file

@ -4317,6 +4317,71 @@ def test_calculate_usage_service_tier_defaults_to_none():
assert usage.service_tier is None
@pytest.mark.parametrize("provider", ["anthropic", "azure_ai"])
@pytest.mark.parametrize(
("openai_tier", "anthropic_tier"),
[("auto", "auto"), ("default", "standard_only"), ("standard_only", "standard_only")],
)
def test_service_tier_reaches_anthropic_request_body(provider, openai_tier, anthropic_tier):
optional_params: Final = litellm.utils.get_optional_params(
model="claude-sonnet-4-6", custom_llm_provider=provider, service_tier=openai_tier
)
body: Final = AnthropicConfig().transform_request(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello"}],
optional_params=optional_params,
litellm_params={},
headers={},
)
assert body["service_tier"] == anthropic_tier
@pytest.mark.parametrize("tier", ["flex", "priority", "fast", "scale"])
def test_service_tier_without_anthropic_equivalent_raises_with_drop_params_hint(monkeypatch, tier):
monkeypatch.setattr(litellm, "drop_params", False)
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params") as exc_info:
litellm.utils.get_optional_params(
model="claude-sonnet-4-6", custom_llm_provider="anthropic", service_tier=tier
)
assert exc_info.value.status_code == 400
def test_service_tier_without_anthropic_equivalent_dropped_with_drop_params(monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
optional_params: Final = litellm.utils.get_optional_params(
model="claude-sonnet-4-6", custom_llm_provider="anthropic", service_tier="flex", drop_params=True
)
assert "service_tier" not in optional_params
@pytest.mark.parametrize(
("provider", "model"),
[("bedrock", "invoke/us.anthropic.claude-sonnet-4-6"), ("vertex_ai", "claude-sonnet-4-6")],
)
def test_service_tier_still_rejected_for_claude_hosts_that_400_on_it(monkeypatch, provider, model):
monkeypatch.setattr(litellm, "drop_params", False)
with pytest.raises(litellm.utils.UnsupportedParamsError, match="service_tier"):
litellm.utils.get_optional_params(model=model, custom_llm_provider=provider, service_tier="auto")
def test_bedrock_invoke_service_tier_opt_in_is_sent_untranslated():
optional_params: Final = litellm.utils.get_optional_params(
model="invoke/us.anthropic.claude-sonnet-4-6",
custom_llm_provider="bedrock",
service_tier="default",
allowed_openai_params=["service_tier"],
)
assert optional_params["service_tier"] == "default"
def test_fast_mode_parameter_in_supported_params():
"""
Test that 'speed' is in the list of supported OpenAI params.