From 500e267b3f7d8fc5d0444ca5e9288d0fb6779bc8 Mon Sep 17 00:00:00 2001 From: Ramzi Alashmali <186906047+RAMZI0TO99@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:13:48 +0300 Subject: [PATCH 1/2] fix(openai): honor zero retries for embeddings --- litellm/llms/openai/openai.py | 8 ++-- tests/unit/llms/openai/test_openai.py | 59 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 2461fb4e1af..c9faff270e0 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -1332,8 +1332,8 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): super().embedding() try: data: Final = {"model": model, "input": input, **optional_params} - max_retries = max_retries or litellm.DEFAULT_MAX_RETRIES - if not isinstance(max_retries, int): + effective_max_retries: Final = litellm.DEFAULT_MAX_RETRIES if max_retries is None else max_retries + if not isinstance(effective_max_retries, int): raise OpenAIError(status_code=422, message="max retries must be an int") ## LOGGING logging_obj.pre_call( @@ -1352,7 +1352,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): api_key=api_key, timeout=timeout, client=client, - max_retries=max_retries, + max_retries=effective_max_retries, shared_session=shared_session, litellm_params=litellm_params, ) @@ -1362,7 +1362,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): api_key=api_key, api_base=api_base, timeout=timeout, - max_retries=max_retries, + max_retries=effective_max_retries, client=client, litellm_params=litellm_params, ) diff --git a/tests/unit/llms/openai/test_openai.py b/tests/unit/llms/openai/test_openai.py index 2be691fa65b..b4badf6e5bc 100644 --- a/tests/unit/llms/openai/test_openai.py +++ b/tests/unit/llms/openai/test_openai.py @@ -350,3 +350,62 @@ async def test_async_audio_speech_records_provider_response_headers(): ) _assert_provider_headers_recorded(response) + + +def _embedding_transport() -> httpx.MockTransport: + return httpx.MockTransport( + lambda request: httpx.Response( + 200, + json={ + "object": "list", + "model": "embedding-test", + "data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}], + "usage": {"prompt_tokens": 1, "total_tokens": 1}, + }, + ) + ) + + +@pytest.mark.parametrize("max_retries,expected_retries", [(0, 0), (3, 3), (None, litellm.DEFAULT_MAX_RETRIES)]) +def test_embedding_honors_explicit_retry_limit(max_retries: int | None, expected_retries: int) -> None: + with OpenAI( + api_key="transport-only", + base_url="https://embedding.example/v1", + max_retries=7, + http_client=httpx.Client(transport=_embedding_transport()), + ) as client: + response: Final = litellm.embedding( + model="openai/embedding-test", + input=["retry control"], + encoding_format="float", + client=client, + max_retries=max_retries, + api_key="transport-only", + api_base=str(client.base_url), + ) + + assert client.max_retries == expected_retries + assert response.data == [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("max_retries,expected_retries", [(0, 0), (3, 3), (None, litellm.DEFAULT_MAX_RETRIES)]) +async def test_aembedding_honors_explicit_retry_limit(max_retries: int | None, expected_retries: int) -> None: + async with AsyncOpenAI( + api_key="transport-only", + base_url="https://embedding.example/v1", + max_retries=7, + http_client=httpx.AsyncClient(transport=_embedding_transport()), + ) as client: + response: Final = await litellm.aembedding( + model="openai/embedding-test", + input=["retry control"], + encoding_format="float", + client=client, + max_retries=max_retries, + api_key="transport-only", + api_base=str(client.base_url), + ) + + assert client.max_retries == expected_retries + assert response.data == [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}] From cc24c55ea4bbb0f95b355fae3d8e5303076b9ecc Mon Sep 17 00:00:00 2001 From: Ramzi Alashmali <186906047+RAMZI0TO99@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:36:39 +0300 Subject: [PATCH 2/2] test(openai): count embedding retry attempts --- tests/unit/llms/openai/test_openai.py | 61 ++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/unit/llms/openai/test_openai.py b/tests/unit/llms/openai/test_openai.py index b4badf6e5bc..895c8b71fb7 100644 --- a/tests/unit/llms/openai/test_openai.py +++ b/tests/unit/llms/openai/test_openai.py @@ -1,7 +1,7 @@ import asyncio import json from typing import Final -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock, patch import httpx import pytest @@ -409,3 +409,62 @@ async def test_aembedding_honors_explicit_retry_limit(max_retries: int | None, e assert client.max_retries == expected_retries assert response.data == [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}] + + +def _unavailable_embedding_response(request: httpx.Request) -> httpx.Response: + return httpx.Response(503, json={"error": {"message": "temporarily unavailable"}}, request=request) + + +@pytest.mark.parametrize("max_retries,expected_retries", [(0, 0), (3, 3), (None, litellm.DEFAULT_MAX_RETRIES)]) +def test_embedding_limits_requests_after_retryable_failure(max_retries: int | None, expected_retries: int) -> None: + requests: Final = Mock(side_effect=_unavailable_embedding_response) + with ( + patch("openai._base_client.time.sleep"), + OpenAI( + api_key="transport-only", + base_url="https://embedding.example/v1", + max_retries=7, + http_client=httpx.Client(transport=httpx.MockTransport(requests)), + ) as client, + pytest.raises(litellm.ServiceUnavailableError) as error, + ): + litellm.embedding( + model="openai/embedding-test", + input=["retry control"], + client=client, + max_retries=max_retries, + num_retries=0, + api_key="transport-only", + api_base=str(client.base_url), + ) + + assert error.value.status_code == 503 + assert requests.call_count == expected_retries + 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("max_retries,expected_retries", [(0, 0), (3, 3), (None, litellm.DEFAULT_MAX_RETRIES)]) +async def test_aembedding_limits_requests_after_retryable_failure( + max_retries: int | None, expected_retries: int +) -> None: + requests: Final = Mock(side_effect=_unavailable_embedding_response) + with patch("openai._base_client.anyio.sleep", new_callable=AsyncMock): + async with AsyncOpenAI( + api_key="transport-only", + base_url="https://embedding.example/v1", + max_retries=7, + http_client=httpx.AsyncClient(transport=httpx.MockTransport(requests)), + ) as client: + with pytest.raises(litellm.ServiceUnavailableError) as error: + await litellm.aembedding( + model="openai/embedding-test", + input=["retry control"], + client=client, + max_retries=max_retries, + num_retries=0, + api_key="transport-only", + api_base=str(client.base_url), + ) + + assert error.value.status_code == 503 + assert requests.call_count == expected_retries + 1