Назад в блог

API и SDKИнструкция

Учёт токенов ИИ-агента на Python: usage, бюджет, стоп

Пишите usage каждого запроса, суммируйте по задаче и останавливайте агента на бюджете в долларах. Рабочий пример на Python для OpenAI-совместимого API.

Автор: CheapTokens TeamОпубликовано 5 мин чтения

Карточка в стиле терминала: лог usage по шагам и проверка бюджета, которая останавливает ИИ-агента
Содержание
  1. Где числа: объект usage
  2. Счётчик, который останавливает агента
  3. Во что складываются 40 шагов
  4. Когда модель думает
  5. Потоковый режим: usage приходит последним
  6. Если числа не сходятся

Агент отработал десять минут, а счёт больше, чем сама задача. Чтобы понять почему, читайте объект usage в каждом ответе API, суммируйте по задаче и умножайте на цену модели за 1M токенов. Чтобы ограничить расход, после каждого шага сравнивайте накопленную сумму с бюджетом и останавливайте цикл, когда лимит пройден. Ниже — небольшой класс на Python, который делает ровно это.

В примере — OpenAI-совместимый адрес https://api.cheaptokens.dev/v1 и модель с id deepseek-ai/DeepSeek-V4-Flash-0731 (контекст 380K, до 32 768 токенов в ответе). Оценить расход до запуска — другая задача, о ней как оценить стоимость ИИ-агента. Здесь — измерение и ограничение прямо во время работы.

Где числа: объект usage#

Каждый обычный (не потоковый) ответ chat completions приходит с объектом usage. В справочнике API DeepSeek (проверено 26 октября 2026) перечислены поля:

  • prompt_tokens — всё, что вы отправили в этот раз: системный промпт, история, результаты инструментов.
  • completion_tokens — что модель написала в ответ.
  • total_tokens — их сумма.

DeepSeek описывает и необязательные поля: prompt_cache_hit_tokens и prompt_cache_miss_tokens (вместе дают prompt_tokens), а также completion_tokens_details.reasoning_tokens для моделей с рассуждением. У страницы нет даты публикации, а эти поля — расширения формата OpenAI, поэтому код ниже считает их необязательными и опирается только на первые два.

Зачем писать каждый шаг, а не только последний? Агент отправляет заново весь растущий контекст. На шаге 1 уйдёт 2 000 входных токенов, на шаге 30 — 9 000, а счёт — сумма всех шагов. Почему так, объясняет статья про долгий чат, а про разницу входа и выхода — input vs output tokens.

Счётчик, который останавливает агента#

Вот всё целиком: счётчик складывает токены, печатает строку на шаг и бросает ошибку при превышении бюджета, а вокруг него — простой цикл агента. Поставьте SDK командой pip install openai, ключ положите в CHEAPTOKENS_API_KEY.

python
import os
from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

# example prices, USD per 1M tokens: use the current ones from the pricing table
PRICE_IN = 0.10
PRICE_OUT = 0.40


class BudgetExceeded(Exception):
    pass


class TaskMeter:
    def __init__(self, budget_usd):
        self.budget = budget_usd
        self.steps = 0
        self.tokens_in = 0
        self.tokens_out = 0
        self.reasoning = 0

    @property
    def cost(self):
        return (self.tokens_in * PRICE_IN + self.tokens_out * PRICE_OUT) / 1_000_000

    def add(self, usage):
        self.steps += 1
        self.tokens_in += usage.prompt_tokens
        self.tokens_out += usage.completion_tokens
        details = getattr(usage, 'completion_tokens_details', None)
        self.reasoning += getattr(details, 'reasoning_tokens', 0) or 0
        print(f'step {self.steps}: in={usage.prompt_tokens} '
              f'out={usage.completion_tokens} task=${self.cost:.4f}')
        if self.cost > self.budget:
            raise BudgetExceeded(
                f'budget ${self.budget:.2f} crossed at step {self.steps}: '
                f'${self.cost:.4f}')


def run_task(goal, budget_usd=0.50, max_steps=40):
    meter = TaskMeter(budget_usd)
    messages = [
        {'role': 'system', 'content': 'Work step by step. Reply DONE when finished.'},
        {'role': 'user', 'content': goal},
    ]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model=MODEL, messages=messages, max_tokens=1024)
        meter.add(resp.usage)  # log first, then decide
        reply = resp.choices[0].message.content or ''
        messages.append({'role': 'assistant', 'content': reply})
        if 'DONE' in reply:
            break
        messages.append({'role': 'user', 'content': 'Continue.'})
    return meter


try:
    meter = run_task('Summarise the main risks of a database migration.')
    print(f'finished: {meter.steps} steps, ${meter.cost:.4f}')
except BudgetExceeded as err:
    print('stopped:', err)

Три решения здесь стоит забрать себе.

