Skip to content

Commit 866e9e6

Browse files
committed
refac
1 parent cf6929b commit 866e9e6

1 file changed

Lines changed: 40 additions & 9 deletions

File tree

cptr/utils/ai.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import copy
1414
import json
1515
import logging
16+
import time
1617
import uuid
1718
from collections.abc import AsyncIterator
19+
from email.utils import parsedate_to_datetime
1820
from typing import Dict, List
1921

2022
import 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

Comments
 (0)