← На главную
Мини-портфолио · Инструмент 1

CostTracker

Простой счётчик стоимости вызовов LLM. Один файл, ноль внешних зависимостей — копируете в свой проект и сразу пользуетесь.

Зачем это нужно

Каждый запрос к нейросети через API стоит денег: цена считается не «за сообщение», а по числу токенов — кусочков текста размером примерно в ¾ слова. Если запросов несколько подряд, например когда агент сначала разбирает вопрос, потом ищет информацию и затем формирует ответ, прикинуть общую стоимость на глаз почти нереально.

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

Как это устроено

Инструмент не вмешивается в ваш запрос к нейросети и не заменяет его. Он ничего не знает про Anthropic, OpenAI или любой другой сервис и не пытается быть посредником между вами и API. Вы вызываете модель ровно так, как вызывали раньше, а сразу после ответа одной строкой сообщаете трекеру, сколько токенов потрачено. Это значение сервис и так возвращает вместе с ответом.

Что нужно, чтобы начать

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

1 Создайте папку и два файла

В своём проекте создайте папку, например cost_tracker, и положите в неё два файла: cost_tracker.py и pricing.json.

2 Скопируйте это в файл cost_tracker.py

"""
CostTracker — простой счётчик стоимости вызовов LLM.
"""

import json
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional

PRICING_FILE = Path(__file__).parent / "pricing.json"


@dataclass
class CallRecord:
    label: str
    model: str
    input_tokens: int
    output_tokens: int
    cost: Optional[float]  # None, если цена модели неизвестна


class CostTracker:
    def __init__(self, pricing_file: Path = PRICING_FILE):
        self._pricing = self._load_pricing(pricing_file)
        self._calls: List[CallRecord] = []

    @staticmethod
    def _load_pricing(path: Path) -> dict:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        data.pop("_meta", None)
        return data

    def log_call(
        self,
        model: str,
        input_tokens: int,
        output_tokens: int,
        label: str = "",
    ) -> Optional[float]:
        """Записывает один вызов модели и возвращает его стоимость (или None)."""
        cost = self._calculate_cost(model, input_tokens, output_tokens)
        record = CallRecord(
            label=label or f"вызов {len(self._calls) + 1}",
            model=model,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
        )
        self._calls.append(record)
        return cost

    def _calculate_cost(self, model, input_tokens, output_tokens):
        price = self._pricing.get(model)
        if price is None:
            return None
        input_cost = input_tokens / 1_000_000 * price["input"]
        output_cost = output_tokens / 1_000_000 * price["output"]
        return input_cost + output_cost

    def total_cost(self) -> float:
        return sum(c.cost for c in self._calls if c.cost is not None)

    @staticmethod
    def _fit(text: str, width: int) -> str:
        if len(text) > width:
            return text[: width - 1] + "…"
        return text.ljust(width)

    def report(self) -> str:
        """Читаемый построчный отчёт по шагам + итог."""
        if not self._calls:
            return "Пока нет ни одного зафиксированного вызова."

        label_w, model_w, tokens_w = 30, 22, 16
        header = (
            f"{self._fit('Шаг', label_w)}{self._fit('Модель', model_w)}"
            f"{self._fit('Токены in/out', tokens_w)}{'Стоимость':>16}"
        )
        lines = [header, "-" * len(header)]

        for c in self._calls:
            tokens = f"{c.input_tokens}/{c.output_tokens}"
            cost_str = f"${c.cost:.4f}" if c.cost is not None else "цена неизвестна"
            lines.append(
                f"{self._fit(c.label, label_w)}{self._fit(c.model, model_w)}"
                f"{self._fit(tokens, tokens_w)}{cost_str:>16}"
            )

        lines.append("-" * len(header))
        has_unknown = any(c.cost is None for c in self._calls)
        total_line = f"ИТОГО: ${self.total_cost():.4f}"
        if has_unknown:
            total_line += "  (шаги с неизвестной ценой не учтены)"
        lines.append(total_line)

        return "\n".join(lines)

3 Скопируйте это в файл pricing.json

Это справочник цен — во сколько обходится каждая нейросеть.

Как читать эти числа

У каждой модели два числа: input и output.

Оба числа — доллары за миллион токенов, как их публикует сам сервис на странице цен. Число токенов считать самим не нужно: сервис возвращает его вместе с каждым ответом, вы просто передаёте это значение трекеру.

