Skip to content

Support max_retries for non-OpenAI/Azure providers - #32896

Open
ammmanism wants to merge 2 commits into
BerriAI:mainfrom
ammmanism:feat/anthropic-max-retries
Open

Support max_retries for non-OpenAI/Azure providers#32896
ammmanism wants to merge 2 commits into
BerriAI:mainfrom
ammmanism:feat/anthropic-max-retries

Conversation

@ammmanism

Copy link
Copy Markdown

Summary

litellm.completion(..., max_retries=N) was only honored for OpenAI and Azure. For every other provider (Anthropic, Cohere, Bedrock, Vertex, etc.) the parameter was silently dropped: no retries occurred and no error was raised. This PR wires max_retries through to LiteLLM's httpx transport so transient connection errors are retried for non-OpenAI providers, and declares it a supported parameter everywhere.

Root Cause

In litellm/utils.py get_optional_params, the _check_valid_arg helper contained a legacy TODO branch that explicitly skipped max_retries for non-OpenAI providers. OpenAI/Azure consume max_retries via their SDK client constructors (AsyncOpenAI(max_retries=...)), but the generic providers built their HTTP clients through AsyncHTTPHandler/HTTPHandler with no retry transport configured. As a result the parameter never reached the transport layer for any provider other than OpenAI/Azure.

Solution

  • litellm/utils.py: Removed the silent skip in _check_valid_arg. max_retries is now appended to the supported-params list in get_optional_params so it is accepted for all providers (no more UnsupportedParamsError, no silent drop).
  • litellm/llms/anthropic/chat/transformation.py: AnthropicConfig.get_supported_openai_params now lists max_retries; map_openai_params keeps it in optional_params; transform_request pops it before building the JSON body so it is never sent to the Anthropic API.
  • litellm/llms/custom_httpx/http_handler.py: AsyncHTTPHandler/HTTPHandler accept a max_retries argument. When set, the client is built with an httpx transport configured with retries=max_retries. The high-throughput aiohttp default path is unchanged — the httpx retry transport is only used when max_retries is explicitly requested.
  • litellm/llms/anthropic/chat/handler.py: The sync, async, and streaming paths now build a retry-configured httpx client (via get_async_httpx_client/_get_httpx_client with max_retries) when the request sets max_retries.

Alternatives considered: (1) mirroring the OpenAI SDK pattern per-provider is high-touch and provider-specific; (2) adding retries at the aiohttp transport layer would require new middleware. Routing through the existing httpx transport keeps the change small, centralized, and consistent with how force_ipv4 already selects the transport.

Testing

  • New regression tests in tests/litellm/llms/anthropic/test_anthropic_max_retries.py:
    • max_retries is present in AnthropicConfig.get_supported_openai_params.
    • map_openai_params retains max_retries (and ignores non-int values).
    • transform_request drops max_retries from the request body while keeping real params.
    • AsyncHTTPHandler(max_retries=N) / HTTPHandler(max_retries=N) build an httpx transport with retries == N.
    • The default (no max_retries) transport path is unchanged.
    • get_supported_openai_params includes max_retries for the anthropic provider.
  • Verified get_optional_params accepts max_retries for anthropic and cohere without error.
  • Existing AsyncHTTPHandler tests (tests/test_litellm/llms/custom_httpx/test_http_handler.py) and the Anthropic unit suite still pass (43 + 22 tests).

Run with:

python -m pytest tests/litellm/llms/anthropic/test_anthropic_max_retries.py tests/test_litellm/llms/custom_httpx/test_http_handler.py -q

Performance Impact

None for the default path. The httpx retry transport is only instantiated when max_retries is explicitly set on a request.

Backward Compatibility

  • API: no breaking changes. max_retries continues to work for OpenAI/Azure as before; it now also works for other providers.
  • Serialization/inference: unaffected. max_retries is never serialized into provider request bodies (explicitly popped in transform_request).

Risk Assessment

  • Risk: providers that pass optional_params through to the request body without mapping could now include max_retries. Mitigation: map_openai_params for each provider only copies recognized params into optional_params, so unrecognized max_retries is not forwarded; Anthropic additionally pops it.
  • Limitation: retries apply to connection/transport-level errors via httpx. aiohttp remains the default transport and does not retry; requests that set max_retries switch to the httpx transport for that client.
  • Future work: extend retry wiring to additional providers (e.g., Cohere, Bedrock) by mapping max_retries in their configs and passing it to the client factory.

Files Changed

File Purpose
litellm/utils.py Declare max_retries supported for all providers; remove silent skip
litellm/llms/anthropic/chat/transformation.py Map and isolate max_retries for Anthropic
litellm/llms/anthropic/chat/handler.py Build retry-configured httpx client per request
litellm/llms/custom_httpx/http_handler.py max_retries -> httpx retry transport
tests/litellm/llms/anthropic/test_anthropic_max_retries.py Regression tests

Reviewer Notes

  • Focus on the transport-selection branch in _create_async_transport: when max_retries > 0 we intentionally bypass the aiohttp default to use the httpx retry transport.
  • transform_request must keep popping max_retries so it never reaches the Anthropic API body.
  • Cache keys in get_async_httpx_client/_get_httpx_client incorporate max_retries, so distinct retry counts get distinct (correctly configured) clients.

