команда · деньги

Ревью подорожало: куда на самом деле уходят токены

К каждому вызову модели движок добавляет свой текст до вашего кода: пресет, правила команды, гайдлайн, контракт. На нашем учебном репозитории это около 12 000 символов, и они одни и те же для правки в одну строку и для MR на сотню файлов. Дифф — переменная часть: на коммите с купоном в нём 2 847 символов. Это первое, что стоит знать о счёте: размер постоянной части задаёт ваш конфиг, а не ваш код.

Второе — что одно и то же ревью, прогнанное дважды в течение часа, стоит разных денег: первый прогон платит за запись кэша, повтор — за чтение.

Третье — что две настройки меняют счёт в разы: одна умножает его, нигде об этом не сказав, другая делит, и это не та, на которую обычно думают. Страница про все три, с числами из наших собственных прогонов.

Почти все числа на этой странице сняты на нашем публичном учебном репозитории: его можно склонировать и прогнать те же ревью у себя. На вашем проекте числа будут другими, но счёт устроен так же — из тех же частей, с тем же кэшем и теми же настройками. И смотреть на него нужно в том же месте: в блоке диагностики прогона, о нём ниже. Цены — по прайс-листам вендоров на 23 сентября 2026 года.

Что уходит в модель и сколько это весит

Замерено на учебном репозитории: пресет nestjs, шесть правил команды и свой review_prompt, допущений в dont_flag нет, полные файлы и окружение проекта включены. В сумме 12 023 символа:

Часть входаРазмер, символовКак оплачивается
инструкции пресета3 527каждый вызов, кэшируется
дефолтный гайдлайн3 056каждый вызов, кэшируется
контракт ревью2 838каждый вызов, кэшируется
правила команды + dont_flag1 500каждый вызов, кэшируется
служебные строки: заголовки блоков, разделители, строка про язык, инструкция про id правила442каждый вызов, кэшируется
собственный review_prompt команды429каждый вызов, кэшируется
окружение проекта: версии из манифестов231каждый вызов, кэшируется
директива о языке вывода (для русского пустая, для других языков 211)0каждый вызов, кэшируется
сам дифф4 158каждый вызов, не кэшируется никогда

Всё, что выше диффа, между прогонами не меняется, поэтому кэшируется. Дифф не кэшируется и не может: он каждый раз другой. В токенах, которыми считает счёт, ту же картину показывает блок диагностики прогона на коммите с купоном: из 14 309 токенов входа 8 394 — постоянная часть (в строке вызова это «кэш: 8,4K»), остальные 5 915 — дифф, полные файлы и служебные строки. И постоянная часть растёт вместе с конфигом: у команды с двумя десятками правил вроде наших один блок правил весит больше пресета.

И платится это на роль, а не на прогон. Весь этот системный блок уходит генератору, потом каждому судье, потом арбитру — и каждый заново вчитывается в ваши правила и ваш пресет. Схема из генератора, двух судей и арбитра оплачивает этот текст трижды, а когда судьи расходятся и зовут арбитра — четырежды. Это и есть арифметика за фразой «разросшийся свод правил оплачивается на каждом вызове каждого прогона» из рецепта про правила. Вход самого судьи ещё тяжелее: он получает весь системный промпт генератора, плюс собственный контракт, плюс файлы целиком.

Почему одно и то же ревью стоит разных денег

Постоянная часть входа в каждом прогоне одна и та же, и именно её можно закэшировать: записать один раз, а на следующих вызовах читать за малую долю цены. У Anthropic запись стоит вдвое против ставки входа, чтение — десятую часть: в двадцать раз дешевле за тот же самый текст. Дифф не кэшируется никогда, он каждый раз другой. На коммите с купоном один и тот же прогон стоил 0,12 $ с записью кэша и 0,09 $ с чтением, и вся разница была в этом блоке. Вендоры работают с кэшем по-разному: Anthropic кэширует то, что пометит запрос, и считает запись и чтение отдельно; OpenAI-совместимые провайдеры кэшируют сами, по совпадающему началу запроса; у посредников вроде OpenRouter свои правила, и для той же модели кэш может не включиться вовсе. Проверить просто: во втором прогоне подряд в блоке диагностики у вызова должно стоять «кэш: …» размером с системный блок. Нет числа — нет и экономии.

арифметика, которую упускают из видуПримем цену системного блока по ставке входа за единицу. Без кэша N прогонов стоят N. С часовым кэшем — 2 за первый, это запись, и по 0,1 за каждый следующий: 2 + 0,1 × (N−1). Два прогона: 2,1 против 2 — кэш проигрывает. Три: 2,2 против 3 — выигрывает и дальше только выигрывает. То есть часовой кэш окупается с третьего прогона в пределах часа, а не со второго: команда, делающая два ревью за утро, платит за запись и не успевает ею воспользоваться. С пятиминутным кэшем запись стоит 1,25, и он окупается уже со второго прогона — если тот пришёл в пределах пяти минут.

Любая правка любого куска системного промпта из таблицы выше даёт новый ключ кэша — включая исправление одного слова в одном правиле. Поправили правило посреди дня — и первый прогон после этого снова платит за запись. Сам TTL в конфигах не живёт: ни в конфиге репозитория, ни в домашнем. Это переменная окружения LLM_CACHE_TTL, у бота в его env, у терминального reviewgate — в вашей оболочке; по умолчанию час, альтернативы — 5m и off. Если прогоны идут реже, чем раз в час, часовой кэш только вредит: каждый прогон платит за запись вдвое. Тогда либо 5m — запись 1,25×, окупается, когда следующий прогон приходит в пределах пяти минут, — либо off, по обычной ставке и без надбавки.

