fix: ignore non-finite Retry-After hints

A gateway that answers 429 with Retry-After: inf (or 1e999, which float()
happily rounds to inf) used to reach backoff_delay as retry_after_s=inf.
max(delay, retry_after) then picked it, and since the library deliberately
does not clamp the hint with backoff_max_s, the attempt slept forever.

- _parse_retry_after now rejects non-finite values via math.isinf and
  returns None, so the call falls back to plain exponential backoff
- it emits exactly one warning carrying the source name and the verdict
  word retry_after_not_finite, never the raw header: under a 429 storm an
  echoed header drowns the real signal and does not help localisation
- source_name becomes a required keyword-only argument; the function is
  private, so no default is given and a missed call site fails loudly
  instead of silently dropping the source identity from the warning
- nan keeps flowing through the existing seconds > 0 semantics; no new
  branch, no reordering of the parse
This commit is contained in:
2026-09-10 04:20:56 -04:00
parent 9474c76ab0
commit da77b123ec
2 changed files with 95 additions and 4 deletions
+20 -3
View File
@@ -9,6 +9,7 @@ SSE 纯函数移植 VT `adapters/llm.py:51-124`;错误翻译移植 CHS
from __future__ import annotations
import json
import math
import re
import time
from dataclasses import replace
@@ -108,14 +109,30 @@ async def _iter_sse_deltas(
# —— 错误翻译(CHS invokers.py 同款)——
def _parse_retry_after(raw: str | None) -> float | None:
"""解析 Retry-After 头;仅支持秒数形态,HTTP-date 返回 None(CHS 同款)。"""
def _parse_retry_after(raw: str | None, *, source_name: str) -> float | None:
"""解析 Retry-After 头;仅支持秒数形态,HTTP-date 返回 None(CHS 同款)。
**非有限值必须当作"无提示"**(issue F1): `"inf"` / `"1e999"` 能被 `float()`
成功解析,又能通过 `seconds > 0`,于是一路变成 `retry_after_s=inf`——而
`backoff_delay` 的 `max(delay, retry_after)` 取大之后就是一次**永不醒来**的
退避 sleep(库刻意不拿 `backoff_max_s` 去夹它,见设计 §6.2)。
`source_name` 是**必填** keyword-only 参数: 本函数是私有的,不给默认值,
漏传即 `TypeError`,免得将来新增调用点静默丢掉源标识(告警定位不到是哪个源)。
`nan` 不新增分支,沿用既有 `seconds > 0` 恒假的值语义。
"""
if raw is None:
return None
try:
seconds = float(raw.strip())
except ValueError:
return None
if math.isinf(seconds):
# 只写源名与判据词: 429 风暴下回显原始头会把日志淹掉,也无助于定位
logger.warning(
"{} 的 Retry-After 非有限值,按无提示处理(retry_after_not_finite)", source_name
)
return None
return seconds if seconds > 0 else None
@@ -137,7 +154,7 @@ def _translate_429(
)
return TransientError(
compose_message(f"{source.name} 限速: 429", summary),
retry_after_s=_parse_retry_after(headers.get("retry-after")),
retry_after_s=_parse_retry_after(headers.get("retry-after"), source_name=source.name),
**ctx,
)