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.):
- The SDK retries the same HTTP call up to 2 times (with its own exponential backoff).
- 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).
- 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
- No stacked retries: nanobot should be the sole retry controller. SDK-level retries should be disabled (
max_retries=0).
- Reasonable timeouts: Read timeout should be bounded (e.g., 180s configurable) instead of the SDK's 600s default.
- 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
- Set
max_retries=0 on AsyncOpenAI and AsyncAnthropic client construction.
- Set explicit
timeout (configurable, default ~180s read + 10s connect) on both clients.
- 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
- Configure nanobot with an OpenAI-compatible provider pointing at a backend that returns 504.
- Send a message — observe the agent hangs silently for several minutes.
- Check logs: SDK-level retry lines (
_base_client.py Retrying request) interleave with nanobot-level retry warnings.
Summary
OpenAI Python SDK and Anthropic Python SDK both have built-in retry logic (
max_retries=2by default). nanobot'sLLMProvider.chat_with_retry/chat_stream_with_retryadds 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
LLMProvider(base.py),OpenAICompatProvider,AnthropicProvider,AgentLoopmax_retries=2,timeout=Timeout(connect=5s, read=600s)max_retries=2,timeout=Timeout(connect=5s, read=600s)Problem
When an LLM backend returns a transient error (504, 503, timeout, etc.):
chat_with_retrysees the error content, matches it against_TRANSIENT_ERROR_MARKERS, and retries at the application level (1s, 2s, 4s delays).Worst-case scenario (504 with ~60s upstream timeout):
Additionally, the SDK's default
read=600stimeout means even a single stuck request blocks for up to 10 minutes before the SDK gives up.Expected Behavior
max_retries=0).Affected Providers
max_retriesset?timeout?OpenAICompatProviderAsyncOpenAI(...)AnthropicProviderAsyncAnthropic(...)AzureOpenAIProviderhttpx.AsyncClient(timeout=60)directlySuggested Fix
max_retries=0onAsyncOpenAIandAsyncAnthropicclient construction.timeout(configurable, default ~180s read + 10s connect) on both clients.on_retrycallback parameter tochat_with_retry/chat_stream_with_retryso callers (e.g.,AgentLoop) can surface retry progress to users.Reproduction
_base_client.py Retrying request) interleave with nanobot-level retry warnings.