Сначала запись, потом решение. meter.add сперва фиксирует шаг и только потом проверяет бюджет, поэтому даже остановленный запуск оставляет полный след. Проверка идёт после вызова, который пересёк черту, так что перерасход — не больше одного шага. max_tokens=1024 ограничивает выход этого последнего шага.

Два жёстких стопа, а не один. max_steps ловит агента, который крутится без больших трат; бюджет в долларах ловит того, кто тратит много за шаг. Нужны оба.

Цены лежат в одном месте. PRICE_IN и PRICE_OUT — условные числа. Актуальные для своей модели берите на странице цен; сейчас DeepSeek-V4-Flash — $0.015 за 1M входных токенов и $0.10 за 1M выходных.

Во что складываются 40 шагов#

Допустим, агент делает 40 шагов, и из-за растущего контекста средний шаг отправляет 6 000 входных токенов и получает 400 в ответ. Это 40 × 6 000 = 240 000 входных токенов и 40 × 400 = 16 000 выходных на задачу.

Шаг 1 отправляет 2 000 входных токенов, шаг 30 — 9 000; итог задачи — сумма всех шагов
Иллюстрация с допущениями статьи, а не результат прогона

На DeepSeek-V4-Flash это $0.0052. Таблица считает ту же нагрузку на остальных моделях и ставит рядом референсную цену OpenRouter:

Модель Стоимость OpenRouter
GLM-5.3-Flash $0.0054 $0.0111
DeepSeek-V4-Flash $0.0052 $0.0139
MiniMax-M2.7 $0.0144 $0.0912

240 000 входных и 16 000 выходных токенов всего. Цены за 1M токенов актуальны и меняются вместе с нашими тарифами; столбец OpenRouter показывает справочные цены для той же нагрузки.

В сумме, с учётом входа и выхода, DeepSeek-V4-Flash в этой задаче выходит дешевле OpenRouter. Заметьте и другое: из 256 000 токенов 240 000 — вход. Счёт — это контекст, который агент тащит за собой, поэтому в логе стоит следить именно за in= на каждом шаге. Бюджет $0.50 на задачу в примере — потолок на случай «убежавшего» агента, а не цель.

Сменить модель — значит поменять MODEL. Id берите из документации CheapTokens: у разных провайдеров имена отличаются, а короткий id из меток цен API не принимает. Для длинных циклов у DeepSeek-V4-Flash одна цена в любое время суток: задача стоит одинаково в полдень и в три ночи. Лимиты модели — в каталоге моделей.

Когда модель думает#

Если ответ в токенах заметно длиннее видимого текста, загляните в reasoning_tokens. Токены рассуждения считаются выходными, поэтому шаг с долгим размышлением даёт заметно больше выходных токенов, чем видно в тексте, а они оплачиваются как выход. Счётчик выше складывает их в meter.reasoning, когда API возвращает поле, и оставляет 0, когда не возвращает, — на отсутствии ключа он не упадёт. Выведите его рядом с out=, чтобы видеть, сколько выхода ушло на размышления. Почему так — в статье про reasoning effort и thinking tokens.

Потоковый режим: usage приходит последним#

Если агент работает в потоке, в обычных чанках usage нет. В формате OpenAI его просят через stream_options={'include_usage': True}. Справочник DeepSeek (проверено 26 октября 2026) говорит, что опция работает только вместе с stream=True (иначе API отвечает 400), что usage равен null во всех чанках, кроме одного, и что последний чанк перед data: [DONE] несёт итог по всему запросу. Значит, в потоковом цикле вызывайте meter.add один раз — на чанке, где usage не пуст. Проще всего начать с непотокового цикла выше; сам потоковый цикл — в стриминге на Python.

Если числа не сходятся#

  • usage равен None. Вы стримите без include_usage. Добавьте опцию или перейдите на обычный вызов.
  • Сумма меньше, чем списано с баланса. Проверьте, что каждый вызов идёт через счётчик: повторные попытки, вызов для сводки или выбора инструмента вне run_task легко потерять. Оберните client.chat.completions.create в одну функцию и пускайте всё через неё.
  • Цена шага всё растёт. Для цикла, который дописывает всё в messages, это нормально. Обрезайте старый вывод инструментов или сжимайте историю; идея — в статье про вывод инструментов вне контекста.
  • BudgetExceeded срабатывает на втором шаге. Бюджет меньше одного шага. Проверьте, что PRICE_IN указан за 1M токенов, а не за 1K.

Чтобы сохранять числа, пишите meter.steps, meter.tokens_in, meter.tokens_out и meter.cost одной JSON-строкой на задачу. Неделя таких строк покажет реальное число токенов на шаг для вашего агента — его и подставляйте в любые оценки. Модели, цены и первый запрос — в документации; вызовы инструментов — в статье про function calling на Python.