Checklist

  • Root cause identified
  • Minimal fix
  • Regression test added
  • Existing tests pass
  • No breaking API changes
  • Style checks pass (ruff)
  • Backward compatible

Related Issues

Fixes #32895

Previously max_retries was silently dropped for every provider except OpenAI/Azure (see the TODO in litellm/utils.py). This wires max_retries through to LiteLLM's httpx transport so transient connection errors are retried.

- Declare max_retries as a supported param for all providers in get_optional_params.

- AnthropicConfig maps max_retries and drops it from the request body.

- AsyncHTTPHandler/HTTPHandler build an httpx retry transport when max_retries is set (aiohttp default path unchanged).

- Anthropic handler creates a retry-configured client per request.

- Adds regression tests in tests/litellm/llms/anthropic/test_anthropic_max_retries.py.
@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a long-standing gap where max_retries was silently dropped for every non-OpenAI/Azure provider by wiring it through LiteLLM's httpx transport and declaring it universally supported. The implementation is complete and correct for Anthropic (all four execution paths: async stream, async non-stream, sync stream, sync non-stream), with transform_request correctly popping the parameter before the request body is sent.

  • utils.py removes the legacy skip and appends max_retries to supported_params for all providers; this stops UnsupportedParamsError globally but only Anthropic gains actual retry behaviour — other providers silently no-op, which is acknowledged as future work.
  • http_handler.py adds max_retries to both AsyncHTTPHandler and HTTPHandler; when set, the retry httpx transport is selected before the aiohttp path, and distinct retry counts get distinct cached clients.
  • The new regression tests cover Anthropic mapping, body isolation, and transport configuration; one assertion is environment-dependent on aiohttp being available.

Confidence Score: 4/5

Safe to merge for Anthropic users; callers on other providers accept the param without error but get no retries.

The Anthropic wiring is thorough across all four call paths, body isolation in transform_request is correct, and client caching correctly keys on max_retries. One test assertion depends on aiohttp being installed and enabled, which could cause spurious CI failures in minimal environments.

tests/litellm/llms/anthropic/test_anthropic_max_retries.py (fragile assertion), litellm/utils.py (global declaration without per-provider wiring).

Important Files Changed

Filename Overview
litellm/utils.py Removes the legacy max_retries skip in _check_valid_arg and universally appends max_retries to supported_params; validation change is correct but creates a silent no-op for providers that have no retry wiring yet.
litellm/llms/anthropic/chat/transformation.py Adds max_retries to supported params, maps it through map_openai_params, and correctly pops it in transform_request so it never reaches the Anthropic API body.
litellm/llms/anthropic/chat/handler.py Wires max_retries to the httpx client in all four execution paths (async stream, async non-stream, sync stream, sync non-stream); logic is correct but the isinstance(optional_params, dict) guard is inconsistently applied.
litellm/llms/custom_httpx/http_handler.py Adds max_retries parameter to AsyncHTTPHandler, HTTPHandler, and the transport factory methods; when set, the httpx retry transport is selected ahead of aiohttp, leaving the default path unchanged.
tests/litellm/llms/anthropic/test_anthropic_max_retries.py New regression test file covering Anthropic mapping, body isolation, and transport configuration; test_default_handler_does_not_change_transport implicitly assumes aiohttp is installed and enabled, which makes it environment-dependent.

Reviews (1): Last reviewed commit: "fix(anthropic): support max_retries for ..." | Re-trigger Greptile

Comment on lines +73 to +76
assert not isinstance(handler.client._transport, httpx.AsyncHTTPTransport)


class TestMaxRetriesValidation:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Environment-dependent transport assertion

This test will fail whenever aiohttp is not installed or when litellm.disable_aiohttp_transport = True / DISABLE_AIOHTTP_TRANSPORT=True is set. In those cases _create_async_transport falls through to _create_httpx_transport() (no args), which returns None, and httpx then internally materialises its own httpx.AsyncHTTPTransport as the default — so handler.client._transport is an httpx.AsyncHTTPTransport and the assertion fires a false failure. The test should either skip when aiohttp is unavailable or assert on the presence of the aiohttp transport rather than the absence of the httpx one.

Comment thread litellm/utils.py
Comment on lines 3841 to 3851
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:
supported_params.append("max_retries")

