Политики резервирования бюджета
Как Meridian резервирует расходы до отправки запроса в провайдера — flat, worst-case и отключённое резервирование, когда какой режим выбирать и как настроить.
Обзор
При проверке бюджета Meridian резервирует часть лимита до того, как запрос ушёл к LLM-провайдеру, и списывает реальную стоимость после ответа. Это двухфазный протокол:
RESERVE — резервирует maxCost (верхняя оценка) на бюджете
↓
Запрос к LLM-провайдеру
↓
FINALIZE — списывает фактическую стоимость, освобождает резервСколько именно резервируется на шаге RESERVE, определяется политикой резервирования. От её выбора напрямую зависит:
- насколько строго блокируются превышения бюджета в момент пика нагрузки;
- какая часть лимита временно недоступна под параллельные запросы;
- как часто отказы происходят на этапе
RESERVEпротив отказов после расчёта реальной стоимости.
Политика задаётся глобально для шлюза в секции governance.reservation файла config.json и применяется ко всем бюджетам — клиентов, команд, виртуальных ключей и per-provider-config — единообразно.
Жизненный цикл резервирования
У записи о резервировании ровно два состояния: активна (создаётся RESERVE в начале запроса) и удалена (после FINALIZE — списание состоялось). Списание происходит ровно один раз, независимо от того, чем закончился запрос.
Все три перехода применяют одно и то же действие — FINALIZE. Различается только то, какая сумма списывается и кто инициирует:
| Что произошло с запросом | Кто завершает резерв | Что списано | Когда |
|---|---|---|---|
| Получен ответ от провайдера | PostLLMHook | actualCost (из ответа) | сразу после получения ответа |
| HTTP-timeout провайдера (нет тела ответа) | PostLLMHook | MaxCost (зарезервированная сумма) | сразу после срабатывания таймаута |
Падение ноды до FINALIZE | ReservationTracker | MaxCost | через reservation_ttl (по умолчанию 1 час) |
Запрос реально идёт дольше reservation_ttl | ReservationTracker | MaxCost | через reservation_ttl |
Принцип прост: каждое резервирование завершается списанием. Если фактическая стоимость известна — списывается она; если нет — MaxCost как консервативный заменитель. Потерь учёта быть не может.
ReservationTracker — фоновый процесс на лидере каждого шарда. Сканирует резервирования каждые 5 секунд; работает только с теми, у которых истёк ExpiresAt. На штатных запросах не вмешивается — PostLLMHook всегда успевает первым.
Рекомендуемые настройки
reservation_ttl определяет, как долго резервирование считается «живым», прежде чем его подберёт ReservationTracker. Значение по умолчанию — 1 час — рассчитано на любую реальную длительность одиночного запроса.
Связанная настройка — таймаут HTTP-запросов провайдера (network_config.default_request_timeout в конфигурации провайдера). Он задаёт, сколько Meridian держит TCP-соединение с upstream'ом, ожидая ответа.
| Параметр | Значение по умолчанию | Рекомендуемая граница |
|---|---|---|
cluster_config.raft.reservation_ttl | 1h | оставить по умолчанию |
providers[*].network_config.default_request_timeout | задаётся явно | не более 30 минут |
Не настраивайте таймаут HTTP-запросов провайдера больше 30 минут — это практический верхний предел. Даже самые тяжёлые сценарии (Claude Opus с контекстом 200K, генерация длинных потоков) укладываются в 10–15 минут. Более высокие таймауты не дают пользы, удерживают TCP-соединения в висящем состоянии, маскируют реальные деградации провайдера и удлиняют окно недоступности при сбоях. Если провайдер не ответил за 30 минут — почти наверняка не ответит и за час.
Почему это важно
Политика резервирования напрямую влияет на финансовую предсказуемость и пропускную способность при высоких нагрузках. Поведение, которое выглядит идентично на малом RPS, расходится тем сильнее, чем выше параллелизм и чем дороже отдельный запрос.
LLM-автоматизации (агентные пайплайны, batch-обработка)
Агенты и пайплайны часто выпускают серии запросов в коротком окне — десятки последовательных шагов или пачки в десятки тысяч запросов. Каждый отдельный запрос может стоить от долей цента до нескольких долларов в зависимости от модели и контекста. При параллельной отправке пачки система должна решать на этапе RESERVE, хватит ли бюджета — до того, как известны фактические стоимости.
Слишком пессимистичный резерв (worst_case) гарантирует, что бюджет физически невозможно превысить, но при дорогих моделях резервирует так много на каждый запрос, что параллельные запросы конкурируют за один и тот же «слот» резерва и часть из них отклоняется. Оптимистичный резерв (flat $0.05 или меньше) пропускает пачку целиком, но фактический расход после FINALIZE может на короткое время превысить лимит.
AI SaaS-сервисы с тарифными планами клиентов
В SaaS-моделях клиент покупает план с лимитом расходов на период (например, $50/мес). Каждый его запрос проверяется по бюджету клиента/команды/VK. Здесь критично:
- Жёсткий контроль перерасхода — пользователь, заплативший за $50, не должен «потратить» $54 из-за гонки RESERVE/FINALIZE;
- Предсказуемое поведение для оператора — отказы должны быть объяснимы (превышен бюджет), а не плавать в зависимости от размера модели;
- Возможность отдельно настроить дорогие модели — Claude Opus и GPT
xhighклассов требуют worst-case резерва, а быстрые/дешёвые — нет.
Подходящий выбор политики — flat-резерв на дешёвые модели + worst-case на премиум-модели через model_overrides. См. Когда какой режим выбирать.
Крупные потребители LLM/AI с высоким объёмом и низкой средней стоимостью
Внутренние сервисы и крупные продукты часто работают с бюджетами на порядки больше стоимости отдельного запроса ($100K/мес, бессрочные лимиты на отделы). В этом режиме резервирование per-request почти не несёт защитной функции — даже worst-case резерв в $4 на каждый запрос значительно меньше доступного лимита. Зато strict RESERVE искусственно ограничивает пропускную способность под пиковой нагрузкой.
Здесь часто выгодно отключить резервирование совсем — фактический расход всё равно списывается через FINALIZE и отказы будут происходить только после реального превышения лимита, без блокировок на этапе RESERVE.
Три политики
Meridian поддерживает три режима резервирования. Все они применяются единообразно ко всей иерархии governance — customers, teams, virtual keys и provider configs.
| Политика | Сумма резерва на RESERVE | Защита от перерасхода | Параллелизм при малых бюджетах |
|---|---|---|---|
| Flat $0.05 (по умолчанию) | Фиксированно $0.05 на запрос | Средняя: фактическая стоимость может немного превысить лимит до FINALIZE | Высокий — на бюджет $20 проходят ~400 параллельных запросов |
| Worst-case | Catalog-driven верхняя оценка (от $0.01 до нескольких долларов) | Строгая: RESERVE физически не пропустит превышение | Ограничен текущим доступным бюджетом и размером резерва |
| Disabled (sentinel) | $1e-9 (символический) | Без RESERVE-защиты, только пост-фактум через FINALIZE | Максимальный — RESERVE не ограничивает |
Flat $0.05 (политика по умолчанию)
RESERVE всегда резервирует одинаковую сумму — $0.05, независимо от модели и размера запроса. Это компромисс «достаточно, чтобы поймать очевидное превышение, мало, чтобы не блокировать параллелизм».
Как это выглядит на практике. Команда с бюджетом $20 и моделью openai/gpt-5.4 xhigh (фактическая стоимость ~$4.05/запрос):
- RESERVE #1: pending=$0.05, settled=$0 → $0.05 < $20 ✓ пропускает
- RESERVE #2: pending=$0.10, settled=$0 → $0.10 < $20 ✓ пропускает (FINALIZE первого ещё не пришёл)
- … несколько запросов проходит подряд …
- После FINALIZE: settled=$8.10, бюджет $20 пока не исчерпан
- Когда settled превышает $20, следующий запрос получает 402 на этапе RESERVE — но запросы, уже находящиеся в полёте, успешно завершаются.
Свойства:
- Высокая пропускная способность для дешёвых моделей и больших бюджетов;
- Возможен кратковременный перерасход на величину
actualCost − $0.05× (число запросов в полёте на момент пересечения лимита); - Хорошо работает, когда стоимость одного запроса много меньше доступного бюджета.
Worst-case (catalog-driven)
RESERVE вычисляет верхнюю оценку стоимости запроса по каталогу цен моделей: учитываются input_cost_per_token, output_cost_per_token, максимальное число output-токенов и тип запроса (chat/embedding/image и т. п.). Например, для Claude Opus при контексте в 1 миллион токенов верхняя оценка превышает $8 за 1 запрос — и именно эта сумма резервируется до получения ответа.
Свойства:
- Строгая гарантия неперерасхода: если RESERVE пропустил запрос, фактическая стоимость гарантированно меньше резерва и
FINALIZEтолько освобождает разницу; - Меньшая пропускная способность параллельных запросов на одну и ту же модель: при бюджете $8 и worst-case резерве $4.146 одновременно «в полёте» помещается только один запрос Opus;
- Возможны конфликты по
RESERVEна малых бюджетах под нагрузкой: два параллельных Opus-запроса при $8-лимите — первый пройдёт, второй получит 402.
Это поведение Meridian до введения настраиваемого резервирования. Установка default_mandatory: "worst_case" восстанавливает строгий контроль расходов «как раньше».
Disabled — отключённое резервирование
Если enabled: false, RESERVE использует символическую сумму $1e-9 — настолько маленькую, что pre-check практически никогда не отклоняет запрос. Двухфазный протокол RESERVE/FINALIZE всё равно выполняется (это нужно, чтобы корректно учесть расход), но защитная функция RESERVE отключена.
Свойства:
- Запросы блокируются только после того, как
FINALIZEзафиксировал превышение лимита; - Между моментом пересечения лимита и моментом, когда об этом узнают все ноды кластера, может пройти несколько секунд (зависит от dumper-цикла, см. архитектуру бюджетов);
- В этот период все приходящие запросы успевают пройти RESERVE и попасть к провайдеру — фактический перерасход потенциально неограничен;
- Зато пропускная способность при больших бюджетах не ограничивается резервированием.
Когда какой режим выбирать
| Сценарий | Рекомендуемый режим |
|---|---|
| Стандартный шлюз для команд с бюджетами от десятков долларов | Flat $0.05 (по умолчанию) |
| AI SaaS с подпиской и жёстким лимитом «не больше $X в месяц» | Flat для базовых моделей + model_overrides: "claude-opus-*": "worst_case" для премиум |
| Запуск batch-пайплайна с дорогими моделями (Opus, GPT-4o) на ограниченном бюджете | Worst-case глобально или для премиум-моделей через model_overrides |
| Очень большой корпоративный бюджет (>$10K/мес) с акцентом на пропускную способность | Disabled — учёт через FINALIZE, без блокировки RESERVE |
| Тестовая среда с маленькими бюджетами под несколько десятков долларов | Worst-case: иначе несколько Opus-запросов мгновенно израсходуют месячный тест-бюджет |
| Mixed workload: львиная доля — дешёвые модели, изредка дорогие | Flat + точечные worst_case override для дорогих моделей |
| Высоконагруженная инференс-платформа с тысячами параллельных запросов в секунду | Disabled или Flat — worst-case съест параллелизм |
Главный практический вопрос — что ценнее: строгая защита от перерасхода или высокий параллелизм под пиковой нагрузкой. Worst-case гарантирует первое ценой второго; flat-резерв и disabled — наоборот. Использование model_overrides позволяет получить оба свойства одновременно: строгая защита для дорогих моделей, высокий throughput для дешёвых.
Переопределения по моделям
Глобальная политика может быть точечно переопределена через model_overrides — карту glob-шаблон → значение резерва. Это нужно, когда основной политике подчиняются десятки моделей, а отдельные требуют другого режима.
{
"governance": {
"reservation": {
"enabled": true,
"default_mandatory": 0.05,
"model_overrides": {
"claude-opus-*": "worst_case",
"gpt-4o-*": "worst_case",
"claude-haiku-*": 0.01
}
}
}
}Семантика и приоритеты:
- Шаблоны — синтаксис
filepath.Match(*,?,[abc]); - При совпадении нескольких шаблонов выбирается самый специфичный: длиннее литеральный префикс → больше литеральных символов → раньше вставленный;
- Литеральный ключ
"claude-opus-4.7"сильнее, чем"claude-opus-*"; - Если ни один шаблон не подошёл, применяется
default_mandatory; - Значение
0запрещено и для default-а, и для override-ов — для нулевого резерва используйтеenabled: false.
Имя модели в model_overrides сопоставляется с именем модели как его видит резолвер — без префикса провайдера. То есть для запроса model: "fake/small" шаблоны проверяются против строки "small", не "fake/small". Это особенность Meridian; в логах и в каталоге pricing-моделей используется тот же формат.
Настройка через config.json
Сегодня политика резервирования настраивается только через config.json — отдельной страницы в веб-интерфейсе и API-эндпоинта для динамического переключения политики нет.
Будущие версии могут добавить настройку политики через UI и API. Текущая реализация — статическая: значения берутся из config.json при старте процесса.
Базовая конфигурация (значения по умолчанию)
Если секция governance.reservation не задана, шлюз применяет значения по умолчанию: flat $0.05 с включённым резервированием и без переопределений по моделям. Явно проставлять секцию нужно только если вы хотите отличающееся поведение.
{
"governance": {
"reservation": {
"enabled": true,
"default_mandatory": 0.05
}
}
}Worst-case как политика по умолчанию
Восстанавливает поведение Meridian до введения настраиваемого резервирования: каждый запрос резервирует каталоговую верхнюю оценку.
{
"governance": {
"reservation": {
"enabled": true,
"default_mandatory": "worst_case"
}
}
}Flat-резерв с точечным worst-case для премиум-моделей
Рекомендуемая конфигурация для AI SaaS-сервисов: высокий параллелизм по дешёвым моделям, строгая защита по дорогим.
{
"governance": {
"reservation": {
"enabled": true,
"default_mandatory": 0.05,
"model_overrides": {
"claude-opus-*": "worst_case",
"claude-sonnet-*": "worst_case",
"gpt-4o": "worst_case",
"gpt-4o-mini": 0.02
}
}
}
}Отключённое резервирование (только пост-фактум учёт)
Для крупных потребителей с большими бюджетами и приоритетом на пропускную способность.
{
"governance": {
"reservation": {
"enabled": false
}
}
}При enabled: false поля default_mandatory и model_overrides игнорируются: RESERVE всегда использует символический $1e-9, а реальный учёт расхода выполняется через FINALIZE после ответа провайдера.
Описание полей
| Поле | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
enabled | boolean | true | При false RESERVE использует $1e-9, фактический учёт идёт только через FINALIZE |
default_mandatory | number | "worst_case" | 0.05 | Сумма резерва на запрос. Положительное число = flat $X. Строка "worst_case" = верхняя оценка по каталогу |
model_overrides | object | {} | Карта glob-шаблон → значение резерва. Тот же тип значений, что у default_mandatory |
Правила валидации:
enabled— boolean;default_mandatoryи значения вmodel_overrides— строго положительное число или строка"worst_case";- Значение
0запрещено: для отключённого резерва используйтеenabled: false; - Шаблоны в
model_overrides— синтаксисfilepath.Match; некорректный шаблон валится на старте.
Дальнейшее чтение
- Бюджеты и лимиты — иерархия бюджетов, поток проверки, calendar-aligned сброс, rate limits;
- Виртуальные ключи — основная точка применения governance, к которой привязаны бюджеты;
- Кластеризация и Raft — как резервирования синхронизируются между нодами кластера через Raft (
reservation_ttl, write-path через лидера группы).