Назад в блог

AI для новичковИнструкция

Лимит расходов на пакет LLM-вызовов в Python: журнал и бюджет

Пишите токены и стоимость каждого вызова API в файл-журнал, останавливайте пакет на бюджете и считайте счёт до запуска. Код на Python внутри.

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

Терминал: пакетный прогон останавливается, когда выбран бюджет
Содержание
  1. Что делает прогон Clocktower и что оттуда можно взять
  2. Журнал: один файл, одна строка на вызов
  3. Цикл по пакету: пропускаем сделанное, стопаем на лимите
  4. Оцените счёт до запуска
  5. Счёт больше ожидаемого: читаем журнал
  6. Перед запуском

Скрипт гоняет 200 запросов, вы закрываете крышку ноутбука, а утром узнаёте, во сколько обошлась ночь. Исправить это можно двумя небольшими привычками. После каждого вызова API дописывайте в файл-журнал одну строку с токенами и стоимостью. Перед каждым вызовом сравнивайте накопленную сумму с бюджетом и останавливайтесь, если он выбран. Это меньше 50 строк на Python, а пробный прогон на 5 запросах покажет счёт до того, как начнётся настоящий.

Что делает прогон Clocktower и что оттуда можно взять#

Дваркеш Пател, ведущий Dwarkesh Podcast, устроил турнир из 11 моделей в игре Blood on the Clocktower (по README — 386 партий) и выложил код, логи и рейтинги (github.com/dwarkeshsp/clocktower, проверено 8 ноября 2026). Из репозитория стоит забрать две вещи. В папке каждой партии лежит журнал вызовов с токенами и стоимостью, а расходы пишутся в games/ledger.jsonl. Флаг --budget ограничивает общую сумму; по умолчанию это $1,000. Сайт бенчмарка добавляет третий предохранитель: у всех моделей одинаковый потолок в 8 000 токенов на ход (clocktowerbench.com).

Итого: потолок на каждый вызов, запись о каждом вызове и предел на всю сумму. Турниру нужны все три, и вашему ночному прогону тоже, только в масштабе поменьше.

Журнал — это чековая лента в кассе магазина. Каждая продажа печатает строку, менеджер в любой момент суммирует ленту и видит, когда размен закончился.

Журнал: один файл, одна строка на вызов#

Вот всё целиком. Код использует пакет openai, направленный на CheapTokens (https://api.cheaptokens.dev/v1; ключ и id моделей — в документации).

python
import json
import os
from pathlib import Path

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'
PRICE_IN, PRICE_OUT = 1.00, 2.00  # USD per 1M tokens: example, substitute current ones


class BudgetExceeded(Exception):
    pass


class Ledger:
    def __init__(self, path, budget_usd):
        self.path = Path(path)
        self.budget = budget_usd
        self.rows = []
        if self.path.exists():
            with self.path.open() as f:
                self.rows = [json.loads(line) for line in f if line.strip()]

    @property
    def spent(self):
        return sum(r['cost_usd'] for r in self.rows)

    def done(self):
        return {r['unit'] for r in self.rows}

    def check(self, unit):
        if self.spent >= self.budget:
            raise BudgetExceeded(
                f'stopped before {unit}: spent {self.spent:.4f} of {self.budget} USD'
            )

    def add(self, unit, usage, finish_reason):
        cost = (usage.prompt_tokens * PRICE_IN
                + usage.completion_tokens * PRICE_OUT) / 1_000_000
        row = {
            'unit': unit,
            'model': MODEL,
            'prompt_tokens': usage.prompt_tokens,
            'completion_tokens': usage.completion_tokens,
            'finish_reason': finish_reason,
            'cost_usd': round(cost, 6),
        }
        self.rows.append(row)
        with self.path.open('a') as f:
            f.write(json.dumps(row) + '\n')


def ask(ledger, unit, prompt, max_tokens=400):
    ledger.check(unit)
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{'role': 'user', 'content': prompt}],
        max_tokens=max_tokens,
    )
    ledger.add(unit, resp.usage, resp.choices[0].finish_reason)
    return resp.choices[0].message.content

Каждый вызов проходит одни и те же четыре шага:

Четыре шага вокруг каждого вызова API: проверить бюджет, вызвать, прочитать usage, дописать в журнал
Цикл, который стоит за каждым вызовом в обёртке

Три решения здесь сделаны намеренно.

  • Один JSON-объект на строку (JSONL), файл открыт на дозапись. Если скрипт упадёт на 130-м вызове, вызовы с 1-го по 129-й уже на диске.
  • Сумма берётся из файла, а не из переменной. Перезапустите скрипт — лимит сохранится, потому что журнал помнит, сколько потратил прошлый запуск.
  • finish_reason записан рядом с токенами. Строка со значением length значит, что ответ упёрся в max_tokens и обрезан. Позже это пригодится.

Константы PRICE_ — заглушки. Актуальные числа берите из таблицы цен. Для DeepSeek-V4-Flash на момент, когда вы это читаете, это $0.015 за 1M входных токенов и $0.10 за 1M выходных. Поменяйте MODEL на zai-org/GLM-5.3-Flash или MiniMaxAI/MiniMax-M2.7 и обе константы — тот же журнал будет работать и для них.