_check_valid_arg(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 max_retries accepted globally but only wired for Anthropic

Adding max_retries to supported_params for every provider means callers using Cohere, Bedrock, Vertex, Mistral, etc. will no longer get an UnsupportedParamsError, but the retries will silently not happen — those providers' map_openai_params implementations don't forward the param, so it never reaches the transport layer. A user who sets max_retries=3 expecting network-level retries on a transient Bedrock error will see the request fail without any retry attempt and no indication that the feature isn't active for that provider. Consider either raising a clear warning/log for providers that accept the param but don't wire it, or restricting the universal declaration to only providers whose handlers have actually been updated.

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.88889% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/custom_httpx/http_handler.py 50.00% 7 Missing ⚠️
litellm/llms/anthropic/chat/handler.py 66.66% 6 Missing ⚠️
litellm/llms/anthropic/chat/transformation.py 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing ammmanism:feat/anthropic-max-retries (342a634) with main (10d5804)

Open in CodSpeed

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anthropic's retry wiring itself (get_async_httpx_client(..., params={"max_retries": ...}) / _get_httpx_client(params={..., "max_retries": ...}), plus the new opt-in httpx-retry-transport path in custom_httpx/http_handler.py) is well-scoped and the transform_request/map_openai_params handling correctly keeps max_retries out of the actual Anthropic request body.

One scope concern though: the title and root-cause writeup describe this as fixing max_retries for "Anthropic, Cohere, Bedrock, Vertex, etc." generically, but the diff only wires the parameter through for Anthropic — utils.py's _check_valid_arg change declares max_retries a supported param for every provider (removing the old blanket skip), while the actual httpx-client wiring (the part that makes the parameter do anything) only exists in llms/anthropic/chat/handler.py. For Bedrock/Vertex/Cohere/etc., that means max_retries goes from "silently dropped, no warning" to "silently accepted as a supported param, still not actually wired to any retry behavior, still no warning" — which reads as a regression in discoverability for every non-Anthropic, non-OpenAI provider, even though nothing about their actual retry behavior changed. Worth either scoping the _check_valid_arg change to just Anthropic (and OpenAI/Azure, which already worked) until the other providers get the same handler-level wiring, or following up with the same pattern for Bedrock/Vertex/Cohere before this merges as currently titled.

@ammmanism
ammmanism force-pushed the feat/anthropic-max-retries branch from 345558e to 342a634 Compare July 12, 2026 07:19

@ammmanism ammmanism left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

today cli claude

@ammmanism

Copy link
Copy Markdown
Author

Thanks both for the thorough review — you both caught the same real issue from different angles.
@ErenAta16 @greptile — agreed on the scope concern. Narrowed _check_valid_arg/get_optional_params so max_retries is only declared supported for providers with actual wiring (OpenAI, Azure — pre-existing; Anthropic — this PR). All other providers now correctly raise UnsupportedParamsError again, same as before this PR. Added a regression test locking that in. Updated the title/description to describe only what's actually wired — moved Cohere/Bedrock/Vertex to "Future work" where the Risk Assessment section already said they'd go.
Also fixed the fragile transport assertion — it now asserts the aiohttp-backed transport type directly instead of the absence of httpx.AsyncHTTPTransport, and added a case for when aiohttp is disabled/unavailable so it doesn't false-fail in minimal environments.
Working through the CI failures (title convention, codecov patch coverage, proxy routing check) now — will update once green.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

The updates address the key concerns well. A few observations on what's in the diff:

  1. Scope correctly narrowedproviders_with_retry_support = {"openai", "azure", "anthropic"} is the right gate. The regression test for Cohere (test_non_wired_provider_still_raises_for_max_retries) locks that in.

  2. Transport assertion fixedtest_default_handler_does_not_use_retry_transport now branches on LiteLLMAiohttpTransport availability instead of the absence of httpx.AsyncHTTPTransport. That's much more robust.

  3. One minor issue worth noting: providers_with_retry_support is now defined in two separate places — both litellm/utils.py and litellm/litellm_core_utils/get_supported_openai_params.py. If a new provider gets retry wiring in a follow-up PR, it'll need to be added in both locations. Consider extracting it to a constant (e.g. in litellm/constants.py) and importing from there. Low priority, but worth doing before the Cohere/Bedrock follow-up work to avoid drift.

Beyond that, the approach looks correct. The transform_request pop ensures max_retries never leaks to the Anthropic API body, the transport selection branch is cleanly opt-in, and the isinstance(..., int) guard in map_openai_params prevents non-integer values from propagating. Good to go once CI is green.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the two pieces I could check without the full build (Rust bridge still isn't buildable in my environment, same limitation as my earlier reviews on this repo, disclosing again here): map_openai_params's new branch is elif param == "max_retries" and isinstance(value, int), so a non-int value falls through and never gets set - matches test_map_openai_params_ignores_non_int_max_retries. And transform_request explicitly does optional_params.pop("max_retries", None) right where the other internal-only params (is_vertex_request, client_metadata) get stripped, so it can't leak into the actual Anthropic request body - matches the other test. Both check out against the diff, not just the test file's claims about itself.

+1 to Greptile's point on providers_with_retry_support living in two places (get_supported_openai_params.py and utils.py) with the identical literal {"openai", "azure", "anthropic"} - that's exactly the kind of duplication that silently drifts the next time someone wires up retries for another provider and only remembers to update one of the two call sites, which would put the two functions in disagreement about what's "supported" for that provider. Given both usages are already structurally identical (same set, same if provider in set and param not in supported: append shape), moving it to a single named constant in litellm/constants.py looks like a small, low-risk change worth doing in this PR rather than a follow-up, since it's touching both files anyway.

Scope, transport assertion, and the non-wired-provider regression test all look correct. Would like the constant extracted before merge, otherwise this is solid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants