Когда расходы на reasoning-модель в вашем отчёте выше ожидаемых, сначала проверьте арифметику полей usage. У OpenAI и Claude детализация рассуждений уже включена в число выходных токенов. У нативного Gemini generateContent выход для обычного текстового запроса считают как сумму candidatesTokenCount и thoughtsTokenCount. Одинаковое действие «прибавить reasoning» поэтому исправляет один расчёт и ломает другой.
Это правила учёта из документации на 7 сентября 2026 года. Они помогают получить базу для расчёта стоимости вывода, но не заменяют полный расчёт счёта с входом, кэшем, инструментами и отдельными тарифами. Примеры ниже условные; реальные платные запросы для них не выполнялись.
Какое поле умножать на тариф вывода
Начните с названия интерфейса, а не с названия модели. Следующая таблица относится к исходным ответам указанных API.
| Интерфейс | Количество оплачиваемых выходных токенов | Что делать с детализацией рассуждений |
|---|---|---|
| OpenAI Responses | usage.output_tokens | usage.output_tokens_details.reasoning_tokens уже внутри итога |
| OpenAI Chat Completions | usage.completion_tokens | usage.completion_tokens_details.reasoning_tokens уже внутри итога |
Gemini generateContent | usageMetadata.candidatesTokenCount + usageMetadata.thoughtsTokenCount | Сложить две отдельные категории, если обе известны |
| Claude Messages | usage.output_tokens | usage.output_tokens_details.thinking_tokens, когда возвращается, уже внутри итога |
У OpenAI это прямо следует из определения числа выходных токенов. У Google отдельные счётчики описаны в UsageMetadata, а цена вывода включает thinking в таблице тарифов Gemini API. У Claude включение thinking в выходной итог и доступная детализация описаны в разделе о стоимости рассуждений.
total_tokens и totalTokenCount нельзя автоматически брать вместо выходного счётчика: общий итог включает также вход. Если умножить его целиком на цену вывода, ошибка возникнет даже при полностью выключенных рассуждениях.
Почему у Gemini другая формула
В нативном generateContent Google отдельно сообщает токены сгенерированных ответов и токены мыслительного процесса. Для обычного текстового случая:
textвыход = candidatesTokenCount + thoughtsTokenCount общий итог = promptTokenCount + candidatesTokenCount + thoughtsTokenCount
В promptTokenCount уже учитывается кэшированное содержимое. Данные о кэше нужны для применения соответствующего тарифа ко входу; добавлять их повторно к общему количеству входных токенов тоже нельзя.
Эта формула не является универсальным правилом для всех продуктов Google. Interactions, Live, Vertex и совместимые интерфейсы требуют проверки собственного формата usage. Для мультимодальных запросов и вызовов инструментов дополнительно важны категории потребления и применимые к ним цены.
Особенно легко ошибиться с посредником, который возвращает completion_tokens для модели Gemini. Он мог уже объединить native-поля в один итог. Пока документация конкретного интерфейса этого не подтверждает, складывать его поля по правилу generateContent нельзя. Сохраните исходный ответ, адрес интерфейса и версию адаптера: одного идентификатора модели недостаточно.
Один условный запрос, три способа записать расход
Представим, что три ответа содержат следующие данные. Это арифметический пример, а не сравнение того, сколько разные модели потратят на одну задачу.
| Ответ | Поля | Правильный итог вывода | Ошибочный расчёт |
|---|---|---|---|
| OpenAI Responses | output_tokens=1200, reasoning_tokens=800 | 1 200 | 1 200 + 800 = 2 000 |
Gemini generateContent | candidatesTokenCount=400, thoughtsTokenCount=800 | 1 200 | Только 400 |
| Claude Messages | output_tokens=1200, thinking_tokens=800 | 1 200 | 1 200 + 800 = 2 000 |
При условном тарифе 10 долларов за миллион выходных токенов правильная стоимость вывода каждого примера равна 1200 × 10 / 1 000 000 = 0,012 доллара. Повторное добавление 800 токенов даёт 0,020 доллара: локальный отчёт завышает эту часть расходов примерно на 66,7%. Если в примере Gemini забыть thinking, отчёт покажет только 0,004 доллара.
Тариф здесь намеренно вымышленный. Для реального расчёта берут цену конкретной модели на дату потребления с учётом режима обслуживания. В частности, не следует безусловно исключать thinking из Batch-цены: в таблице Google цены вывода, включая thinking, приведены и для Standard, и для Batch. Это не основание применять одну цену ко всем моделям или режимам.
Не называйте разность output_tokens − reasoning_tokens точным числом токенов видимого ответа. В выходном счётчике могут присутствовать служебные элементы форматирования, каналов и вызовов инструментов. Даже нулевой reasoning не гарантирует совпадения полного вывода с отдельно токенизированным текстом на экране. Для OpenAI это ограничение прямо указано в документации о подсчёте токенов.