Две настройки, которые меняют счёт в разы

full_file_context — самый дорогой тумблер, который у вас есть. Выключен по умолчанию, и не зря. Включённый, он отдаёт генератору все изменённые файлы целиком — не только те, где нашлись замечания, — плюс их шаблоны, стили и определения того, что они импортируют. У этого свой бюджет в 200 000 символов, и он не заменяет бюджет диффа в 400 000: они складываются.

Наш первый прогон хука на учебном репозитории — ровно пример того, как это выглядит, когда идёт не так: дифф в 37 строк по четырём файлам стоил 0,33 $, потому что pnpm-lock.yaml уехал в контекст целиком — 143 962 байта из 146 179, то есть 98,5% контекста файлов, который получила модель. С lock-файлом в ignore тот же коммит обошёлся в 0,01 $; разбор этого случая — в лесенке хуков. Поэтому порядок важен: кладите lock-файлы, снапшоты, генерируемый код и фикстуры в ignore до того, как включите полный контекст. Иначе вы платите за чтение файлов, в которых нет ни одной строки, написанной человеком.

полный контекст меняет не только ценуЕщё одно, что этот тумблер делает молча: он меняет в контракте правило против домыслов. Без контекста модели велено опираться только на показанный дифф и не предполагать содержимое непоказанных файлов. С ним — опираться на дифф и предоставленный контекст, а если факта нет даже там, не выдумывать: лучше промолчать, чем выдумать. Знать полезно: это меняет то, что модель готова утверждать.

Из настроек файл до вызова модели убирают только две: список ignore и маркер reviewgate-ignore-file с причиной внутри самого файла. Всё остальное — dont_flag, min_severity, правила — это текст в промпте или обработка ответа, и дешевле прогон от них не становится. Сам движок вдобавок отбрасывает удалённые файлы, пустые диффы и бинарники, но это не ваши рычаги.

Включите две настройки, выключенные по умолчанию

.reviewgate/config.yml
# .reviewgate/config.yml
diagnostics: true          # the 🔬 block: every call, its tokens, its duration

cost:
  show: true               # the price line under the summary
  currency: "$"
  models:                  # a price for EVERY model of the run, or no amount is shown
    # $ per 1M tokens, vendors' price lists as of 2026-09-23
    claude-sonnet-5: { input: 2, output: 10 }
    claude-opus-5: { input: 5, output: 25 }
    deepseek-v4-flash: { input: 0.30, output: 1.20 }   # peak rate; off-peak is half

ignore:                    # one of the two levers that remove a file BEFORE
  - "pnpm-lock.yaml"       # the model is called; the other is the in-file marker
  - "**/*.generated.ts"
  - "**/__snapshots__/**"

diagnostics добавляет в отчёт блок, где виден каждый вызов модели: роль, модель, токены входа и выхода и сколько входа пришло из кэша, сколько заняло, сколько замечаний выдал генератор, сколько судья снял или понизил и почему. Без него прогон — чёрный ящик с числом в конце.

cost.show добавляет строку стоимости — и ему нужно ещё одно: цены для каждой модели прогона. Пропустите одну — и денег вы не увидите: в терминале строки не будет, в сводке MR она останется без суммы, с именем модели, для которой нет цены. Это не гипотеза: три наших замерочных прогона в терминале не напечатали цену, потому что DeepSeek не было в прайсе, и отчёт был прав, промолчав, вместо того чтобы показать число, считающее часть прогона.

Два случая, когда прогон вдруг дорожает

Ответ упёрся в потолок вывода. Модель оборвали на полуслове, прогон повторил вызов на пониженном усилии, и оба вызова в вашем счёте — один из них впустую. Отчёт говорит об этом прямо. На нашем собственном репозитории, на ветке из трёх страниц рецептов, это стоило 0,94 $ вместо 0,52 $: тогда потолок по умолчанию был 16 000 токенов, сейчас 64 000, но своей рукой его всё ещё можно опустить. У OpenAI-совместимых провайдеров повтора нет: обрезанный ответ — сразу ошибка прогона.

Reasoning-модель оказалась многословной. Один и тот же вход на DeepSeek v4-flash дал 22,7K, 6,8K и 19,4K токенов выхода за три прогона — разброс втрое, по причинам на стороне модели, а не вашей. Два из этих трёх не влезли бы в прежний потолок 16 000 токенов. Если в вашей схеме есть reasoning-модель, её потолку нужен запас, которого не нужно обычной.

Если нужен жёсткий предел, есть потолок трат в токенах на прогон, выключенный по умолчанию: у бота это переменная LLM_BUDGET_TOKENS, у терминального reviewgate — ключ budget_tokens в блоке llm домашнего конфига. Он не обрывает вас молча, а пишет строки над списком замечаний, как остальные пометки. На 80% — обычная строка: «расход бэкенда anthropic приближается к лимиту прогона: 41 000 из 50 000 токенов». На 100% — с ⚠️: «лимит прогона для бэкенда anthropic превышен: 52 000 токенов при лимите 50 000 — проверьте раскладку ролей и размер диффа». На 300% прогон останавливается на ближайшей контрольной точке: «🛑 ревью остановлено по бюджету … Не выполнено: судейство. Опубликовано то, что успело собраться».

Дальше