Сверка счёта провайдера: токены, ретраи и кэш без споров
Сверка счёта провайдера без споров между финансами и инженерией: токены, отменённые стримы, ретраи, кэш и чек-лист проверки.

Почему счет не сходится
Финансы смотрят на сумму в инвойсе. Инженеры смотрят на request_id, логи, usage и статусы запросов. Оба отдела видят реальные данные, но не одно и то же событие.
Путаница начинается там, где один пользовательский запрос распадается на несколько технических действий. Клиент открыл стрим и закрыл его на середине. Сервис отправил ретрай после таймаута. Тот же промпт пришел повторно и вернулся из кэша. Для бизнеса это одна операция. Для учета это уже несколько следов, и считать их одинаково нельзя.
Если команда работает через единый шлюз, например RU LLM, слоев становится больше. У шлюза есть свои логи маршрутизации и usage, а у апстрим-провайдера - свой биллинг. Без заранее принятой схемы сверки даже разница в несколько процентов быстро превращается в спор.
Обычно расхождение дают четыре вещи:
- финансы сверяют период по инвойсу, а инженерия - по логам приложения
- один запрос попадает и в стрим, и в ретрай, и в итоговый успешный ответ
- кэш уменьшает реальный расход у провайдера, но внутренний отчет считает все ответы одинаково
- разные часовые пояса сдвигают запросы на соседний день или месяц
Часовой пояс ломает сверку особенно часто. Запрос ушел в 23:59 по UTC, а в московском отчете это уже следующий день. На закрытии месяца нескольких минут на границе периода хватает, чтобы получить заметную разницу в токенах и сумме.
Когда нет общих правил, команда уходит в ручную проверку. Финансы достают счет, инженерия ищет отдельные request_id, аналитик сводит все в таблицу. Это долго и почти всегда повторяется в следующем месяце. Спор идет не только о сумме. Спор идет о том, какую систему считать источником истины и в какой момент расход считается состоявшимся.
Что выгрузить до начала
Сверка ломается не на формулах, а на исходных данных. Если финансы смотрят счет за один период, а инженеры выгружают usage за другой, проблема появится сразу. Сначала зафиксируйте один интервал, одну таймзону и один набор систем, из которых вы берете цифры.
Первая выгрузка - счет провайдера и его usage за тот же период. Нужны не только итоговые суммы, но и построчная детализация: время запроса, модель, число входных и выходных токенов, цена, валюта, тип тарификации.
Вторая выгрузка - логи приложения или API-шлюза. Без request_id сверка почти сразу превращается в ручной разбор. Для каждой записи обычно хватает таких полей:
request_idилиcorrelation_id- время отправки и время ответа
- модель и провайдер
- статус запроса и код ошибки
- тип ответа: обычный, streaming, tool call, cached
Этого достаточно, чтобы связать внутренний запрос с записью у провайдера. Если request_id меняется на каждом ретрае, сохраните еще и родительский идентификатор попытки. Иначе один пользовательский вызов вы примете за несколько разных расходов.
Отдельно поднимите события, которые чаще всего дают расхождение: ретраи, отмены стримов, таймауты клиента и попадания в кэш. По ним нужны время события, исходный request_id, причина и итоговый статус. Для кэша важно видеть, вернулся ли ответ из локального слоя, из prompt cache провайдера или запрос все же ушел наружу.
Если какой-то таблицы не хватает, не начинайте сводить суммы. Сначала соберите сырые события в один формат. Потом считайте токены и деньги. Иначе вы потратите час на спор о цифрах, а потом выяснится, что команды смотрели на разные выгрузки.
Порядок сверки по шагам
Если начать с общей суммы в счете, спор почти гарантирован. Рабочий путь другой: сначала выравниваете данные, потом сравниваете суммы по слоям - день, модель, тип расхода, и только после этого ищете спорные запросы.
Для сверки удобнее собрать одну сводную таблицу. В ней каждый запрос должен жить под одним request_id, даже если у вас отдельные логи у приложения, прокси и провайдера.
- Зафиксируйте рамки сверки. Выберите один период, одну валюту и один часовой пояс. Если финансы считают день по Москве, а логи у инженеров лежат в UTC, расхождение появится даже при точных токенах.
- Соберите сырые данные в одну таблицу. Обычно нужны
request_id, время запроса, модель, провайдер, статус, число входных и выходных токенов, признакcache hit, число ретраев, причина отмены стрима и сумма по биллингу. Не ищите ошибки на этом этапе. Сначала добейтесь того, чтобы каждая строка одинаково читалась и для финансов, и для инженерии. - Разложите расход на понятные группы. Отдельно посчитайте входные токены, выходные токены,
cached tokens, неуспешные попытки, отмененные стримы и повторы после таймаута. Это снимает половину споров. Один отдел видит общий счет, другой смотрит только на успешные ответы, а расход часто сидит именно в отменах и ретраях. - Сверяйте итоги сверху вниз. Сначала сравните сумму по каждому дню. Если день не сходится, проверьте разбивку по моделям или провайдерам. Если не сходится группа, спускайтесь до набора
request_idи ищите, где расход попал в другую категорию.
На практике полезно держать отдельный статус для спорных строк, например ждет проверки. Допустим, запрос дал 8 000 входных токенов, стрим оборвался на стороне клиента, а затем приложение отправило повтор. Провайдер мог честно посчитать обе попытки, а внутренняя отчетность - только финальный успешный ответ. Если такие случаи не помечать сразу, цифры будут спорить бесконечно.
Хорошая сверка заканчивается не красивой таблицей, а коротким списком причин расхождений с количеством запросов и суммой по каждой причине. Тогда финансы видят итог, а инженерия понимает, что нужно поправить в логировании и правилах учета.
Как считать токены по одной схеме
Больше всего споров возникает не из-за цены, а из-за разных правил подсчета. Финансы берут usage из ответа API, а инженеры считают токены локально и часто складывают все в одно число. После этого обсуждают уже не факты, а методику.
Зафиксируйте одну схему учета для каждого запроса:
- входные токены считаются отдельно
- выходные токены считаются отдельно
- системный промпт хранится отдельно от ответа модели
usageпровайдера и внутренняя оценка лежат в разных полях- версия модели и токенизатора пишется рядом с запросом
На этом месте отчет ломается чаще, чем кажется. Если в одном месте вы включили системный промпт во вход, а в другом записали его как служебный текст вне расчета, расхождение появится даже без ошибок у провайдера.
Не смешивайте токены ответа с токенами системного промпта. Оба типа могут участвовать в расходе, но это разные части запроса. Когда команда пишет в таблице одно поле all tokens, потом уже нельзя понять, где вырос вход, а где модель просто ответила длиннее обычного.
Проверку лучше вести в два слоя. Сначала сравните usage, который вернул API: prompt_tokens, completion_tokens, total_tokens. Потом отдельно сравните внутреннюю оценку тем же разрезом. Если числа не совпали, не правьте отчет вручную. Пометьте расхождение и найдите причину: другой токенизатор, урезанный системный промпт в логах, округление или повторная сборка сообщения перед отправкой.
Простой пример: в запрос ушло 800 токенов пользовательского текста и 250 токенов системного промпта. Модель вернула 180 токенов. Корректная запись такая: вход 1050, выход 180. Если записать 800 как вход, а 250 спрятать в служебные поля, счет уже не сойдется.
Версию модели и токенизатора храните всегда. Один и тот же текст разные модели режут по-разному. Если маршрут до модели может меняться без смены SDK, в логе должен остаться точный model id, иначе потом придется гадать, почему локальная оценка дала другой результат.
Как разобрать отмененные стримы
Спор по стримам почти всегда начинается с неверной единицы учета. Инженеры иногда считают каждый chunk как отдельный ответ, а провайдер считает один запрос и те токены, которые успел принять и сгенерировать до остановки.
Сначала разложите все остановленные стримы по типу события. Отмена пользователем, обрыв сети и таймаут выглядят похоже в интерфейсе, но для биллинга это разные случаи. Если пользователь нажал stop, клиент сам закрыл соединение. Если сеть оборвалась, клиент мог перестать получать данные, хотя провайдер еще несколько секунд продолжал генерацию. Если таймаут сработал на стороне приложения или шлюза, важно понять, дошел ли сигнал об остановке до провайдера.
Для каждой спорной сессии проверьте четыре вещи:
- один ли это
request_id, даже если chunk-ов много - кто закрыл соединение: клиент, ваш шлюз или провайдер
- в какой момент соединение закрылось относительно последнего полученного chunk
- успел ли провайдер вернуть
usageили записать расход в биллинговый лог
Не складывайте chunk-ы поштучно. Они нужны, чтобы восстановить ход ответа и момент остановки, но не для подсчета отдельных списаний. Если у вас 180 chunk-ов в одном SSE-стриме, это все равно один запрос.
Дальше смотрите на usage. Некоторые провайдеры присылают его только в финальном событии. Если стрим оборвался раньше, в приложении usage может не быть, хотя счет потом покажет расход. В такой ситуации нельзя ставить ноль. Для сверки берите логи провайдера или шлюза, а не только клиентский лог.
Практический пример: пользователь остановил ответ на 220-м токене, приложение получило 220 токенов, а провайдер записал 236. Эти 16 токенов не всегда ошибка. Они могли сгенерироваться после последнего доставленного chunk и до фактического закрытия соединения.
Как учесть ретраи и повторы
Если один пользовательский запрос породил три попытки, не считайте их как три разных события. Сначала свяжите все попытки общим correlation_id. Этот идентификатор должен пройти через API, очередь, воркер и ответ провайдера. Тогда видно, где была первая отправка, где ретрай, а где лишний дубль.
Потом разделите повторы по источнику. Причины у них разные, и в сверке их нельзя смешивать:
- SDK повторил запрос после таймаута или сетевой ошибки
- приложение отправило запрос заново по своей логике
- очередь задач вернула джобу в работу после сбоя
ack - человек или скрипт перезапустил задачу вручную
После этого проверьте каждую попытку по двум наборам данных: вашим логам и выгрузке провайдера. Если попытка получила provider_request_id, usage или попала в биллинг, это уже не шум в логах. Даже если клиент увидел timeout, провайдер мог успеть обработать запрос и включить его в счет.
Обратная ситуация тоже встречается часто. SDK сделал ретрай, но первая попытка умерла до приема на стороне провайдера. Внутри сервиса вы увидите две записи, а в счете будет одна. Поэтому несколько строк с одним correlation_id еще не означают двойной расход.
Нормальная проверка выглядит так: для каждого correlation_id соберите цепочку попыток по времени, отметьте тип ретрая, затем оставьте только те попытки, у которых есть признаки фактического биллинга. После этого сравните сумму токенов и число запросов с внутренним учетом.
На практике спор обычно выглядит одинаково. Воркер ждет 30 секунд, получает timeout, очередь запускает ту же задачу еще раз, вторая попытка успешна. Если первая попытка тоже дошла до модели, у вас будет два списания по одному бизнес-событию. Это не ошибка финансов и не ошибка инженерии. Это цена конкретной схемы ретраев, и ее нужно вынести в отдельную категорию расхода.
Как отделить кэш от реального расхода
Кэш ломает сверку не меньше, чем ретраи. Проблема в том, что под словом кэш обычно смешивают два разных механизма: prompt cache у провайдера и ваш кэш готовых ответов на стороне приложения.
Prompt cache уменьшает число токенов, которые провайдер реально тарифицирует. Ваш кэш ответов вообще может не отправлять запрос наружу. Для финансов это два разных события, и в учете их тоже нужно разделять.
Если не развести их по логам, получится ложный перерасход. Команда увидит 1000 запросов, умножит их на средний расход токенов и получит одну сумму. Счет провайдера покажет другую, потому что часть входных токенов прошла как cached, а часть запросов вообще закрылась внутренним кэшем.
Что писать в логах
Для каждого запроса храните отдельные поля:
- был ли запрос отправлен провайдеру
cache hitилиmissу вашего кэша ответовcache hitилиmissуprompt cacheпровайдера- объем
cached tokens - отдельно
billed input tokensиbilled output tokens
Этого хватает, чтобы потом не гадать по агрегатам.
Дальше правило простое. Если ответ пришел из вашего кэша, не приписывайте этому запросу полный расход токенов. Для внешнего счета у него расход нулевой. Если сработал prompt cache провайдера, не считайте все входные токены как обычные billed input tokens. Часть из них уже прошла по другой схеме тарификации.
Сверяйте кэш по дням, а не только за месяц. Иначе итог уплывет. Частый случай: 30 числа вы прогрели prompt cache большим пакетом запросов, а 1 числа получили резкое падение стоимости на похожем трафике. Если смотреть только месячный итог, финансы решат, что инженеры занизили расход в одном месяце и завысили в другом.
Хорошая сверка показывает три числа отдельно: полный теоретический расход без кэша, фактический биллинг провайдера и экономию от каждого типа кэша. После этого спор обычно заканчивается быстро.
Пример сверки за один день
Возьмем 14 мая. По счету провайдера за день прошло 1 463 900 токенов, а во внутреннем учете вышло 1 629 800. Разница заметная: 165 900 токенов, или чуть больше 10%.
Сначала соберите одну сводную таблицу. Важно помнить, что строки ниже пересекаются. Один и тот же запрос мог попасть и в ретрай, и в отмененный стрим.
| Группа | Запросов | Сырой учет, токены | После сверки, токены |
|---|---|---|---|
| Все запросы дня | 12 480 | 1 629 800 | 1 463 900 |
| Отмененные стримы | 410 | 96 400 | 34 500 |
| Ретраи | 287 | 71 200 | 24 700 |
| Cache hits | 1 930 | 54 700 | 0 |
Почти весь разрыв дали три группы.
У отмененных стримов приложение записывало ожидаемый объем ответа по max_tokens, хотя пользователь останавливал вывод раньше. Провайдер считал только реально отданные токены. На этом месте внутренний учет завысил расход на 61 900 токенов.
С кэшем ошибка была совсем простой. Ответы брались из внутреннего кэша, но в отчет все равно попадала старая оценка стоимости. Это еще 54 700 лишних токенов.
С ретраями картина смешанная. Из 287 повторов 186 вообще не ушли к провайдеру: очередь перезапустила задачу локально после таймаута, а логгер уже создал запись о расходе. Здесь добавилось еще 46 500 токенов лишнего учета.
Остаток закрыли две мелочи: 24 дубля из асинхронного обработчика дали плюс 4 600 токенов в сыром отчете, а 9 успешных вторых попыток, наоборот, не попали во внутреннюю выгрузку. Их пришлось добавить обратно, это 1 800 токенов.
Итоговая сверка выглядит так:
1 629 800 - 61 900 - 54 700 - 46 500 - 4 600 + 1 800 = 1 463 900
Финансы получают одну итоговую цифру. Инженеры получают список причин и конкретные request_id, по которым нужно поправить правила учета.
Ошибки, которые дают лишний расход
Лишний расход обычно появляется не из-за цен провайдера, а из-за того, что команда сравнивает разные события. Финансы смотрят на инвойс по календарному дню, а инженеры берут логи по локальному времени или по времени завершения задачи. Даже при точных цифрах на выходе получается спор.
Самая частая ошибка - разный часовой пояс. Провайдер считает расход в UTC, а BI строит отчет по Москве. Запрос, который ушел в 21:05 UTC, для одной системы попадет во вчера, а для другой - в сегодня. На большом трафике это дает заметный сдвиг по токенам и сумме, хотя лишней генерации не было.
Не менее часто теряют request_id после ретрая. Первый вызов упал по таймауту, приложение повторило запрос, а фоновая задача записала второй результат уже с новым внутренним id. Если потом сверять по user_id, дате и модели, система посчитает два расхода вместо одного инцидента с повтором. Для биллинга нужен один сквозной идентификатор: исходный request_id, attempt_number и причина повтора.
Проблемы дает и смешивание версий модели в одной строке расхода. Разница между двумя версиями может быть небольшой по API, но заметной по цене и числу токенов. Если вы складываете все в колонку Qwen или gpt-4.1, сверка теряет смысл. Храните точное имя модели, ревизию и тариф на момент вызова.
Отмененные стримы тоже часто считают как полный успешный ответ. Это ошибка. Если пользователь закрыл поток после 180 выходных токенов, нельзя подставлять среднюю длину полного ответа или брать расчет из успешных нестриминговых запросов. Считать нужно только то, что сервер реально отдал до отмены.
Полезно зафиксировать несколько простых правил:
- использовать единый часовой пояс для провайдера, логов и BI
- не терять
request_idпри ретраях и фоновой обработке - разделять расход по точной версии модели
- помечать отмену стрима отдельным статусом, а не успехом
Если у вас есть аудит по каждому запросу, делайте его главным источником истины. Тогда у финансов и инженерии будет один и тот же набор событий, а не две похожие, но разные истории.
Быстрый чек-лист перед закрытием
Месячное закрытие проходит спокойно, когда финансы и инженеры смотрят на один и тот же набор фактов. Если отчеты собраны в разных часовых поясах, ретраи смешаны с успешными вызовами, а кэш не вынесен в отдельное поле, расхождение почти гарантировано.
Перед закрытием достаточно пройтись по пяти пунктам:
- Зафиксируйте один период и один часовой пояс для всех выгрузок. Если биллинг у провайдера идет по UTC, а внутренний отчет строится по Москве, часть запросов уедет на соседний день и даст ложную дельту.
- Проверьте идентификаторы. У каждой фактической попытки нужен свой
request_id, а у всей цепочки с ретраями и повторами - общийcorrelation_id. Иначе один пользовательский запрос легко посчитать дважды. - Разделите
usageпо смыслу, а не одной суммой. Минимум нужны отдельные поля для входа, выхода,cachedиstreamed. Когда все сложено вtotal, уже не понять, где живой расход, а где ответ из кэша или оборванный стрим. - Разведите статусы по отдельным корзинам.
success,cancel,timeoutиretryнельзя смешивать. Отмененный стрим и таймаут могут иметь токены, но это не то же самое, что успешный ответ, который дошел до пользователя. - Перед закрытием месяца сделайте короткую сверку по дням. Один день с аномалией найти легко, а месячный ком почти всегда заканчивается долгой ручной перепроверкой логов.
Простое правило такое: если строку нельзя однозначно отнести к дню, статусу и цепочке попыток, ее нельзя отправлять в финальный отчет. Сначала добейтесь нормальной структуры данных, потом считайте деньги.
Что сделать дальше
Если после одной сверки цифры сошлись, этого мало. Нужно закрепить единые правила так, чтобы через месяц команда не спорила заново из-за разных выгрузок, статусов и формул.
Сначала зафиксируйте одну схему полей для всех источников: логов приложения, BI-отчета и финансовой таблицы. Обычно хватает request_id, provider_request_id, модели, провайдера, времени запроса, prompt_tokens, completion_tokens, cached_tokens, признака ретрая, статуса стрима, причины отмены и итоговой суммы. Если в одном месте поле называется retry_count, а в другом attempt, расхождения появятся снова.
Потом введите короткую ежедневную сверку. Не ждите конца месяца, когда накопится тысяча спорных строк. Лучше раз в день поднимать только крупные отклонения, например по проектам, моделям или клиентам, где разница вышла за ваш порог. Так инженерия видит источник ошибки сразу, а финансы не разбирают старые инциденты задним числом.
Рабочий порядок обычно такой:
- раз в день сравнивать расход провайдера и внутренний учет по одинаковому окну времени
- отдельно смотреть отмененные стримы, ретраи и кэшированные ответы
- сразу помечать расхождения как ошибку логирования, особенность тарификации или сбой интеграции
- хранить итог решения рядом с записью, а не в переписке
Если вы работаете с несколькими провайдерами, имеет смысл сделать одну точку учета. Иначе каждый провайдер считает токены и статусы немного по-своему, а команда потом сводит это вручную. На малом объеме это терпимо. В продакшене это быстро превращается в лишние часы и новые ошибки.
В такой схеме RU LLM может быть полезен как единый слой учета: совместимый с OpenAI эндпоинт, общие audit trail по запросам и биллинг внутри РФ упрощают сверку, когда провайдеров и моделей несколько. Но даже в этом случае внутренние правила учета все равно нужно зафиксировать заранее.
И последнее: назначьте владельца процесса. Пока за сверку отвечают все понемногу, за нее не отвечает никто.
Часто задаваемые вопросы
С чего начать сверку счёта с внутренним учётом?
Начните не с суммы в счёте, а с рамок сверки. Зафиксируйте один период, одну валюту и один часовой пояс, а потом сведите в одну таблицу инвойс провайдера, usage и свои логи по request_id или correlation_id.
Когда данные лежат в одном формате, сравнивайте их сверху вниз: день, модель, тип расхода, потом спорные запросы.
Почему часовой пояс так часто ломает сверку?
Берите тот часовой пояс, по которому финансы закрывают период, и приведите к нему все источники. Если провайдер считает по UTC, а BI строит отчёт по Москве, часть запросов уедет на соседний день и даст ложную разницу.
На границе месяца это особенно заметно, поэтому не смешивайте локальное время приложения и время из биллинга.
Какие поля нужно выгружать для нормальной сверки?
Минимум нужен request_id, а для цепочки попыток ещё и correlation_id. Добавьте время запроса и ответа, модель, провайдера, статус, входные и выходные токены, признак cache hit, число ретраев, причину отмены стрима и сумму по биллингу.
Если этих полей нет, команда быстро скатится в ручной разбор строк вместо нормальной сверки.
Как правильно считать токены, чтобы потом не спорить?
Считайте вход и выход отдельно и не смешивайте их в одно поле. Системный промпт храните рядом с запросом и включайте в входные токены по одной и той же схеме во всех отчётах.
Сравнивайте сначала usage от провайдера, потом свою локальную оценку. Если цифры не сходятся, ищите причину в токенизаторе, версии модели или сборке сообщения перед отправкой.
Что делать с отменёнными стримами?
Считайте один стрим одним запросом, даже если он пришёл сотнями chunk-ов. Потом проверьте, кто закрыл соединение и успел ли провайдер записать usage в биллинг.
Если клиент оборвал поток раньше финального события, в вашем логе может не быть usage, но расход у провайдера всё равно появится. В таком случае смотрите лог шлюза или провайдера, а не ставьте ноль.
Как учитывать ретраи и повторы запроса?
Свяжите все попытки одного бизнес-события общим correlation_id, а потом проверьте каждую попытку по признакам реального списания. Если попытка получила provider_request_id, usage или попала в биллинг, это уже расход, даже если клиент увидел timeout.
Если первая попытка умерла до провайдера, а вторая дошла успешно, в счёте будет одна строка, хотя в ваших логах их две.
Как отделить кэш от реального расхода?
Разведите два случая. Если ответ вернулся из вашего кэша, внешний расход равен нулю; если сработал prompt cache провайдера, часть входных токенов пойдёт по другой схеме тарификации.
Храните отдельно cache hit, cached_tokens, billed input tokens и факт отправки наружу. Тогда вы увидите и реальный счёт, и экономию от кэша без догадок.
Почему в логах токенов больше, чем в инвойсе?
Чаще всего внутренняя система завышает расход. Обычно она считает отменённые стримы как полные ответы, записывает локальные ретраи как списания или не убирает запросы, которые закрылись из кэша.
Провайдер считает только то, что реально принял и обработал по своей схеме биллинга. Поэтому разница сама по себе не значит ошибку в инвойсе.
В какой момент запрос можно считать реально списанным?
Считайте расход состоявшимся тогда, когда у вас есть признак фактической обработки у провайдера или в шлюзе. Это может быть provider_request_id, запись в биллинге, финальный usage или другой явный след, что запрос дошёл до модели.
Если вы опираетесь только на клиентский лог отправки, вы будете путать попытку с реальным списанием.
Поможет ли единый шлюз вроде RU LLM уменьшить споры между финансами и инженерией?
Да, единый шлюз упрощает сверку, если вы гоняете трафик через нескольких провайдеров. Когда все запросы идут через один OpenAI-совместимый слой, команде проще держать общие request_id, audit trail и единые правила по токенам, кэшу и ретраям.
Но сам шлюз не решит проблему, если вы заранее не договорились о часовой зоне, статусах и схеме полей. Эти правила всё равно нужно закрепить внутри команды.