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

Откуда берутся споры по токенам
Споры редко возникают из-за одной большой ошибки. Обычно дело в десятке мелких различий в учёте. Один провайдер пишет prompt_tokens и completion_tokens, другой сразу делит расход на вход, выход, кэш и служебные поля, а третий отдаёт usage только в конце стрима или в отдельной выгрузке. Финансы и инженеры смотрят на одни и те же запросы, но видят разные числа.
Даже одинаковый промпт не даёт одинаковый счётчик. У моделей разные токенизаторы, системные вставки и правила обработки JSON, tool calling и reasoning. Иногда провайдер добавляет свои служебные инструкции, а команда их не видит в логе приложения. На бумаге запрос один, а токенов уже больше.
Чаще всего проблема сводится к четырём вещам:
- провайдеры по-разному называют и группируют usage
- один и тот же запрос проходит через разные токенизаторы
- кэш учитывается отдельно не у всех
- в инвойс попадает то, чего нет в прикладных логах
С кэшем путаница особенно частая. У одного провайдера кэшированные входные токены идут отдельным полем и стоят дешевле. У другого они входят в общий input, и скидку видно только в счёте. У третьего кэш есть, но в ответе API это почти не отражено. Поэтому инженер говорит: "мы отправили 2 млн входных токенов", а финансист видит сумму так, будто часть из них считалась по другой ставке.
Есть и ещё одна причина: команды сверяют разные источники. Финансы берут месячный инвойс. Инженеры поднимают логи по запросам, часто только успешным. Но в счёт могут попасть повторные попытки, фоновые проверки безопасности, служебные вызовы, округления по минимуму тарификации и запоздавшие асинхронные задания. Это не обязательно ошибка. Это просто разные правила учёта.
Когда компания работает сразу с несколькими моделями через единый шлюз, разница заметна сильнее. Для приложения это один endpoint, а за ним живут десятки провайдеров со своими форматами usage и правилами биллинга. Если не договориться об общей схеме заранее, закрытие месяца превращается в спор в чате.
Что считать в одной записи
Одна запись должна описывать один фактически оплачиваемый вызов модели. Не чат целиком и не сессию пользователя за день, а один запрос к конкретной модели у конкретного провайдера. Иначе сверка быстро ломается: инженеры смотрят на логи приложения, а финансы - на счёт от провайдера, и цифры не сходятся.
Для единого учёта токенов мало хранить только общий итог. Нужны отдельные поля по тем частям запроса, которые реально влияют на цену.
Сначала входные токены. Это не только текст пользователя, но и системный промпт, шаблон роли, инструкции безопасности и любые вставки, которые приложение добавило перед отправкой.
Выходные токены тоже храните отдельно. Это ответ модели, который получил пользователь, и дополнительные части ответа, если провайдер включает их в биллинг. Когда команда видит только общую сумму входа и выхода, в конце месяца уже непонятно, что выросло: длинные промпты или слишком многословные ответы.
С кэшем нужна отдельная графа. Если повторяющаяся часть промпта попала в prompt cache, не смешивайте эти токены с обычным входом. По деньгам это часто другая ставка, а по анализу это вообще другой тип расхода. У одного запроса могут быть три разных числа: полный вход, кэш-попадание и реально оплаченный некэшированный вход.
Служебные токены тоже не прячьте в общий счётчик. Сюда обычно попадают токены для вызовов инструментов, схем JSON, служебных сообщений между шагами агента и внутренних проходов, если провайдер отдаёт их отдельно. В агентных сценариях это особенно заметно: снаружи пользователь задал один вопрос, а внутри система сделала ещё несколько служебных обменов.
Обычно хватает такого набора полей:
- входные токены пользователя
- входные токены системной части
- выходные токены модели
- токены кэша и служебные токены
- ставка, единица тарифа и валюта
Ставку и валюту лучше хранить рядом со счётчиками, а не в отдельной таблице, на которую потом ссылается полсистемы. Тарифы меняются, провайдеры считают по-разному, а ретро-пересчёт почти всегда заканчивается спором.
Если вы работаете через единый OpenAI-совместимый слой вроде AI Router, такие ответы удобно приводить к одной записи сразу на входе. Тогда позже не нужно разбирать, какой счётчик что означал у каждого провайдера.
Как привести ответы провайдеров к одному виду
У разных LLM-провайдеров одни и те же числа приходят под разными именами. Один пишет input_tokens, другой prompt_tokens, третий отдаёт только общую сумму. Если команда не сведёт всё к одной форме, биллинг быстро превращается в спор о терминах.
Нужна одна внутренняя запись, в которую вы приводите любой ответ провайдера. Не пытайтесь хранить "как у всех". Храните "как у вас", а чужие поля маппите в свой формат.
Минимальная схема
Обычно хватает таких полей:
request_idиattempt_idprovider_id,model_id,tariff_versioninput_tokens,output_tokens,cache_read_tokens,cache_write_tokensservice_tokensили отдельные поля для служебных токенов- флаги
is_stream,is_retry,is_fallback raw_responseиnormalized_at
Эта схема снимает половину проблем. Финансы видят одинаковые счётчики по всем провайдерам. Инженеры видят, из какого ответа появились эти числа.
Сырой ответ лучше хранить рядом с нормализованной записью. Когда провайдер меняет формат usage или внезапно добавляет новый счётчик, команда может быстро перепроверить старые расчёты. Это особенно полезно в месяцах, где были retry и fallback: итоговая сумма есть, но без сырого ответа трудно понять, какой именно вызов её дал.
Не смешивайте ноль и неизвестное значение. Если провайдер явно вернул 0, записывайте 0. Если он не прислал поле вообще, ставьте null и помечайте причину. Иначе отчёт покажет, будто кэш или служебные токены точно не использовались, хотя провайдер просто их не раскрыл.
Отдельно фиксируйте provider_id, model_id и версию тарифа. Модель могла остаться той же по имени, но цена изменилась в середине месяца. Без tariff_version потом сложно доказать, почему два одинаковых запроса стоят по-разному.
Со stream, retry и fallback нужна жёсткая дисциплина. Для stream считайте итог только после завершения ответа. Для retry храните каждую попытку отдельно и связывайте их общим request_id. Для fallback записывайте исходный маршрут: какая модель не ответила, на какую ушёл запрос и чьи токены вошли в счёт.
Если у вас несколько провайдеров за одним endpoint, поддерживать такую схему заметно проще, когда нормализация проходит централизованно, а не в каждом сервисе по-своему.
Как внедрить учёт по шагам
Начните не с дашборда, а с одной общей записи usage. Пока у каждой команды свои поля и свои названия, единого учёта не будет. Сначала договоритесь о минимальном наборе: request_id, дата, провайдер, модель, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, service_tokens, currency, unit_price, total_cost.
Эта схема должна жить в одном месте и меняться по версии. Если в середине месяца вы переименуете поле или решите считать кэш иначе, финансы получат одну цифру, инженеры другую. Правило простое: новые правила вступают в силу только с нового отчётного периода.
Что сделать в первую неделю
- Для каждого провайдера опишите маппинг полей в таблице, а не в памяти команды. Например,
prompt_tokensотправляйте вinput_tokens,completion_tokensвoutput_tokens, а кэш и служебные токены храните отдельно. - Считайте стоимость на уровне каждого запроса. Не ждите конца дня и не умножайте среднюю цену на общий объём. У одного и того же провайдера цена может отличаться по модели, типу токенов и режиму кэша.
- Сохраняйте рядом не только итоговую сумму, но и формулу расчёта. Тогда любой спор разбирается быстро: видно, какие токены попали в счёт и по какой ставке.
После этого введите ежедневную сверку. Сумма по логам за день должна сходиться с вашей агрегированной таблицей и с тем, что позже придёт в инвойсе. Небольшое расхождение из-за округления нормально, но порог лучше зафиксировать заранее, например в пределах 0,5-1%.
Если команда отправляет запросы через единый шлюз, этот этап проще: у вас уже есть один поток запросов и централизованные логи. Но сверку с инвойсом это всё равно не отменяет.
Небольшой пример быстро показывает, работает ли схема. Банк отправляет часть запросов в OpenAI, часть в Anthropic, а внутренний чат использует хостируемую open-weight модель. Для отчёта за день все эти вызовы должны превратиться в записи одного вида. Тогда бухгалтер видит сумму в тенге, а инженер понимает, из каких токенов она сложилась.
В конце закрепите роли. Один человек обновляет маппинг, второй проверяет дневную сумму, третий утверждает изменения правил на новый месяц. Когда ответственность названа заранее, биллинг LLM перестаёт быть постоянной темой для споров.
Какие правила для денег принять сразу
Деньги расходятся с инженерными цифрами обычно не из-за сложных формул, а из-за разных правил учёта. Эти правила нужно зафиксировать в самом начале.
Первое правило: цену берите по той модели, которая реально ответила на запрос. Алиас удобен для кода, но для денег он плохой ориентир. Запись вроде "smart-default" ничего не говорит финансам, если шлюз в этот момент отправил трафик в конкретную модель у конкретного провайдера. В событии должны быть provider, model, версия тарифа и время запроса.
Второе правило: не складывайте все токены в один мешок. Входные, выходные, кэшированные и служебные токены часто стоят по-разному. Иногда служебные токены не нужно показывать клиенту отдельной строкой, но внутри системы их всё равно полезно хранить отдельно. Если провайдер прислал поля для cached input или reasoning tokens, сохраняйте их как есть.
С кредитами и бесплатными квотами договоритесь до первого инвойса. Можно уменьшать ими итог месяца. Можно выводить их отдельной строкой как скидку. Можно относить их на внутренний бюджет команды, которая получила квоту. Любой из этих вариантов рабочий. Плохо только менять трактовку каждый месяц.
С округлением нужна обычная дисциплина. Храните сырой объём токенов и стоимость с точностью хотя бы до 6 знаков после запятой, а до 2 знаков округляйте только финальную сумму отчёта. Если один дашборд округляет каждую строку, а другой округляет только итог, спор появится даже при честных данных.
Отдельно разведите тестовый и боевой трафик. Не по имени проекта в таблице, а по тегу среды, ключу, бюджету и правилам выгрузки в отчёты. Тогда нагрузочный прогон, демо для продаж и ночной эксперимент не попадут в финансовый отчёт продукта.
Минимум правил выглядит так:
- одна запись учёта на один запрос или один батч
- фактическая модель и провайдер в каждой записи
- отдельные поля для input, output, cache и служебных токенов
- одно правило для кредитов, квот и округления
- явная метка среды: test или prod
Если трафик идёт через единый шлюз, часть полей можно собирать централизованно. Но правила денег всё равно задаёт ваша команда. Зафиксируйте их один раз, и закрытие месяца станет обычной операцией, а не спором о том, "чьи цифры правильнее".
Где команды чаще ошибаются
Споры почти всегда начинаются не из-за больших сумм, а из-за мелких расхождений в правилах учёта. Один отчёт считает всё, что пришло от провайдера, другой пытается "очистить" цифры, и в конце месяца никто не понимает, где ошибка.
Если команда гоняет запросы через нескольких провайдеров, разница видна сразу. Особенно когда один поставщик отдельно пишет cache read, другой считает служебные токены внутри input, а третий возвращает usage уже после ретрая.
Типичные ошибки выглядят так:
- кэшированные токены складывают с обычным входом, и скидка на cache hit исчезает в сводной таблице
- повторные запросы после таймаута не помечают отдельно, и расход кажется вдвое выше
- цену берут из документации, а не из фактического счёта
- usage сверяют по UTC, а отчёт по деньгам строят по локальной дате
- служебные токены смешивают с текстом пользователя
На практике это выглядит просто. Команда видит 2 млн входных токенов за месяц и считает бюджет по полной ставке. Потом выясняется, что 600 тысяч из них пришли из кэша и должны стоить дешевле. Ещё 120 тысяч появились из-за повторов после таймаутов. А часть расхода вообще относится к служебной обвязке, которую можно сократить без вреда для ответа.
Если вы сводите несколько провайдеров через один слой, такие ошибки легче поймать на одном уровне учёта. Полезное правило здесь одно: храните рядом и сырые поля провайдера, и свою нормализованную схему. Не заменяйте одно другим.
Простой тест полезнее любого спора: возьмите один день, переведите все метки времени в один часовой пояс, отделите cache tokens, retries и служебные токены, затем пересчитайте сумму по фактическим ставкам из счёта. Обычно после этого расхождение либо исчезает, либо становится понятным за несколько минут.
Пример месяца без ручных споров
У службы поддержки бот отвечает клиентам весь месяц. Обычные вопросы он отправляет к одному провайдеру, а сложные диалоги с длинным контекстом - к другому. Одни и те же системные инструкции повторяются тысячами раз, поэтому часть длинных промптов уходит в кэш.
К концу месяца инженеры смотрят usage по каждому запросу. Они видят входные и выходные токены, частоту кэш-хитов и средний ответ. Финансы смотрят на другое: сколько стоит каждый тип токенов по ставкам провайдера, где кэш дешевле обычного входа, а служебные токены идут отдельной строкой или прячутся внутри input.
Если команда работает через единый шлюз, свести эти данные проще. Ответы разных провайдеров уже проходят через один формат, и вам остаётся договориться об одной таблице для отчёта.
Одна сводка на конец месяца
| Провайдер | Вход | Кэш | Выход | Служебные | Сумма |
|---|---|---|---|---|---|
| A | 1 200 000 | 420 000 | 310 000 | 0 | 184 200 тг |
| B | 640 000 | 150 000 | 290 000 | 86 000 | 233 900 тг |
| Итого | 1 840 000 | 570 000 | 600 000 | 86 000 | 418 100 тг |
Инженер в этой картине видит 2 440 000 токенов по запросам, если сложить вход, кэш как часть входа, и выход. Финансы видят 418 100 тенге и спрашивают, почему сумма выше ожидаемой. Ответ лежит в одной строке: у провайдера B есть 86 000 служебных токенов, а 570 000 кэш-токенов нельзя считать по обычной ставке входа.
После нормализации спор исчезает. Для команды действует простое правило: вход, выход, кэш и служебные токены живут в отдельных колонках, а итоговая сумма считается только после применения ставок к каждой колонке. Тогда инженер быстро находит запросы с ростом usage, а финансист видит, из чего сложился счёт, без ручной сверки CSV и скриншотов.
Такая сводка нужна не только в конце месяца. Если сумма вдруг скачет на второй неделе, таблица сразу покажет источник расхождения: больше сложных диалогов ушло ко второму провайдеру, кэш просел или служебная обвязка стала длиннее после нового промпта.
Быстрые проверки перед закрытием месяца
Перед закрытием месяца не нужен большой аудит. Хватает нескольких коротких сверок, которые ловят почти все причины споров. Если единый учёт уже есть, такие проверки занимают 15-20 минут, а не полдня переписки между финансами и инженерами.
Сначала проверьте арифметику. Сумма запросов по дням должна совпадать с месячным итогом. Если дневные числа дают 3 842 190 токенов, а в сводном отчёте стоит 3 861 000, проблема почти всегда в фильтрах, дубликатах или поздно догруженных логах. Не ищите объяснение в тарифах, пока не сошлась база.
Дальше посмотрите на записи без provider_id и model_id. Даже маленькая доля таких строк портит картину. Финансы видят расход, а инженер не может привязать его к тарифу и источнику. Лучше сразу выносить такие записи в отдельный список и не пускать их в финальный отчёт без ручной разметки.
Отдельно проверьте retries. Повторная отправка запроса не должна теряться внутри общего счётчика. Иначе одна команда считает, что модель стала дороже, а другая знает, что просто выросло число повторов из-за таймаутов.
С округлением часто появляется тихая, но неприятная ошибка. Один отчёт округляет по каждой записи, другой - только итог за день или месяц. На больших объёмах разница уже заметна. Правило должно быть одно и записано явно: где округляете, до какого знака и на каком этапе.
Ещё одна ловушка - кэш. Если кэшированные токены смешаны с обычным входом, команда видит только общий input и теряет реальную картину затрат. Кэш надо показывать отдельно: обычный вход, кэшированный вход, выход и служебные токены. Тогда видно, где вы действительно платите больше, а где система, наоборот, экономит деньги.
Хороший финальный тест звучит скучно, но работает: возьмите один день из середины месяца и пройдите путь от сырого лога до суммы в отчёте. Если на этом отрезке всё читается без догадок, месяц обычно закрывается спокойно. Если уже на одном дне есть пропуски и разная логика подсчёта, в конце месяца спор почти гарантирован.
Что сделать дальше
Не пытайтесь сразу охватить все команды, модели и счета. Для пилота хватит одного сервиса и одного закрытого месяца. Так проще увидеть расхождения, проверить формулы и понять, где учёт ломается ещё до большого запуска.
Начните с сырых данных. Сложите usage от всех провайдеров в одно хранилище без "улучшений" на лету. Сначала сохраняйте ответ как есть, а уже потом стройте нормализованную запись. Это скучный шаг, но именно он спасает, когда финансы спрашивают, почему в отчёте 12 430 000 токенов, а в счёте другая цифра.
Минимальный план такой:
- выберите один продукт со стабильным трафиком и понятным владельцем
- соберите все ответы usage, включая вход, выход, кэш и служебные токены
- зафиксируйте правила пересчёта в единую схему и не меняйте их внутри месяца
- сверяйте итог не только по токенам, но и по деньгам в счёте
Отдельно договоритесь с финансами, какие поля они считают основанием для счёта. Обычно хватает периода, провайдера, модели, project или cost center, типа токенов, цены за единицу, валюты и итоговой суммы. Если этого списка нет, спор начнётся заново при первом же отклонении на 2-3%.
Я бы не начинал с красивого дашборда. Сначала нужен простой реестр, где каждая строка объяснима. Если аналитик или инженер не может руками пройти путь от сырого ответа провайдера до суммы в акте, схема ещё сырая.
Когда провайдеров много, ручная склейка быстро надоедает. В такой ситуации удобно свести маршрутизацию и учёт через AI Router на airouter.kz: это единый OpenAI-совместимый API-шлюз, где можно сохранить привычные SDK, код и промпты, а затем централизованно нормализовать usage по разным провайдерам. Для команд в Казахстане и Центральной Азии полезно и то, что B2B-инвойсинг идёт ежемесячно в тенге.
Хороший итог пилота выглядит просто: за один месяц у вас есть единая таблица, понятные правила и список расхождений с причиной по каждому случаю. После этого схему уже можно расширять на другие сервисы без ручных споров по каждой строке.
Часто задаваемые вопросы
Почему токены в логах и в счёте часто не совпадают?
Обычно вы сравниваете разные источники. Инженеры смотрят на логи запросов, а финансы — на инвойс, где уже учтены ретраи, кэш, служебные вызовы, округление и иногда поздние асинхронные задачи.
Что считать одной записью usage?
Считайте одной записью один фактически оплачиваемый вызов модели. Не объединяйте в одну строку весь чат, сессию за день или несколько попыток одного запроса.
Какие поля нужны в единой схеме учёта?
Минимум храните request_id, attempt_id, провайдера, модель, входные токены, выходные токены, токены кэша, служебные токены, цену, валюту и итоговую стоимость. Этого хватает, чтобы потом проверить и объём, и деньги без догадок.
Как правильно учитывать кэшированные токены?
Не смешивайте кэш с обычным входом. Держите отдельно полный вход, cache_read_tokens или похожее поле и тот объём, который пошёл по обычной ставке, иначе скидка на кэш пропадёт в общей сумме.
Что делать, если провайдер не прислал часть usage?
Ставьте null, если провайдер поле не прислал, и 0, если он явно вернул ноль. Эта разница спасает от ложного вывода, будто кэша или служебных токенов точно не было.
Как считать retries и fallback без путаницы?
Каждую попытку храните отдельно под общим request_id. Для денег берите ту модель и того провайдера, которые реально обработали конкретную попытку, а не алиас из кода.
Когда нужно округлять стоимость?
Округляйте только финальную сумму отчёта, а в базе держите токены и стоимость с запасом по знакам после запятой. Если один отчёт округляет каждую строку, а другой — только итог, спор появится даже при честных данных.
Как не смешать тестовый и боевой трафик?
Разведите среды по тегу, ключу, бюджету и правилам выгрузки, а не по названию проекта. Тогда демо, нагрузочные тесты и ночные эксперименты не попадут в боевой финансовый отчёт.
Нужно ли сохранять сырой ответ провайдера?
Да, храните его рядом с нормализованной записью. Когда провайдер меняет формат usage или цена меняется в середине месяца, вы быстро проверите старый расчёт и увидите, откуда взялась каждая цифра.
С чего начать внедрение единого учёта без большого проекта?
Начните с одного сервиса и одного закрытого месяца. Соберите сырые ответы, введите одну схему для всех провайдеров, посчитайте стоимость на уровне каждого запроса и каждый день сверяйте сумму по логам с агрегированной таблицей.