Mistral header fix

This commit is contained in:
DazedAnon 2026-06-22 05:33:02 -05:00
parent 3dea0b8972
commit a1b8c69190

View file

@ -1085,22 +1085,10 @@ def runTranslationBatches(poll=60):
# ===== Mistral (la Plateforme) adaptive rate limiting ===== # ===== Mistral (la Plateforme) adaptive rate limiting =====
# Mistral enforces independent request and token limits, both PER-MODEL. # Mistral enforces per-minute request and token limits that vary by tier/model.
# Verified live against api.mistral.ai (the response headers are authoritative): # The limiter starts from a conservative seed, then pins itself to the real
# * x-ratelimit-limit-req-minute — requests/minute. PER-MODEL and varies a # limits read from the live x-ratelimit-* response headers. One limiter is
# lot: mistral-medium=25, mistral-small=50, codestral=125, ministral-3b=750. # shared across file threads. Override seeds via
# Mistral's dashboard shows this /60 as "RPS" (25/min = 0.42 "RPS"), so the
# effective rate spans well below AND above 1 req/sec. There is NO
# per-second header; we divide req-minute by 60 to get the pacing interval.
# * x-ratelimit-limit-tokens-minute — input+output throughput, a true minute
# window, also per-model (e.g. 50k1.3M).
# * Tokens per month — overall cap (not paced here; surfaces as a 429).
# The limiter paces requests >= 1/RPS apart (so a burst of file threads can't
# overrun the per-minute request budget) AND charges them against a
# minute-windowed token budget. It starts from a conservative seed, then the
# live headers on the first response pin req_per_sec / tok_per_min to the exact
# per-model values. One limiter is shared across all file threads (this tool
# runs one model per session). Override the seeds with
# mistralReqPerSec / mistralTokPerMin / mistralTokenHeadroom. # mistralReqPerSec / mistralTokPerMin / mistralTokenHeadroom.
_mistral_limiter = None _mistral_limiter = None
_mistral_limiter_lock = threading.Lock() _mistral_limiter_lock = threading.Lock()
@ -1141,10 +1129,10 @@ def _header_float(headers, *names):
class AdaptiveLimiter: class AdaptiveLimiter:
"""Paces requests off live ratelimit headers. """Paces requests off live ratelimit headers.
Two dimensions, enforced together by acquire(): acquire() enforces two gates together:
* requests: spaced >= min_interval (1/RPS) apart Mistral's request cap * requests: spaced >= min_interval (1/RPS) apart, so bursts can't overrun
is per-second, so this prevents bursts from tripping it. the per-minute request cap.
* tokens: a minute-windowed budget (input+output), with a headroom margin. * tokens: a minute-windowed input+output budget with a headroom margin.
""" """
def __init__(self, req_per_sec, tok_per_min, headroom): def __init__(self, req_per_sec, tok_per_min, headroom):
@ -1179,42 +1167,37 @@ class AdaptiveLimiter:
time.sleep(min(sleep_for, 5)) time.sleep(min(sleep_for, 5))
def update(self, headers): def update(self, headers):
"""Sync budgets from the live ratelimit headers (authoritative). """Sync budgets from the live ratelimit headers."""
Accepts both the per-second request headers and (for forward/back compat)
the older per-minute request header names; the token headers are minute."""
if not headers: if not headers:
return return
with self.lock: with self.lock:
# The authoritative request limit is x-ratelimit-limit-req-MINUTE # Mistral's documented request cap is x-ratelimit-limit-requests
# (verified live: mistral-medium=25, ministral-3b=750, codestral=125; # (RPM); accept alternate spellings too and /60 to a pacing interval.
# Mistral's dashboard "RPS" is just this number / 60). There is no # A true per-second header, if present, is used undivided.
# per-second header. Convert to RPS for the pacing interval. A
# per-second header is still accepted first in case Mistral adds one.
limit_rps = _header_float(headers, "x-ratelimit-limit-req-second", limit_rps = _header_float(headers, "x-ratelimit-limit-req-second",
"x-ratelimit-limit-requests-second") "x-ratelimit-limit-requests-second")
if limit_rps is None: if limit_rps is None:
limit_rpm = _header_float(headers, "x-ratelimit-limit-req-minute", limit_rpm = _header_float(headers, "x-ratelimit-limit-requests",
"x-ratelimit-limit-req-minute",
"x-ratelimit-limit-requests-minute") "x-ratelimit-limit-requests-minute")
if limit_rpm is not None: if limit_rpm is not None:
limit_rps = limit_rpm / 60.0 limit_rps = limit_rpm / 60.0
if limit_rps and limit_rps > 0: if limit_rps and limit_rps > 0:
self.req_per_sec = limit_rps self.req_per_sec = limit_rps
self.min_interval = 1.0 / self.req_per_sec self.min_interval = 1.0 / self.req_per_sec
limit_tok = _header_int(headers, "x-ratelimit-limit-tokens-minute", limit_tok = _header_int(headers, "x-ratelimit-limit-tokens",
"x-ratelimit-limit-tokens") "x-ratelimit-limit-tokens-minute")
if limit_tok and limit_tok > 0: if limit_tok and limit_tok > 0:
self.tok_per_min = limit_tok self.tok_per_min = limit_tok
rem_tok = _header_int(headers, "x-ratelimit-remaining-tokens-minute", rem_tok = _header_int(headers, "x-ratelimit-remaining-tokens",
"x-ratelimit-remaining-tokens") "x-ratelimit-remaining-tokens-minute")
if rem_tok is not None: if rem_tok is not None:
self.tokens_remaining = rem_tok self.tokens_remaining = rem_tok
# Remaining requests this minute -> also cap the pace so a fresh # If the minute's request budget is already spent, hold off ~a minute.
# limiter that joins mid-window doesn't burst the leftover budget. rem_req = _header_int(headers, "x-ratelimit-remaining-requests",
rem_req = _header_int(headers, "x-ratelimit-remaining-req-minute", "x-ratelimit-remaining-req-minute",
"x-ratelimit-remaining-requests-minute") "x-ratelimit-remaining-requests-minute")
if rem_req is not None and rem_req <= 0: if rem_req is not None and rem_req <= 0:
# budget already spent this minute — hold off ~ a minute
self.next_request_at = max(self.next_request_at, time.monotonic() + 60.0) self.next_request_at = max(self.next_request_at, time.monotonic() + 60.0)
@ -1222,19 +1205,14 @@ def _get_mistral_limiter():
global _mistral_limiter global _mistral_limiter
with _mistral_limiter_lock: with _mistral_limiter_lock:
if _mistral_limiter is None: if _mistral_limiter is None:
# mistralReqPerMin kept as a deprecated alias: a per-minute number is # mistralReqPerMin is a deprecated per-minute alias, converted to RPS.
# converted to RPS so old .env files keep pacing sanely.
rps_env = os.getenv("mistralReqPerSec") rps_env = os.getenv("mistralReqPerSec")
if rps_env: if rps_env:
req_per_sec = float(rps_env) req_per_sec = float(rps_env)
elif os.getenv("mistralReqPerMin"): elif os.getenv("mistralReqPerMin"):
req_per_sec = max(0.05, float(os.getenv("mistralReqPerMin")) / 60.0) req_per_sec = max(0.05, float(os.getenv("mistralReqPerMin")) / 60.0)
else: else:
# Per-model RPS varies and is often well under 1 (free-tier # Conservative seed; the first response's headers correct it.
# mistral-medium ~0.83, magistral ~0.08). Start conservative so
# the FIRST request doesn't over-burst; the live header from that
# first response then corrects req_per_sec to the model's exact
# value (which is why few large batches > many tiny ones).
req_per_sec = 0.5 req_per_sec = 0.5
_mistral_limiter = AdaptiveLimiter( _mistral_limiter = AdaptiveLimiter(
req_per_sec, req_per_sec,