{
  "_meta": {
    "note": "Цены за 1 000 000 токенов, в долларах. Официального API 'текущая цена' ни у одного провайдера нет — эти цифры сверены вручную по документации на дату ниже.",
    "checked_on": "2026-07-02",
    "sources": [
      "https://platform.claude.com/docs/en/about-claude/pricing",
      "https://developers.openai.com/api/docs/pricing"
    ]
  },
  "claude-opus-4-6": { "input": 5.0, "output": 25.0 },
  "claude-sonnet-4-6": { "input": 3.0, "output": 15.0 },
  "claude-haiku-4-5": { "input": 1.0, "output": 5.0 },
  "gpt-5": { "input": 1.25, "output": 10.0 },
  "gpt-5-mini": { "input": 0.25, "output": 2.0 },
  "gpt-4.1": { "input": 2.0, "output": 8.0 },
  "gpt-4.1-mini": { "input": 0.40, "output": 1.60 },
  "gpt-4o-mini": { "input": 0.15, "output": 0.60 },
  "local-model": { "input": 0.0, "output": 0.0 }
}

Пример расчёта: один вызов с 1200 входными и 180 выходными токенами на модели claude-sonnet-4-6 ($3 / $15 за миллион):

input_cost  = 1200 / 1_000_000 * 3.0   = $0.0036
output_cost = 180  / 1_000_000 * 15.0  = $0.0027
итого       = $0.0036 + $0.0027 = $0.0063
Важный нюанс. Ни один провайдер, ни Anthropic, ни OpenAI, не даёт способа узнать «текущую цену» автоматически, такого API попросту не существует. Цены время от времени меняются вручную самим провайдером, поэтому раз в какое-то время стоит заглянуть на официальную страницу цен и поправить нужное число в этом файле. Код трогать не нужно, только цифры в JSON.

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

4 Как это использовать в своём коде

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

from cost_tracker import CostTracker

tracker = CostTracker()

# ваш обычный запрос к нейросети — как и раньше, без изменений
response = client.messages.create(...)

# одна новая строка сразу после ответа:
tracker.log_call(
    model="claude-sonnet-4-6",
    input_tokens=response.usage.input_tokens,
    output_tokens=response.usage.output_tokens,
    label="разбор вопроса пользователя",  # необязательно, просто подпись для отчёта
)

# когда захотите посмотреть, сколько всё стоило:
print(tracker.report())

При работе с OpenAI всё то же самое, только поля с токенами называются иначе:

response = client.chat.completions.create(...)
tracker.log_call(
    model="gpt-4.1",
    input_tokens=response.usage.prompt_tokens,
    output_tokens=response.usage.completion_tokens,
)

tracker.report() можно вызывать где угодно и когда угодно, хоть после каждого ответа, хоть в самом конце. Сам он ничего никуда не выводит и не сохраняет, а печатает то, что вы попросите и когда попросите.

Если вызовов модели несколько. Если ваш агент делает несколько обращений к модели подряд, например сначала разбирает запрос, потом ищет информацию, потом собирает ответ, строку log_call() нужно вставлять после каждого такого обращения, а не один раз в начале. Инструмент не отслеживает ваш код сам: он видит ровно столько шагов, сколько раз вы вызвали log_call(). Если забыть вставить эту строку после одного из вызовов, этот вызов просто не попадёт в отчёт, без какого-либо предупреждения об ошибке.

5 Проверьте, что всё работает

Если хотите сразу увидеть, как выглядит результат, не делая реального запроса к нейросети, создайте рядом третий файл try_it.py с таким содержимым:

from cost_tracker import CostTracker

tracker = CostTracker()

tracker.log_call(
    model="claude-haiku-4-5",
    input_tokens=450,
    output_tokens=60,
    label="шаг 1: разобрать запрос пользователя",
)

tracker.log_call(
    model="claude-sonnet-4-6",
    input_tokens=1200,
    output_tokens=180,
    label="шаг 2: вызвать инструмент поиска",
)

print(tracker.report())

И запустите его в терминале:

python3 try_it.py

Вы увидите готовую таблицу вроде такой:

Шаг                           Модель                Токены in/out          Стоимость
------------------------------------------------------------------------------------
шаг 1: разобрать запрос польз…claude-haiku-4-5      450/60                   $0.0008
шаг 2: вызвать инструмент пои…claude-sonnet-4-6     1200/180                 $0.0063
------------------------------------------------------------------------------------
ИТОГО: $0.0070

Если такая таблица появилась, всё работает. Можно убирать пробный файл и подключать реальные вызовы своей нейросети из шага 4.

Что этот инструмент не делает