Skip to content

fix: SDK built-in retries stack with chat_with_retry causing silent 10+ min hangs #2511

Description

@xzq-xu

Summary

OpenAI Python SDK and Anthropic Python SDK both have built-in retry logic (max_retries=2 by default). nanobot's LLMProvider.chat_with_retry / chat_stream_with_retry adds another retry layer on top (_CHAT_RETRY_DELAYS = (1, 2, 4), up to 3 retries + 1 final attempt). These two layers stack independently, resulting in up to (2+1) × (3+1) = 12 requests for a single transient failure — and zero user feedback during the entire wait.

Environment

  • Components: LLMProvider (base.py), OpenAICompatProvider, AnthropicProvider, AgentLoop
  • SDK defaults:
    • OpenAI: max_retries=2, timeout=Timeout(connect=5s, read=600s)
    • Anthropic: max_retries=2, timeout=Timeout(connect=5s, read=600s)

Problem

When an LLM backend returns a transient error (504, 503, timeout, etc.):

  1. The SDK retries the same HTTP call up to 2 times (with its own exponential backoff).
  2. After the SDK exhausts its retries and returns an error response, nanobot's chat_with_retry sees the error content, matches it against _TRANSIENT_ERROR_MARKERS, and retries at the application level (1s, 2s, 4s delays).
  3. Each application-level retry triggers a fresh SDK call — which itself may retry up to 2 more times internally.

Worst-case scenario (504 with ~60s upstream timeout):

  • 12 total HTTP requests × ~60s each = ~12 minutes of silent hanging
  • No progress indication to the user
  • Steering/interruption cannot break in (stuck in synchronous retry loops)

Additionally, the SDK's default read=600s timeout means even a single stuck request blocks for up to 10 minutes before the SDK gives up.

Expected Behavior

  1. No stacked retries: nanobot should be the sole retry controller. SDK-level retries should be disabled (max_retries=0).
  2. Reasonable timeouts: Read timeout should be bounded (e.g., 180s configurable) instead of the SDK's 600s default.
  3. User feedback on retry: When retrying, the user should see a progress message (e.g., "AI service temporarily unavailable, retrying (1/3)…").

Affected Providers

Provider Client max_retries set? Custom timeout?
OpenAICompatProvider AsyncOpenAI(...) No (SDK default 2) No (SDK default 600s read)
AnthropicProvider AsyncAnthropic(...) No (SDK default 2) No (SDK default 600s read)
AzureOpenAIProvider Uses httpx.AsyncClient(timeout=60) directly N/A (no SDK retry) Yes (60s)

Suggested Fix

  1. Set max_retries=0 on AsyncOpenAI and AsyncAnthropic client construction.
  2. Set explicit timeout (configurable, default ~180s read + 10s connect) on both clients.
  3. Add an on_retry callback parameter to chat_with_retry / chat_stream_with_retry so callers (e.g., AgentLoop) can surface retry progress to users.

Reproduction

  1. Configure nanobot with an OpenAI-compatible provider pointing at a backend that returns 504.
  2. Send a message — observe the agent hangs silently for several minutes.
  3. Check logs: SDK-level retry lines (_base_client.py Retrying request) interleave with nanobot-level retry warnings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions