1313import copy
1414import json
1515import logging
16+ import time
1617import uuid
1718from collections .abc import AsyncIterator
19+ from email .utils import parsedate_to_datetime
1820from typing import Dict , List
1921
2022import httpx
@@ -51,6 +53,8 @@ def _openrouter_headers(url: str) -> dict[str, str]:
5153
5254
5355_STREAM_RETRY_ATTEMPTS = 3
56+ _STREAM_RETRY_MAX_DELAY_SECONDS = 30
57+ _STREAM_RETRY_STATUS_CODES = {408 , 409 , 429 , 500 , 502 , 503 , 504 }
5458_STREAM_TIMEOUT = httpx .Timeout (
5559 STREAM_CONNECT_TIMEOUT_SECONDS ,
5660 read = STREAM_READ_TIMEOUT_SECONDS ,
@@ -59,6 +63,7 @@ def _openrouter_headers(url: str) -> dict[str, str]:
5963_STREAM_RETRY_ERRORS = (
6064 httpx .ConnectError ,
6165 httpx .ConnectTimeout ,
66+ httpx .HTTPStatusError ,
6267 httpx .ReadError ,
6368 httpx .ReadTimeout ,
6469 httpx .RemoteProtocolError ,
@@ -75,6 +80,32 @@ class ChatCompletionForm(BaseModel):
7580 tools : List [Dict ] = []
7681
7782
83+ def _is_retryable_stream_error (exc : BaseException ) -> bool :
84+ if isinstance (exc , httpx .HTTPStatusError ):
85+ return exc .response .status_code in _STREAM_RETRY_STATUS_CODES
86+ return isinstance (exc , _STREAM_RETRY_ERRORS )
87+
88+
89+ def _stream_retry_delay (exc : BaseException , attempt : int ) -> float :
90+ delay = 0.5 * (attempt + 1 )
91+ if not isinstance (exc , httpx .HTTPStatusError ):
92+ return delay
93+
94+ retry_after = exc .response .headers .get ("retry-after" )
95+ if not retry_after :
96+ return delay
97+
98+ try :
99+ delay = float (retry_after )
100+ except ValueError :
101+ try :
102+ delay = parsedate_to_datetime (retry_after ).timestamp () - time .time ()
103+ except (TypeError , ValueError , OverflowError ):
104+ return delay
105+
106+ return min (max (delay , 0 ), _STREAM_RETRY_MAX_DELAY_SECONDS )
107+
108+
78109# ── Non-streaming completion ────────────────────────────────
79110
80111
@@ -377,16 +408,16 @@ async def stream_anthropic(
377408 emitted = True
378409 yield {"type" : "done" }
379410 return
380- except _STREAM_RETRY_ERRORS :
381- if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 :
411+ except _STREAM_RETRY_ERRORS as exc :
412+ if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 or not _is_retryable_stream_error ( exc ) :
382413 raise
383414 logger .warning (
384415 "[stream] anthropic transient stream failure before first event; retrying (%s/%s)" ,
385416 attempt + 1 ,
386417 _STREAM_RETRY_ATTEMPTS ,
387418 exc_info = True ,
388419 )
389- await asyncio .sleep (0.5 * ( attempt + 1 ))
420+ await asyncio .sleep (_stream_retry_delay ( exc , attempt ))
390421
391422
392423# ── OpenAI Chat Completions ──────────────────────────────────
@@ -657,16 +688,16 @@ def complete_reasoning_item() -> dict | None:
657688 emitted = True
658689 yield {"type" : "done" }
659690 return
660- except _STREAM_RETRY_ERRORS :
661- if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 :
691+ except _STREAM_RETRY_ERRORS as exc :
692+ if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 or not _is_retryable_stream_error ( exc ) :
662693 raise
663694 logger .warning (
664695 "[stream] openai completions transient stream failure before first event; retrying (%s/%s)" ,
665696 attempt + 1 ,
666697 _STREAM_RETRY_ATTEMPTS ,
667698 exc_info = True ,
668699 )
669- await asyncio .sleep (0.5 * ( attempt + 1 ))
700+ await asyncio .sleep (_stream_retry_delay ( exc , attempt ))
670701
671702
672703# ── OpenAI Responses API ─────────────────────────────────────
@@ -974,13 +1005,13 @@ def get_reasoning_item(event: dict) -> dict:
9741005 emitted = True
9751006 yield {"type" : "done" }
9761007 return
977- except _STREAM_RETRY_ERRORS :
978- if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 :
1008+ except _STREAM_RETRY_ERRORS as exc :
1009+ if emitted or attempt == _STREAM_RETRY_ATTEMPTS - 1 or not _is_retryable_stream_error ( exc ) :
9791010 raise
9801011 logger .warning (
9811012 "[stream] openai responses transient stream failure before first event; retrying (%s/%s)" ,
9821013 attempt + 1 ,
9831014 _STREAM_RETRY_ATTEMPTS ,
9841015 exc_info = True ,
9851016 )
986- await asyncio .sleep (0.5 * ( attempt + 1 ))
1017+ await asyncio .sleep (_stream_retry_delay ( exc , attempt ))
0 commit comments