fix(anthropic): scope max_retries to providers with retry wiring

This commit is contained in:
ammmanism 2026-07-12 12:46:51 +05:30
parent 56f84c8856
commit 342a634666
3 changed files with 156 additions and 12 deletions

View file

@ -53,6 +53,16 @@ def get_supported_openai_params(
if provider_config and request_type == "chat_completion":
supported_params = provider_config.get_supported_openai_params(model=model)
allowed_openai_params = getattr(provider_config, "allowed_openai_params", None)
supported_params = supported_params or []
allowed_openai_params = allowed_openai_params or []
supported_params.extend(allowed_openai_params)
# ``max_retries`` is supported for providers with actual retry wiring
# (OpenAI/Azure retry at the SDK layer; anthropic retries at LiteLLM's
# httpx transport layer when ``max_retries`` is set).
providers_with_retry_support = {"openai", "azure", "anthropic"}
if custom_llm_provider in providers_with_retry_support and "max_retries" not in supported_params:
supported_params.append("max_retries")
if base_model and base_model != model:
base_model_params = provider_config.get_supported_openai_params(model=base_model)
supported_params = list(dict.fromkeys([*supported_params, *base_model_params]))

View file

@ -3841,11 +3841,8 @@ def get_optional_params(
supported_params = supported_params or []
allowed_openai_params = allowed_openai_params or []
supported_params.extend(allowed_openai_params)
# ``max_retries`` is supported across all providers (OpenAI/Azure retry at
# the SDK layer; non-OpenAI providers retry at LiteLLM's httpx transport
# layer when ``max_retries`` is set). Declare it supported everywhere so it
# is no longer silently dropped for non-OpenAI providers.
if "max_retries" not in supported_params:
providers_with_retry_support = {"openai", "azure", "anthropic"}
if custom_llm_provider in providers_with_retry_support and "max_retries" not in supported_params:
supported_params.append("max_retries")
_check_valid_arg(

View file

@ -2,10 +2,9 @@
Regression tests for `max_retries` support on non-OpenAI providers.
`max_retries` was previously silently dropped for every provider except
OpenAI/Azure (see the TODO in `litellm/utils.py`). These tests lock in the
fix that:
OpenAI/Azure. These tests lock in the fix that:
1. Declares `max_retries` as a supported param for all providers.
1. Declares `max_retries` as a supported param for providers with retry wiring (openai, azure, anthropic).
2. Maps it through `AnthropicConfig` without leaking it into the request body.
3. Wires it to LiteLLM's httpx transport so transient connection errors are
retried.
@ -15,7 +14,12 @@ import httpx
import pytest
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
HTTPHandler,
get_async_httpx_client,
_get_httpx_client,
)
class TestAnthropicMaxRetriesSupported:
@ -67,10 +71,92 @@ class TestMaxRetriesHttpTransport:
assert isinstance(transport, httpx.HTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 2
def test_default_handler_does_not_change_transport(self):
# Without max_retries the default (aiohttp) transport path is untouched.
def test_async_handler_retry_transport_with_force_ipv4(self):
import litellm
original_force_ipv4 = litellm.force_ipv4
litellm.force_ipv4 = True
litellm.disable_aiohttp_transport = True
try:
transport = AsyncHTTPHandler._create_async_transport(max_retries=3)
assert isinstance(transport, httpx.AsyncHTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 3
assert transport._pool._local_address == "0.0.0.0"
finally:
litellm.force_ipv4 = original_force_ipv4
litellm.disable_aiohttp_transport = False
def test_sync_handler_retry_transport_with_force_ipv4(self):
import litellm
original_force_ipv4 = litellm.force_ipv4
litellm.force_ipv4 = True
litellm.disable_aiohttp_transport = True
try:
handler = HTTPHandler(max_retries=3, timeout=5)
transport = handler.client._transport
assert isinstance(transport, httpx.HTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 3
assert transport._pool._local_address == "0.0.0.0"
finally:
litellm.force_ipv4 = original_force_ipv4
litellm.disable_aiohttp_transport = False
handler.close()
def test_async_handler_force_ipv4_without_max_retries(self):
import litellm
original_force_ipv4 = litellm.force_ipv4
litellm.force_ipv4 = True
litellm.disable_aiohttp_transport = True
try:
transport = AsyncHTTPHandler._create_async_transport()
assert isinstance(transport, httpx.AsyncHTTPTransport)
assert transport._pool._local_address == "0.0.0.0"
finally:
litellm.force_ipv4 = original_force_ipv4
litellm.disable_aiohttp_transport = False
def test_default_handler_does_not_use_retry_transport(self):
# Without max_retries we do not use the retry transport.
handler = AsyncHTTPHandler()
assert not isinstance(handler.client._transport, httpx.AsyncHTTPTransport)
transport = handler.client._transport
# Check if aiohttp transport is available and enabled
aiohttp_enabled = AsyncHTTPHandler._should_use_aiohttp_transport()
# Import the aiohttp transport type for checking
try:
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
HAS_AIOHTTP_TRANSPORT = True
except ImportError:
HAS_AIOHTTP_TRANSPORT = False
LiteLLMAiohttpTransport = None
if aiohttp_enabled and HAS_AIOHTTP_TRANSPORT:
# When aiohttp is available and enabled, we should get the aiohttp transport
# which has no built-in retry mechanism in LiteLLM
assert isinstance(transport, LiteLLMAiohttpTransport)
else:
# When aiohttp is not available/disabled, we should get an httpx transport
# with explicit retries=0 (no retry)
assert isinstance(transport, httpx.AsyncHTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 0
def test_async_handler_transport_none_when_aiohttp_disabled_and_ipv4_off(self):
import litellm
original_force_ipv4 = litellm.force_ipv4
original_disable = litellm.disable_aiohttp_transport
litellm.force_ipv4 = False
litellm.disable_aiohttp_transport = True
try:
transport = AsyncHTTPHandler._create_async_transport()
assert transport is None
finally:
litellm.force_ipv4 = original_force_ipv4
litellm.disable_aiohttp_transport = original_disable
class TestMaxRetriesValidation:
@ -85,3 +171,54 @@ class TestMaxRetriesValidation:
supported = get_supported_openai_params(model=model, custom_llm_provider=provider)
assert "max_retries" in supported
def test_non_wired_provider_still_raises_for_max_retries(self):
"""
Regression test: providers without retry wiring should still raise
UnsupportedParamsError when max_retries is passed via completion().
"""
from litellm.utils import get_supported_openai_params
# Cohere doesn't have actual retry wiring yet
# get_supported_openai_params should NOT include max_retries for cohere
supported_params = get_supported_openai_params(model="command", custom_llm_provider="cohere")
assert "max_retries" not in supported_params, "max_retries should not be in supported params for cohere"
class TestMaxRetriesHandlerPaths:
def test_get_async_httpx_client_with_max_retries(self):
"""Test that get_async_httpx_client creates client with max_retries when provided."""
from litellm.types.utils import LlmProviders
import asyncio
client = get_async_httpx_client(llm_provider=LlmProviders.ANTHROPIC, params={"max_retries": 5})
try:
transport = client.client._transport
assert isinstance(transport, httpx.AsyncHTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 5
finally:
asyncio.run(client.close())
def test_get_httpx_client_with_max_retries(self):
"""Test that _get_httpx_client creates client with max_retries when provided."""
handler = _get_httpx_client(params={"max_retries": 4, "timeout": 30})
try:
transport = handler.client._transport
assert isinstance(transport, httpx.HTTPTransport)
assert getattr(transport._pool, "_retries", 0) == 4
finally:
handler.close()
def test_get_async_httpx_client_without_max_retries(self):
"""Test that get_async_httpx_client creates client without max_retries when not provided."""
from litellm.types.utils import LlmProviders
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
import asyncio
client = get_async_httpx_client(llm_provider=LlmProviders.ANTHROPIC, params={})
try:
transport = client.client._transport
# Should be LiteLLMAiohttpTransport when available (default)
assert isinstance(transport, LiteLLMAiohttpTransport)
finally:
asyncio.run(client.close())