Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 2 additions & 24 deletions src/stirrup/clients/chat_completions_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -114,28 +107,13 @@ 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],
tools: dict[str, Tool],
) -> 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.
Expand Down
27 changes: 3 additions & 24 deletions src/stirrup/clients/open_responses_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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().
Expand Down Expand Up @@ -338,28 +332,13 @@ 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],
tools: dict[str, Tool],
) -> 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.
Expand Down
47 changes: 47 additions & 0 deletions tests/test_openai_sdk_retries.py
Original file line number Diff line number Diff line change
@@ -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
Loading