За одно и то же окно, по одному ключу, три системы показали три суммы: $0.20 в личном кабинете провайдера, $0.07231 в Langfuse, $0.0711 в Grafana. Ключ был заведён специально под этот стенд, так что посторонний расход исключён. Вопрос выглядел как «кто из трёх врёт».
Правильный вопрос оказался другим.
Сколько здесь независимых измерений
Langfuse и Grafana — не два свидетеля. Обе системы получали число из одной и той же формулы: токены_из_ответа × статичный прайс из конфига. Поэтому они и совпадали друг с другом — не потому, что обе правы, а потому, что у них общий вход. Независимый источник ровно один, и это леджер провайдера: по нему списывают деньги.
| Источник | Откуда берёт число | Независим |
|---|---|---|
| леджер провайдера | по нему списывают деньги | да |
| Langfuse | формула «токены × прайс» | нет |
| Prometheus / Grafana | та же формула | нет |
Согласие двух дашбордов читается как подтверждение и им не является. Считать свидетелей нужно по источникам данных, а не по количеству панелей. После этого разбор сузился до одного вопроса: почему единственная формула недосчитывает почти втрое.
Прайсы оказались верными
Первое подозрение — цены в конфиге устарели. Гипотеза отсекается дёшево: каталог моделей у агрегатора публичный и ключа не требует. Сверка дала совпадение до третьего знака: 0.43 / 0.87 против 0.435 / 0.87 у одной модели, точное 0.10 / 0.40 у другой.
Заодно всплыла деталь, которая позже пригодилась: у reasoning-модели в каталоге стоит отдельная ценовая строка internal_reasoning, не равная цене completion.
Улика: один и тот же множитель по обоим направлениям
Прямая проба с включённым usage-accounting показала расхождение не везде:
| Модель | Своя формула | Выставил провайдер | Отношение |
|---|---|---|---|
| gemini-класс | $6.67e-5 | $6.67e-5 | 1.00 |
| deepseek-класс | $2.81e-4 | $4.82e-4 | 1.7× |
Одна модель сходилась до цента, вторая расходилась в 1.7 раза. Решает здесь не сам разрыв, а его форма. В cost_details промптовая часть была посчитана по цене 0.745/M вместо 0.435/M — и коэффициент 1.7 оказался одинаковым и на промпте, и на completion.
При верных ценах один и тот же множитель сразу по обоим направлениям может означать только одно: числа токенов в ответе — не те числа, по которым выставляют счёт.
Что было под расхождением
Агрегатор биллил по нативным токенам модели-провайдера, а в usage.prompt_tokens и usage.completion_tokens отдавал нормализованные — приведённые к общей шкале, чтобы разные модели можно было сравнивать между собой. У одной модели нативный токенайзер плотнее, отсюда 1.7×. У второй нормализованные совпадают с нативными — поэтому она и сходилась.
Дальше арифметика: самый дорогой и самый нагруженный алиас на стенде был как раз тем, который расходился. Он и тянул общий итог с $0.20 до $0.07.
Разделение двух счётчиков никуда не делось — оно живёт в API до сих пор. Эндпоинт статистики генерации документирует оба набора отдельными полями: tokens_prompt и tokens_completion против native_tokens_prompt, native_tokens_completion, native_tokens_reasoning, native_tokens_cached — последние с пометкой «as reported by provider».
Правило, которое переживает смену провайдера
Собственная формула стоимости — это оценка, а не измерение. Её место — запасной путь. Авторитетное число отдаёт тот, кто выставляет счёт.
Практически это означает четыре вещи:
- Брать
usage.costиcost_detailsиз ответа провайдера как источник истины. - Статичную таблицу прайсов держать страховочной сеткой: она нужна там, где провайдер цены не отдаёт вовсе — например, для локального тира, где цена и так ноль.
- Написать прямо в конфиге, что цены — fallback. Иначе через полгода их снова начнут читать как правду.
- Проверить, что клиентский SDK не срезает нестандартные поля.
costиcost_detailsв OpenAI-схемуusageне входят и попадают в «лишние» поля модели ответа; на практике они переживают SDK и достаются черезgetattr,model_dumpиmodel_extra— но это стоит подтвердить пробой, а не верой.
Рецепт устарел, правило — нет
Замер сделан 14 июля 2026. Перед публикацией я сверился с документацией провайдера — и она описывает другое поведение.
| Что | Замер 14.07.2026 | Документация на 23.08.2026 |
|---|---|---|
| счётчики в ответе | нормализованные | «calculated using the model's native tokenizer» |
| основа биллинга | нативные токены | «pricing are based on these native token counts» |
| включение usage-accounting | параметром в теле запроса | «always included automatically» |
Провайдер выровнял поведение: счётчики в ответе стали нативными, а прежний параметр usage: {include: true} объявлен устаревшим и, по документации, ни на что не влияет. Ставить его больше незачем — и это ровно та строка кода, которую не стоит копировать из статей полугодовой давности.
Замер при этом не опровергнут: цены были сверены с живым каталогом, одна модель сходилась до цента, вторая расходилась одинаковым множителем по обоим направлениям. Изменился вендор, а не факт. Читать авторитетное число вместо собственного пересчёта по-прежнему остаётся единственным способом не разойтись с леджером — и об экономии токенов это ничего не говорит, это отдельный разговор.
Почему арифметики из двух слагаемых мало
Формула prompt × цена_in + completion × цена_out предполагает, что вся генерация описывается двумя числами. Для моделей с reasoning это неверно как минимум двумя способами.
Первый — отдельная ценовая строка: reasoning может тарифицироваться по своей цене, отличной от completion. Второй — неочевидная вложенность. На замере reasoning-токены лежали внутри completion_tokens (261 из 317), но это факт про конкретную пару провайдер-модель, а не общее правило. Если у другой пары они лежат рядом, формула молча потеряет целую статью расхода.
Сюда же кэш: input_cache_read и cache_write_tokens тарифицируются отдельно. В моём замере они были нулевыми, так что поведение формулы при непустом кэше я не проверял.
Есть и соседний класс, где цены нет вовсе: генерация, завершившаяся ошибкой на стороне провайдера, приходит с кодом 200, без контента и без строки стоимости — её не видит ни один слой защиты. Там цена отсутствует. Здесь она есть, просто посчитана не по тем токенам.
Проверка тоже врёт
Сверка «в ноль» получилась не с первого раза, и оба промаха были в самой проверке, а не в системе.
Сначала grep пошёл по несуществующему имени метрики и вернул ноль. Ноль выглядит как «фикс не сработал», хотя означает «я спросил не то».
Потом дельта леджера, прочитанная сразу после прогона, оказалась меньше записанного: дочерние воркфлоу досылали свои вызовы последними, а аккаунтный счётчик провайдера догонял с задержкой. Через пару минут числа совпали ровно — $0.019571 против $0.019571.
Обе ловушки одного класса: проверка соврала про себя, а не про предмет. Тот же класс, что зелёный заголовок политики безопасности, который ничего не доказывает, потому что снимался в состоянии, где нужных запросов ещё нет.
Что остаётся, когда вендор всё исправит
Расхождение нативных и нормализованных счётчиков — деталь конкретного агрегатора, и она уже сгладилась. Механизм под ней остался.
Любой слой между вами и моделью нормализует: приводит счётчики к общей шкале, агрегирует, округляет, добавляет свои строки тарификации. Пока стоимость считается умножением на своей стороне, вы измеряете не расход, а собственную модель расхода. Расходиться она начнёт молча и ровно в тот день, когда поменяется что-то у провайдера.
Простое правило: у стоимости должен быть один источник, и это не ваша формула.