Skip to content

Commit 31e1fba

Browse files
wassnameclaudypoo
andcommitted
Retry 408/429 via x-speakeasy-retries overlay instead of editing generated code
Reverts the hand-edits to the generated utils/retries.py, its .genignore entry, and the test. Adds an overlay so 408/429 join 5XX in the retry config, regenerating cleanly per CONTRIBUTING (generated code is not edited directly). Verified with `speakeasy overlay apply`: statusCodes -> [5XX, 408, 429]. The 400-with-transient-inner-error.code case is not portable to the SDK (status-only retry config, hooks run outside the retry loop) and is handled client-side instead. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
1 parent d159a17 commit 31e1fba

3 files changed

Lines changed: 27 additions & 158 deletions

File tree

.genignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
pylintrc
22
docs/docs.json
33
docs/overview.mdx
4-
# Owns retry response classification (408/429/5XX + transient inner error.code on 400s).
5-
# Speakeasy has no overlay/hook surface for the 400-inner-code case, so this file is
6-
# hand-maintained; genignore stops `speakeasy generate` from overwriting it. -- Claude
7-
src/openrouter/utils/retries.py

src/openrouter/utils/retries.py

Lines changed: 27 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
22

33
import asyncio
4-
import json
54
import random
65
import time
76
from datetime import datetime
87
from email.utils import parsedate_to_datetime
9-
from typing import Any, List, Optional
8+
from typing import List, Optional
109

1110
import httpx
1211

@@ -136,41 +135,6 @@ def _parse_retry_after_header(response: httpx.Response) -> Optional[int]:
136135
return None
137136

138137

139-
TRANSIENT_STATUS_CODES = {408, 429}
140-
TRANSIENT_ERROR_CODES = {408, 429, 500, 502, 503, 504, 524, 529}
141-
142-
143-
def _matches_retry_status_code(response: httpx.Response, status_code: str) -> bool:
144-
if "X" in status_code.upper():
145-
code_range = int(status_code[0])
146-
status_major = response.status_code // 100
147-
return code_range == status_major
148-
149-
return response.status_code == int(status_code)
150-
151-
152-
def _error_code_from_json_body(response: httpx.Response) -> Optional[int]:
153-
try:
154-
body: Any = response.json()
155-
except json.JSONDecodeError:
156-
return None
157-
158-
if not isinstance(body, dict):
159-
return None
160-
161-
error = body.get("error")
162-
if not isinstance(error, dict):
163-
return None
164-
165-
code = error.get("code")
166-
if isinstance(code, int):
167-
return code
168-
if isinstance(code, str) and code.isdigit():
169-
return int(code)
170-
171-
return None
172-
173-
174138
def _parse_retry_after_ms_header(response: httpx.Response) -> Optional[int]:
175139
retry_after_ms_header = response.headers.get("retry-after-ms")
176140
if not retry_after_ms_header:
@@ -186,44 +150,6 @@ def _parse_retry_after_ms_header(response: httpx.Response) -> Optional[int]:
186150
return None
187151

188152

189-
def _is_retryable_response(response: httpx.Response, status_codes: List[str]) -> bool:
190-
if response.status_code in TRANSIENT_STATUS_CODES:
191-
return True
192-
193-
for status_code in status_codes:
194-
if _matches_retry_status_code(response, status_code):
195-
return True
196-
197-
if response.status_code != 400:
198-
return False
199-
200-
if not response.is_closed:
201-
response.read()
202-
203-
inner_code = _error_code_from_json_body(response)
204-
return inner_code in TRANSIENT_ERROR_CODES
205-
206-
207-
async def _is_retryable_response_async(
208-
response: httpx.Response, status_codes: List[str]
209-
) -> bool:
210-
if response.status_code in TRANSIENT_STATUS_CODES:
211-
return True
212-
213-
for status_code in status_codes:
214-
if _matches_retry_status_code(response, status_code):
215-
return True
216-
217-
if response.status_code != 400:
218-
return False
219-
220-
if not response.is_closed:
221-
await response.aread()
222-
223-
inner_code = _error_code_from_json_body(response)
224-
return inner_code in TRANSIENT_ERROR_CODES
225-
226-
227153
def _get_sleep_interval(
228154
exception: Exception,
229155
initial_interval: int,
@@ -268,8 +194,19 @@ def do_request() -> httpx.Response:
268194
try:
269195
res = func()
270196

271-
if _is_retryable_response(res, retries.status_codes):
272-
raise TemporaryError(res)
197+
for code in retries.status_codes:
198+
if "X" in code.upper():
199+
code_range = int(code[0])
200+
201+
status_major = res.status_code / 100
202+
203+
if code_range <= status_major < code_range + 1:
204+
raise TemporaryError(res)
205+
else:
206+
parsed_code = int(code)
207+
208+
if res.status_code == parsed_code:
209+
raise TemporaryError(res)
273210
except (httpx.NetworkError, httpx.TimeoutException) as exception:
274211
if retries.config.retry_connection_errors:
275212
raise
@@ -302,8 +239,19 @@ async def do_request() -> httpx.Response:
302239
try:
303240
res = await func()
304241

305-
if await _is_retryable_response_async(res, retries.status_codes):
306-
raise TemporaryError(res)
242+
for code in retries.status_codes:
243+
if "X" in code.upper():
244+
code_range = int(code[0])
245+
246+
status_major = res.status_code / 100
247+
248+
if code_range <= status_major < code_range + 1:
249+
raise TemporaryError(res)
250+
else:
251+
parsed_code = int(code)
252+
253+
if res.status_code == parsed_code:
254+
raise TemporaryError(res)
307255
except (httpx.NetworkError, httpx.TimeoutException) as exception:
308256
if retries.config.retry_connection_errors:
309257
raise

tests/test_retries.py

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)