Lumen / lumen_model_speed.py
SilverElixir
Run routes as consecutive provider blocks and keep Gemma out of the Gemini daily limit
5fcacff
Raw History Blame Contribute Delete
4.24 kB
"""
lumen_model_speed.py — самокалибрующаяся оценка задержек моделей.
Прод 17.09.2026: голова цепочки тормозила 96с, а роутер ставил её первой. Поэтому замер по факту + EMA, новичок с нейтрального приора. EMA только в памяти.
"""
from __future__ import annotations
# Ключ provider:model_id — из lumen_typing_pace, чтобы не плодить два форматтера.
from lumen_typing_pace import speed_key
# Тот же компромисс стабильности/подстройки, что в typing_pace.
_EMA_ALPHA = 0.3
# Приор нейтрален: новичок без замеров не взлетает в голову и не падает в хвост.
_DEFAULT_TTF_SEC = 10.0
_DEFAULT_TOTAL_SEC = 25.0
# Границы-зажимы ДО усреднения (тот же приём, что clamp в record_observed_speed):
# один выброс не должен утащить EMA в небеса или в пол.
_MIN_TTF_SEC = 1.0
_MAX_TTF_SEC = 90.0
_MIN_TOTAL_SEC = 3.0
_MAX_TOTAL_SEC = 120.0
# Запас на разовое зависание первого куска, а не вечное ожидание.
_TTF_ABANDON_MULT = 2.5
_latency_ema: dict[str, tuple[float, float]] = {}
def expected_ttf_sec(key: str) -> float:
"""Ожидаемое время до первого куска — EMA или приор для новой модели."""
ttf = _latency_ema.get(key, (_DEFAULT_TTF_SEC, _DEFAULT_TOTAL_SEC))[0]
return max(_MIN_TTF_SEC, min(_MAX_TTF_SEC, ttf))
def expected_total_sec(key: str) -> float:
"""Ожидаемое полное время ответа — EMA или приор для новой модели."""
total = _latency_ema.get(key, (_DEFAULT_TTF_SEC, _DEFAULT_TOTAL_SEC))[1]
return max(_MIN_TOTAL_SEC, min(_MAX_TOTAL_SEC, total))
def record_response(key: str, *, total_sec: float, ttf_sec: float | None = None) -> None:
"""Учесть один УСПЕШНЫЙ ответ. Без ttf_sec (нестриминговый ответ первого куска
не видел) ttf-EMA не трогаем: подстановка total завышала first_chunk_limit,
и зависшая голова держалась дольше. Неположительные замеры игнорируются."""
if total_sec <= 0:
return
prev = _latency_ema.get(key)
if ttf_sec is None or ttf_sec <= 0:
obs_ttf = prev[0] if prev is not None else _DEFAULT_TTF_SEC
else:
obs_ttf = max(_MIN_TTF_SEC, min(_MAX_TTF_SEC, ttf_sec))
obs_total = max(_MIN_TOTAL_SEC, min(_MAX_TOTAL_SEC, total_sec))
if prev is None:
_latency_ema[key] = (obs_ttf, obs_total)
else:
_latency_ema[key] = (
_EMA_ALPHA * obs_ttf + (1 - _EMA_ALPHA) * prev[0],
_EMA_ALPHA * obs_total + (1 - _EMA_ALPHA) * prev[1],
)
def first_chunk_limit_sec(key: str, floor_sec: float) -> float:
"""min — floor от холодного старта, max — EMA_ttf × множитель от разового зависания."""
return max(floor_sec, expected_ttf_sec(key) * _TTF_ABANDON_MULT)
def reorder_route(route: list[tuple[str, str]]) -> list[tuple[str, str]]:
"""Переставить модели по задержкам внутри подряд идущего блока одного провайдера.
Склейка блоков по провайдеру уводила резервную Gemini-цепочку вперёд Groq
(аудит 27.10.2026); порядок блоков — часть защиты квот, поэтому не трогаем его.
"""
ordered: list[tuple[str, str]] = []
run: list[tuple[str, str]] = []
for item in route:
if run and item[0] != run[-1][0]:
ordered.extend(sorted(run, key=lambda i: expected_total_sec(speed_key(i[0], i[1]))))
run = []
run.append(item)
if run:
ordered.extend(sorted(run, key=lambda i: expected_total_sec(speed_key(i[0], i[1]))))
return ordered