claude_usage: backoff après échec de refresh OAuth (anti-429)
Token expiré -> refresh tenté à chaque rendu, échec non mémorisé -> on martelait platform.claude.com et le 429 s'entretenait. On impose désormais un backoff de 5 min après un refresh échoué (et sur la voie de refresh forcé 401), pendant lequel on sert la dernière valeur connue au lieu de re-tenter.
This commit is contained in:
@@ -36,6 +36,16 @@ EXPIRY_BUFFER_MS = 120_000 # rafraîchit 2 min avant expiration
|
|||||||
|
|
||||||
_refresh_lock = asyncio.Lock()
|
_refresh_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
# Backoff après échec du refresh OAuth (typiquement 429) : sans ça, le token expiré déclenche un
|
||||||
|
# refresh À CHAQUE rendu, l'échec n'étant pas mémorisé -> on martèle platform.claude.com et on
|
||||||
|
# entretient le rate-limit. Pendant la fenêtre on ne retente pas (on sert la dernière valeur connue).
|
||||||
|
REFRESH_BACKOFF_SECONDS = 300
|
||||||
|
_refresh_state: dict[str, float] = {"backoff_until": 0.0}
|
||||||
|
|
||||||
|
|
||||||
|
class _RefreshThrottled(Exception):
|
||||||
|
"""Refresh en backoff (anti-429), pas une vraie erreur réseau/auth."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Window:
|
class Window:
|
||||||
@@ -157,18 +167,29 @@ async def _refresh(data: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def _valid_token() -> str:
|
async def _valid_token() -> str:
|
||||||
"""Renvoie un access token valide, en rafraîchissant si nécessaire (sérialisé)."""
|
"""Renvoie un access token valide, en rafraîchissant si nécessaire (sérialisé).
|
||||||
|
|
||||||
|
Après un échec de refresh (ex. 429), on impose un backoff et on lève _RefreshThrottled
|
||||||
|
pendant la pause : l'appelant sert alors la dernière valeur connue au lieu de re-marteler."""
|
||||||
data = _load_creds()
|
data = _load_creds()
|
||||||
o = data["claudeAiOauth"]
|
o = data["claudeAiOauth"]
|
||||||
if int(o.get("expiresAt", 0)) - int(time.time() * 1000) > EXPIRY_BUFFER_MS:
|
if int(o.get("expiresAt", 0)) - int(time.time() * 1000) > EXPIRY_BUFFER_MS:
|
||||||
return o["accessToken"]
|
return o["accessToken"]
|
||||||
|
if time.time() < _refresh_state["backoff_until"]:
|
||||||
|
raise _RefreshThrottled()
|
||||||
async with _refresh_lock:
|
async with _refresh_lock:
|
||||||
# Re-lecture après acquisition du lock : un autre coroutine a peut-être déjà rafraîchi.
|
# Re-lecture après acquisition du lock : un autre coroutine a peut-être déjà rafraîchi.
|
||||||
data = _load_creds()
|
data = _load_creds()
|
||||||
o = data["claudeAiOauth"]
|
o = data["claudeAiOauth"]
|
||||||
if int(o.get("expiresAt", 0)) - int(time.time() * 1000) > EXPIRY_BUFFER_MS:
|
if int(o.get("expiresAt", 0)) - int(time.time() * 1000) > EXPIRY_BUFFER_MS:
|
||||||
return o["accessToken"]
|
return o["accessToken"]
|
||||||
return await _refresh(data)
|
if time.time() < _refresh_state["backoff_until"]:
|
||||||
|
raise _RefreshThrottled()
|
||||||
|
try:
|
||||||
|
return await _refresh(data)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
_refresh_state["backoff_until"] = time.time() + REFRESH_BACKOFF_SECONDS
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def _get_usage(token: str) -> httpx.Response:
|
async def _get_usage(token: str) -> httpx.Response:
|
||||||
@@ -232,6 +253,8 @@ async def _fetch_usage() -> ClaudeUsage:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
token = await _valid_token()
|
token = await _valid_token()
|
||||||
|
except _RefreshThrottled:
|
||||||
|
return ClaudeUsage(ok=False, error="refresh en pause (anti-429)")
|
||||||
except (OSError, KeyError, json.JSONDecodeError) as exc:
|
except (OSError, KeyError, json.JSONDecodeError) as exc:
|
||||||
return ClaudeUsage(ok=False, error=f"credentials illisibles: {exc}")
|
return ClaudeUsage(ok=False, error=f"credentials illisibles: {exc}")
|
||||||
except httpx.HTTPError as exc:
|
except httpx.HTTPError as exc:
|
||||||
@@ -239,11 +262,15 @@ async def _fetch_usage() -> ClaudeUsage:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await _get_usage(token)
|
resp = await _get_usage(token)
|
||||||
# Token rejeté malgré le buffer -> une tentative de refresh forcé.
|
# Token rejeté malgré le buffer -> une tentative de refresh forcé (soumise au backoff).
|
||||||
if resp.status_code == 401:
|
if resp.status_code == 401:
|
||||||
data = _load_creds()
|
try:
|
||||||
async with _refresh_lock:
|
data = _load_creds()
|
||||||
token = await _refresh(data)
|
async with _refresh_lock:
|
||||||
|
token = await _refresh(data)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
_refresh_state["backoff_until"] = time.time() + REFRESH_BACKOFF_SECONDS
|
||||||
|
raise
|
||||||
resp = await _get_usage(token)
|
resp = await _get_usage(token)
|
||||||
except httpx.HTTPError as exc:
|
except httpx.HTTPError as exc:
|
||||||
return ClaudeUsage(ok=False, error=f"réseau: {exc}")
|
return ClaudeUsage(ok=False, error=f"réseau: {exc}")
|
||||||
|
|||||||
Reference in New Issue
Block a user