Перейти к содержимому

Сработал fallback на резервную модель

Хорошая система с fallback не падает — она молча переключается на запасную модель или провайдера и продолжает отвечать. Это и есть проблема: «молча». Вы узнаёте о том, что основной провайдер лёг, только когда придёт счёт от дорогого резерва или когда качество ответов просядет. Хочется знать о переключении сразу — но без паники, ведь пользователям сейчас ничего не угрожает.

Идеальный кейс для тихого low-priority push: «работаем в деградированном режиме, причина такая-то».

Оборачиваем цепочку «основной → резерв»: если сработал fallback — считаем переключения в окне и шлём тихий алёрт с причиной.

import os, time, collections, requests
import openai, anthropic
NOTIFLY_URL = os.environ["NOTIFLY_URL"]
NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]
WINDOW_SEC = 300 # окно, в котором копим счётчик переключений
COOLDOWN = 300 # не чаще одного алёрта в 5 минут
_falls = collections.deque() # timestamps срабатываний fallback
_last_alert = 0.0
primary = openai.OpenAI()
backup = anthropic.Anthropic()
def notify(title, msg, prio):
requests.post(f"{NOTIFLY_URL}/message",
params={"token": NOTIFLY_TOKEN},
json={"title": title, "message": msg, "priority": prio},
timeout=5)
def _on_fallback(reason):
global _last_alert
now = time.time()
_falls.append(now)
while _falls and _falls[0] < now - WINDOW_SEC:
_falls.popleft()
if now - _last_alert > COOLDOWN:
_last_alert = now
notify(
"🟠 Работаем на резервной модели",
f"Основной провайдер отвалился ({reason}). "
f"Переключений за {WINDOW_SEC // 60} мин: {len(_falls)}.",
priority=3, # тихо: пользователям сейчас ок
)
def ask(prompt):
"""Пробуем основную модель; при сбое — резерв, с тихим уведомлением."""
try:
r = primary.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}])
return r.choices[0].message.content
except Exception as e:
_on_fallback(f"{type(e).__name__}: {e}")
r = backup.messages.create(
model="claude-haiku-4-5", max_tokens=1024,
messages=[{"role": "user", "content": prompt}])
return r.content[0].text
print(ask("Одно предложение про надёжность."))

priority=3 — намеренно тихий уровень (силент-диапазон 0–3): телефон не звонит, но событие зафиксировано. Счётчик за окно превращает «одно случайное переключение» (обычно шум) в понятный сигнал «основной реально лёг, уже N раз за 5 минут».

Одно переключение — норма. А вот если fallback держится минутами, это уже инцидент: резерв обычно дороже и/или слабее. Добавьте условие «доля fallback-запросов в окне > 50%» и поднимите его до priority=8 — уже громкий алёрт «пора чинить основного, а не жить на запасном».

В reason кладите разобранную причину, а не просто Exception: 529 overloaded (переждать), insufficient_quota (см. кончился free-tier), timeout (см. латентность) — это три разных инцидента, и лечатся они по-разному.

  • с какой модели на какую переключились;
  • разобранную причину сбоя основной;
  • число переключений (или долю fallback-запросов) за окно;
  • насколько резерв дороже — чтобы оценить срочность возврата.