Цикл по пакету: пропускаем сделанное, стопаем на лимите#

Цикл короткий. Допустим, ваши запросы лежат в словаре, где ключ — id единицы работы, например doc-001:

python
prompts = {
    'doc-001': 'Summarize this ticket in one sentence: ...',
    'doc-002': 'Summarize this ticket in one sentence: ...',
    # ... your 200 prompts
}

ledger = Ledger('ledger.jsonl', budget_usd=0.50)
already = ledger.done()

for unit, prompt in prompts.items():
    if unit in already:
        continue
    try:
        answer = ask(ledger, unit, prompt)
    except BudgetExceeded as e:
        print(e)
        break
    Path(f'out-{unit}.txt').write_text(answer)

Запустите, на середине нажмите Ctrl+C, запустите снова: готовые единицы пропустятся, так что журнал заодно служит файлом прогресса. Когда бюджет кончится, вместо сюрприза вы увидите одну строку, примерно такую: stopped before doc-142: spent 0.5003 of 0.5 USD.

Обратите внимание на слово before. Проверка стоит перед каждым вызовом, поэтому последний вызов может чуть вывести сумму за бюджет. Насколько — известно: один вызов не может стоить больше, чем самый длинный запрос плюс max_tokens. Для запроса в 1 500 токенов и max_tokens=400 это не больше $0.000063. Поэтому у каждого вызова в обёртке есть max_tokens: так неограниченный риск превращается в число, под которое можно планировать.

Строки файла-журнала с токенами по вызовам и остановкой на бюджете
Пример строк журнала и сообщения об остановке (значения условные)

Оцените счёт до запуска#

Теперь вторая половина: узнать стоимость до старта. Прогоните первые пять запросов через ask() с небольшим бюджетом и откройте журнал. Допустим, пять строк дают в среднем 1 500 входных и 400 выходных токенов. Эти числа и берём для всего пакета.

Для 200 запросов это 200 × 1 500 = 300 000 входных токенов и 200 × 400 = 80 000 выходных. Вот эта нагрузка на живых ценах:

Модель За запрос 200 запросов OpenRouter, 200 запросов
GLM-5.3-Flash $0.000046 $0.0092 $0.0167
DeepSeek-V4-Flash $0.000063 $0.0125 $0.0322
MiniMax-M2.7 $0.00014 $0.027 $0.324

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

В таблице есть и колонка OpenRouter. С учётом входа и выхода вместе DeepSeek-V4-Flash на такой нагрузке выходит дешевле OpenRouter, а именно эта сумма и уходит с баланса за пакет.

Запросы разной длины, так что пять образцов — оценка, а не обещание. Нужна запасная верхняя граница — берите самый длинный из пяти, а остальное поймает лимит бюджета. Хотите сравнить две модели на тех же 200 запросах? Дайте каждой свой файл журнала, например Ledger(f'ledger-{MODEL.split("/")[-1]}.jsonl', 0.50), и потом две суммы лягут рядом.

Попробуйте на своём пакете: впишите токены на запрос из своей выборки и число запросов.

Калькулятор стоимости

Ваш пакет: токены из выборки в 5 запросов

Входные токены на запрос: 1 500 · Выходные токены на запрос: 400 · Запросов в день: 200

МодельВ деньВ месяц
GLM-5.3-Flash$0.0092$0.276
DeepSeek-V4-Flash$0.0125$0.375
MiniMax-M2.7$0.027$0.81

Как считать токены до отправки — в статье про подсчёт токенов через tiktoken.

Счёт больше ожидаемого: читаем журнал#

Третий сценарий самый неприятный. Агент пошёл не туда и крутится в цикле, а в личном кабинете видна только общая сумма. Журнал показывает, куда она ушла. Отсортируйте строки по стоимости:

python
rows = [json.loads(line) for line in open('ledger.jsonl')]
for r in sorted(rows, key=lambda r: r['cost_usd'], reverse=True)[:5]:
    print(r['unit'], r['prompt_tokens'], r['completion_tokens'], r['finish_reason'])

Для агента называйте единицу по шагу (step-17), чтобы видеть форму прогона. Несколько картин повторяются снова и снова; сопоставьте каждую с привычной причиной:

Найдите пары

Сопоставьте картину в журнале с привычной причиной

  • prompt_tokens понемногу растёт в каждой строкеЦикл на каждом шаге заново отправляет всю историю
  • completion_tokens равен max_tokens, а finish_reason — lengthОтветы обрезаются на вашем потолке, а обрезанный текст вы всё равно оплачиваете
  • Одна и та же единица идёт много раз подрядЦикл повторов или агент, который повторяет один шаг
  • Одна строка стоит намного больше остальныхОдин раздутый вход, например целый файл в запросе

Каждую картину видно в журнале задолго до счёта. Первую разобрали в статье почему агент перечитывает историю.

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

Перед запуском#

Чек-лист

Перед первым настоящим запуском

Контекст и лимит ответа DeepSeek-V4-Flash, удобные при выборе max_tokens, есть на её странице: DeepSeek-V4-Flash в CheapTokens.