diff --git a/src/stirrup/clients/chat_completions_client.py b/src/stirrup/clients/chat_completions_client.py index 3cd2229..4ae0cef 100644 --- a/src/stirrup/clients/chat_completions_client.py +++ b/src/stirrup/clients/chat_completions_client.py @@ -12,14 +12,7 @@ from time import perf_counter from typing import Any -from openai import ( - APIConnectionError, - APITimeoutError, - AsyncOpenAI, - InternalServerError, - RateLimitError, -) -from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential +from openai import AsyncOpenAI from stirrup.clients.utils import to_openai_messages, to_openai_tools from stirrup.core.exceptions import ContextOverflowError @@ -47,7 +40,7 @@ class ChatCompletionsClient(LLMClient): Supports custom base_url for OpenAI-compatible providers (vLLM, Ollama, Azure OpenAI, local models, etc.). - Includes automatic retries for transient failures and token usage tracking. + Delegates retries for transient failures to the OpenAI SDK and tracks token usage. Example: >>> # Standard OpenAI usage @@ -114,18 +107,6 @@ def model_slug(self) -> str: """Model identifier.""" return self._model - @retry( - retry=retry_if_exception_type( - ( - APIConnectionError, - APITimeoutError, - RateLimitError, - InternalServerError, - ) - ), - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=1, max=10), - ) async def generate( self, messages: list[ChatMessage], @@ -133,9 +114,6 @@ async def generate( ) -> AssistantMessage: """Generate assistant response with optional tool calls. - Retries up to 3 times on transient errors (connection, timeout, rate limit, - internal server errors) with exponential backoff. - Args: messages: List of conversation messages. tools: Dictionary mapping tool names to Tool objects. diff --git a/src/stirrup/clients/open_responses_client.py b/src/stirrup/clients/open_responses_client.py index d3b5f75..e438eab 100644 --- a/src/stirrup/clients/open_responses_client.py +++ b/src/stirrup/clients/open_responses_client.py @@ -10,14 +10,7 @@ from time import perf_counter from typing import Any -from openai import ( - APIConnectionError, - APITimeoutError, - AsyncOpenAI, - InternalServerError, - RateLimitError, -) -from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential +from openai import AsyncOpenAI from stirrup.core.exceptions import ContextOverflowError from stirrup.core.models import ( @@ -263,7 +256,7 @@ class OpenResponsesClient(LLMClient): Supports custom base_url for OpenAI-compatible providers that implement the Responses API. - Includes automatic retries for transient failures and token usage tracking. + Delegates retries for transient failures to the OpenAI SDK and tracks token usage. Example: >>> # Standard OpenAI usage @@ -303,6 +296,7 @@ def __init__( (e.g., 'low', 'medium', 'high'). Only used with o1/o3 style models. timeout: Request timeout in seconds. If None, uses OpenAI SDK default. max_retries: Number of retries for transient errors. Defaults to 2. + The OpenAI SDK handles retries internally. instructions: Default system-level instructions. Can be overridden by SystemMessage in the messages list. kwargs: Additional arguments passed to responses.create(). @@ -338,18 +332,6 @@ def model_slug(self) -> str: """Model identifier.""" return self._model - @retry( - retry=retry_if_exception_type( - ( - APIConnectionError, - APITimeoutError, - RateLimitError, - InternalServerError, - ) - ), - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=1, max=10), - ) async def generate( self, messages: list[ChatMessage], @@ -357,9 +339,6 @@ async def generate( ) -> AssistantMessage: """Generate assistant response with optional tool calls using Responses API. - Retries up to 3 times on transient errors (connection, timeout, rate limit, - internal server errors) with exponential backoff. - Args: messages: List of conversation messages. tools: Dictionary mapping tool names to Tool objects. diff --git a/tests/test_openai_sdk_retries.py b/tests/test_openai_sdk_retries.py new file mode 100644 index 0000000..7aeb19a --- /dev/null +++ b/tests/test_openai_sdk_retries.py @@ -0,0 +1,47 @@ +"""Behavioral tests for OpenAI SDK retry configuration.""" + +import httpx +import pytest +from openai import InternalServerError + +from stirrup.clients.chat_completions_client import ChatCompletionsClient +from stirrup.clients.open_responses_client import OpenResponsesClient +from stirrup.core.models import UserMessage + + +@pytest.mark.parametrize( + "client_class", + [ + pytest.param(ChatCompletionsClient, id="chat_completions"), + pytest.param(OpenResponsesClient, id="open_responses"), + ], +) +async def test_max_retries_controls_sdk_request_count( + client_class: type[ChatCompletionsClient] | type[OpenResponsesClient], +) -> None: + request_count = 0 + + def fail(request: httpx.Request) -> httpx.Response: + nonlocal request_count + request_count += 1 + return httpx.Response( + 500, + headers={"retry-after-ms": "1"}, + json={"error": {"message": "retry", "type": "server_error"}}, + request=request, + ) + + client = client_class(model="gpt-4o", api_key="test-key", max_retries=1) + original_sdk_client = client._client # noqa: SLF001 + client._client = original_sdk_client.with_options( # noqa: SLF001 + http_client=httpx.AsyncClient(transport=httpx.MockTransport(fail)) + ) + await original_sdk_client.close() + + try: + with pytest.raises(InternalServerError): + await client.generate([UserMessage(content="hello")], {}) + finally: + await client._client.close() # noqa: SLF001 + + assert request_count == 2