Проблема: free-модели живут своей жизнью
Вы потратили выходные на интеграцию GPT-4o-mini через OpenRouter, а в понедельник модель выдает 503. Или Anthropic внезапно заблокировал регион. Или лимиты закончились на третьем запросе. Знакомо?
Free-модели — штука нестабильная. Провайдеры меняют эндпоинты, вводят капчу, рейт-лимиты, а то и просто выключают модель без предупреждения. Но для MVP или экономии бюджета они незаменимы. Выход — агрегатор с авто-выбором живой модели и fallback-цепочкой. Я покажу, как собрать такого зверя на Python, с учетом ошибок, которые я сам набивал.
Если вы уже сталкивались с ситуацией, когда RAG-система выдаёт чушь из-за сбойной модели, прочитайте этот разбор — там описана похожая проблема и её решение.
Решение: живой агрегатор с heartbeat и ротацией
Идея проста: перед каждым запросом (или по таймеру) мы проверяем, какие free-модели сейчас доступны, выбираем лучшую по скорингу, и при сбое переключаемся на следующую. Плюс храним состояние, чтобы соседние инстансы не дублировали проверки.
1 Сбор и обновление списка free-моделей
Вручную поддерживать список — путь в ад. Модели появляются и исчезают каждый месяц. Используйте агрегаторы вроде OpenRouter, которые отдают актуальный перечень. Но осторожно: у них тоже бывают сбои. Лучше иметь свой резервный список в JSON.
[
{
"id": "gpt-4o-mini",
"provider": "openai",
"base_url": "https://api.openai.com/v1",
"api_key_env": "OPENAI_API_KEY",
"free": true,
"health_endpoint": "/models"
},
{
"id": "claude-3-haiku",
"provider": "anthropic",
"base_url": "https://api.anthropic.com/v1",
"api_key_env": "ANTHROPIC_API_KEY",
"free": false // условно-бесплатно, если есть грант
}
]
Ошибка: положиться на один источник. Если OpenRouter лежит, вы теряете все модели. Держите backup-список из 3-5 проверенных провайдеров, например, вот этот гид поможет найти альтернативы.
2 Health check: пинг, а не просто статус
Просто проверить, что эндпоинт отвечает — мало. Модель может быть available, но деградировать (высокая latency, частые 429). Лучший способ — отправить реальный короткий запрос (например, "Hi") и замерить время ответа и успешность. Но не делайте это для каждой модели при каждом запросе — закэшируйте результат на 1-5 минут в Redis.
async def check_model_health(client, model_config):
"""Проверяет доступность модели через минимальный вызов."""
start = time.time()
try:
# Используем /v1/chat/completions для OpenAI-совместимых
resp = await client.post(
f"{model_config['base_url']}/chat/completions",
json={"model": model_config['id'], "messages": [{"role": "user", "content": "ping"}], "max_tokens": 5},
headers={"Authorization": f"Bearer {os.environ[model_config['api_key_env']]}"},
timeout=10
)
if resp.status_code == 200:
return {"ok": True, "latency": time.time() - start}
else:
return {"ok": False, "code": resp.status_code}
except Exception as e:
return {"ok": False, "error": str(e)}
Почему не просто HEAD request? Потому что провайдеры часто возвращают 200 на статику, а реальный инференс ломается. Единственный способ убедиться — выполнить инференс. Да, это тратит токены, но free-модели обычно имеют бесплатный tier. Если у вас модель без инференса (например, только эмбеддинги), пинг эндпоинта /models — ок.
3 Авто-выбор: скоринг с учётом uptime и задачи
Просто брать первую живую модель — наивно. Лучше сделать скоринг: модель с низкой latency и высоким аптаймом получает приоритет. А для разных задач (код, креатив, RAG) можно взвешивать по-разному. Например, для извлечения фактов лучше подходит Claude 3 Haiku, а для кода — GPT-4o-mini.
Вот примитивный скоринг без внешних зависимостей:
def score_model(health, model_config, task_type="general"):
if not health['ok']:
return 0
# штраф за latency > 5s
latency_penalty = max(0, (health['latency'] - 0.5) / 5)
base = 10 - latency_penalty
# повышаем для специализированных моделей
if task_type == "code" and "code" in model_config['id']:
base += 3
return base
А затем выбираем модель с максимальным score. Если несколько с одинаковым — берём первую случайную (чтобы не нагружать одну).
4 Fallback: не просто retry, а переключение провайдера
Самая частая ошибка — retry на той же модели, которая уже лежит. Fallback должен переключаться на другую модель (возможно, другого провайдера) и при этом не зацикливаться. Реализуем цепочку: при ошибке или таймауте увеличиваем счётчик попыток, после max_retry на одной модели переходим к следующей в порядке скоринга.
async def llm_with_fallback(request, models, task_type: str):
sorted_models = await get_sorted_models(models, task_type)
for model in sorted_models:
for attempt in range(3):
try:
return await call_model(model, request)
except (TimeoutError, RateLimitError) as e:
logger.warning(f"Model {model['id']} failed: {e}")
if attempt == 2:
# Помечаем модель как dead в кэше на 30 секунд
await mark_dead(model['id'], ttl=30)
break # переходим к следующей модели
await asyncio.sleep(2 ** attempt) # exponential backoff
raise NoModelAvailable("All models exhausted")
5 Хранение состояния в Redis для распределённой отказоустойчивости
Если у вас несколько реплик, каждая будет проверять health и обновлять dead-лист независимо. Это порождает дублирующие запросы и несогласованность. Решение — Redis. В нём держим счётчики ошибок, последнее время аптайма, live-список моделей. Каждая реплика пишет в Redis и читает оттуда скоринговую таблицу. TTL на health — 2 минуты. Если ключ протух — запускаем новую проверку.
async def get_leader_board(redis, models):
pipe = redis.pipeline()
for m in models:
pipe.get(f"model:{m['id']}:health")
results = await pipe.execute()
live = []
for i, data in enumerate(results):
if data:
live.append((json.loads(data), models[i]))
return sorted(live, key=lambda x: x[0]['score'], reverse=True)
Не забудьте про очистку старых ключей, иначе Redis разрастётся. Используйте EXPIRE при каждой записи.
Нюансы, которые превратят ваш агрегатор в свалку (и как этого избежать)
- Отсутствие rate limiter на стороне агрегатора. Вы будете долбить все модели одновременно health check'ами — провайдеры забанят IP. Ограничьте параллельные проверки до 2-3.
- Игнорирование различий в API. OpenAI и Anthropic имеют разную структуру запросов/ответов. Придётся писать адаптеры. Хорошая новость — многие free-модели поддерживают OpenAI-совместимый интерфейс (через OpenRouter или локально).
- Бесконечный рекурсивный fallback. Если все модели мертвы, ваш код может циклично перебирать один и тот же список. Введите глобальный лимит попыток и кидайте исключение пользователю, а не вешайте запрос навечно.
- Утечка чувствительных данных при fallback. Убедитесь, что при переключении на другую модель вы не передаете промпты с PII на сервера, которые не имеют соответствующих сертификатов. Об этом я писал в статье про OpenAI Fallback в LlamaIndex.
Когда агрегатор не нужен (спойлер: почти всегда нужен, но бывает исключение)
Если у вас production с жёсткими SLA и бюджетом — лучше купить платный доступ и не заморачиваться с free-моделями. Но для прототипов, pet-проектов и сценариев с нерегулярной нагрузкой агрегатор спасает бюджет. Кстати, для локальных моделей есть свои решения — llama-swap позволяет переключать модели на лету без перезапуска сервера, а LLMeQueue — выстроить очередь запросов.
Осторожно: fallback на локальную модель может быть медленным, если у вас GPU не тянет. Лучше сочетать free API с локальной моделью как последней инстанцией. Пример такого гибрида — мультимодальный краулер на локальных LLM.
Как НЕ надо писать fallback: мой личный антипример
Раньше я делал так: при ошибке просто перебирал модели по порядку, без кэширования health. В результате каждая реплика била по всем моделям, провайдеры блокировали нас за DDoS. А когда одна модель возвращала ошибку, мы пытались её же через секунду — и получали ту же ошибку. Потом добавил Redis, счётчики и TTL — стало терпимо. Главный урок: централизованное состояние спасает.
Прогноз: через год free-модели не исчезнут, но агрегаторы станут умнее
Уже сейчас провайдеры вводят динамические free-квоты, привязанные к поведению пользователя. Агрегаторы будущего будут не просто проверять жива ли модель, а предсказывать её доступность на основе исторических паттернов (ML-based health check). Мы сделали первый шаг — научились не падать при сбое. Следующий шаг — оптимизация стоимости через роутинг (как LLMRouter, который режет расходы на 30-50%).
Совет: не пытайтесь объять необъятное. Начните с 3-4 моделей, отладьте fallback, а потом добавляйте новые. И никогда не забывайте про мониторинг — без него агрегатор превращается в чёрный ящик.