Если thinking не показан или ответ пустой
Отсутствующая детализация и отсутствующий итог — разные ситуации.
Если Claude вернул output_tokens=1200, но не вернул output_tokens_details, база стоимости вывода известна: 1 200. Доля thinking неизвестна. В текущей документации Claude есть поле thinking_tokens, однако его наличие нельзя обещать для каждой модели, старого SDK или посредника. В потоковом ответе эта детализация появляется в финальном message_delta.
Если у Gemini известен только candidatesTokenCount, не подставляйте ноль вместо отсутствующего thoughtsTokenCount без подтверждённого правила конкретного интерфейса. Иначе «не получили данные» превратится в «рассуждений не было». Оставьте итог неопределённым и восстановите его из завершающего usage или другой авторитетной записи. Явно возвращённый ноль, напротив, можно учитывать как ноль.
Пустой текст также не доказывает нулевой расход. OpenAI предупреждает: модель может исчерпать лимит на рассуждения и вернуть незавершённый ответ без видимого текста, сохранив оплачиваемое потребление. max_output_tokens ограничивает в том числе скрытые токены рассуждений. См. руководство по reasoning-моделям.
У Claude скрытое thinking тоже оплачивается; показанное краткое изложение не является счётчиком всех рассуждений. Согласно документации стоимости thinking, создание самого краткого изложения дополнительно не тарифицируется. Для учёта нужен usage, а не длина показанного блока.
Минимальная нормализация без лишнего сложения
Удобно привести ответы к двум собственным полям: output — база для стоимости вывода, reasoning — известная детализация. Второе поле служит аналитике и никогда автоматически не прибавляется к первому. None означает неизвестное значение.
Пример на Python принимает сам объект usage или usageMetadata, а также заранее установленный тип нативного API. Он не распознаёт интерфейс по модели и не вычисляет деньги.
pythondef count(value): if value is None: return None if type(value) is not int or value < 0: raise ValueError("Invalid token count") return value def detail(usage, field, key): value = usage.get(field) if value is None: return None if not isinstance(value, dict): raise ValueError("Invalid token details") return count(value.get(key)) def normalize(api, usage): if api == "gemini.generateContent": candidates = count(usage.get("candidatesTokenCount")) reasoning = count(usage.get("thoughtsTokenCount")) output = (None if candidates is None or reasoning is None else candidates + reasoning) elif api == "openai.responses": output = count(usage.get("output_tokens")) reasoning = detail(usage, "output_tokens_details", "reasoning_tokens") elif api == "openai.chat": output = count(usage.get("completion_tokens")) reasoning = detail(usage, "completion_tokens_details", "reasoning_tokens") elif api == "claude.messages": output = count(usage.get("output_tokens")) reasoning = detail(usage, "output_tokens_details", "thinking_tokens") else: raise ValueError("Unsupported native API") if output is not None and reasoning is not None: if reasoning > output: raise ValueError("Reasoning exceeds output") return {"output": output, "reasoning": reasoning}
Такая функция сохраняет известный включающий итог даже без необязательной детализации и не превращает отсутствующие значения в нули. Отрицательные, дробные, строковые и логические значения отвергаются; доля рассуждений больше итога считается ошибкой данных, а не исправляется обрезанием.
На синтетических примерах локально проверены все четыре ветви, отсутствие детализации и итога, явный ноль, неверные типы и превышение thinking над output. Полноценная система дополнительно проверяет схему ответа, хранит исходный usage, различает финальные и промежуточные данные и учитывает правила повторной доставки событий.
Поток, дубликат события и повторный запрос
Даже правильная формула даст завышенный результат, если применить её несколько раз к одному потреблению.
У Claude значения usage в message_delta накопительные. Если промежуточная запись показывает 40 выходных токенов, а завершающая — 100, итог равен 100, не 140. Используйте последнее авторитетное значение или итоговое сообщение SDK. При обрыве потока итог может остаться неизвестным: последнее увиденное число не следует выдавать за подтверждённый финальный расход. Это поведение описано в документации потоковых ответов Claude.
При записи в собственную базу различайте три случая:
- То же событие доставлено повторно. Повторная обработка одной финальной записи не должна увеличивать расход. Нужна идемпотентная запись по доступным идентификаторам события и ответа.
- Пришло уточнение того же ответа. Накопительный итог обновляет предыдущую запись, а не прибавляется к ней.
- Клиент повторно отправил запрос. Новая попытка может действительно потребить токены. Совпадение prompt не делает её дубликатом для финансового учёта.
Храните собственный идентификатор операции, идентификатор попытки и идентификаторы запроса или ответа поставщика, если интерфейс их предоставляет. Одна пользовательская операция может включать несколько оплачиваемых попыток. Это помогает объяснить расход после тайм-аута: клиент мог не получить завершение первой попытки и отправить вторую.
Отдельный случай — история диалога. Предыдущие рассуждения Claude, сохранённые в контексте, могут оплачиваться как вход следующего запроса; правила сохранения зависят от модели. Это новое потребление на следующем шаге, а не повторное прибавление thinking к выходу предыдущего. Универсальное правило «старое thinking всегда удаляется и бесплатно» неверно; сверяйтесь с документацией thinking.

Как сверить расхождение с реальным счётом
Сначала выберите одну попытку запроса, для которой есть исходный usage и запись поставщика. Проверьте последовательно:
- Совпадают ли модель, интерфейс, время и идентификатор ответа. Для посредника используйте его договорённость о полях и тарифах.
- Не прибавлялась ли детализация рассуждений к включающему итогу. Для Gemini
generateContent, наоборот, проверьте, не потерян ли отдельный thinking. - Финальный ли это usage и не записан ли он несколько раз. Не складывайте накопительные снимки.
- Правильно ли разделены вход, кэш и выход, выбран ли тариф для даты и режима запроса. Проверьте отдельные расходы на инструменты и хранение, если они применимы.
- Не было ли других попыток той же операции. Сопоставляйте потребление по идентификаторам, а не по одинаковому тексту.
Известный пример такого класса ошибки — исторический issue browser-use #4065: добавление reasoning_tokens к completion_tokens завышало локальное отображение usage и стоимости. Issue закрыт исправлением. Он иллюстрирует ошибку клиентского учёта и сам по себе не доказывает двойного списания поставщиком.
Если задача шире и вы выбираете API для проекта, посмотрите сравнение затрат на Gemini, OpenAI и Claude. Для расследования конкретного расхождения полезнее сохранить исходный usage, рассчитанный выходной итог и список попыток: по этим данным можно проверить арифметику, не смешивая её с выбором модели.



