Система биллинга Agent Lab
Система учета стоимости использования ресурсов на уровне компаний.
Принципы работы
Основная идея: Компания имеет баланс, каждый вызов LLM/tool списывает средства с баланса.
Тарифные планы влияют на стоимость через множители к базовой цене:
- FREE - полная цена (множитель 1.0)
- BASIC - скидка 20-30% (множитель 0.7-0.8)
- PREMIUM - скидка 50-70% (множитель 0.3-0.5)
- ENTERPRISE - бесплатно (множитель 0.0)
Компоненты системы
Company (Компания)
Каждая компания имеет:
class Company:
company_id: str # ID компании
tariff_plan: TariffPlan # Тарифный план
balance: float # Текущий баланс в рублях
monthly_budget: float # Месячный лимит расходов (опционально)
current_month_spent: float # Потрачено в текущем месяце
billing_period_start: datetime # Начало периода
BillingService
Основной сервис для работы с биллингом:
Файл: app/services/billing_service.py
Методы:
- can_use_resource() - проверяет достаточно ли средств
- record_usage() - записывает использование и списывает с баланса
- get_resource_cost_for_company() - рассчитывает стоимость для компании
- get_company_usage_stats() - статистика использования
- reset_monthly_billing() - сброс месячных счетчиков
UsageRecord
Запись об использовании ресурса:
class UsageRecord:
usage_id: str
user_id: str
company_id: str
session_id: Optional[str]
usage_type: UsageType # TOOL_CALL, LLM_REQUEST, etc
resource_name: str # "llm:anthropic/claude-sonnet-4.5", "tool:weather_api"
cost: float # Списанная сумма в рублях
quantity: int # Количество (токены, вызовы)
metadata: Dict
timestamp: datetime
Хранятся с ключом: usage:{company_id}:{resource_name}:{usage_id}
Тарифные планы
Определены в app/models/billing_models.py:
TARIFF_PRICES = {
TariffPlan.FREE: {
"llm": {}, # Базовая цена для всех моделей через OpenRouter
"tools": {},
},
TariffPlan.BASIC: {
"llm": {
"anthropic/claude-sonnet-4.5": 0.8, # Скидка 20%
"openai/gpt-4o": 0.8,
},
"tools": {"*": 0.7}, # Скидка 30%
},
TariffPlan.PREMIUM: {
"llm": {"*": 0.5}, # Скидка 50% на все модели
"tools": {"*": 0.3}, # Скидка 70%
},
TariffPlan.ENTERPRISE: {
"llm": {"*": 0.0}, # Бесплатно
"tools": {"*": 0.0},
}
}
Базовые цены ресурсов
Определены в BillingService._get_base_resource_cost():
LLM (за токен через OpenRouter)
Стоимость рассчитывается по токенам (input и output отдельно):
# Настраивается в conf.json
llm_prices = {
"anthropic/claude-sonnet-4.5": {
"input_cost_per_token": 0.00003, # ₽ за токен
"output_cost_per_token": 0.00015, # ₽ за токен
},
"anthropic/claude-opus-4": {
"input_cost_per_token": 0.00015,
"output_cost_per_token": 0.00075,
},
"openai/gpt-4o": {
"input_cost_per_token": 0.000225,
"output_cost_per_token": 0.0009,
},
}
Формула:
Подробнее: LLM документация
Инструменты (за вызов)
tool_base_prices = {
"weather_api": 0.1,
"travel_suggest": 0.2,
"calculator": 0.0, # Бесплатно
"nano_banana_generation": 0.5,
"fashn_buyer_agent": 0.0, # Бесплатно
}
Формат resource_name
Все ресурсы именуются по формату category:resource:
- LLM:
llm:anthropic/claude-sonnet-4.5,llm:openai/gpt-4o - Tools:
tool:weather_api,tool:calculator
Расчет итоговой стоимости
Примеры:
- FREE план, Claude Sonnet 4.5:
- Input: 1000 токенов × 0.00003₽ = 0.03₽
- Output: 500 токенов × 0.00015₽ = 0.075₽
- Базовая: 0.105₽
- Множитель: 1.0 (нет скидки)
-
Итого: 0.105₽
-
BASIC план, Claude Sonnet 4.5:
- Базовая: 0.105₽
- Множитель: 0.8 (скидка 20%)
-
Итого: 0.084₽
-
PREMIUM план, tool:weather_api:
- Базовая: 0.1₽
- Множитель: 0.3 (скидка 70%)
-
Итого: 0.03₽
-
ENTERPRISE план, любой ресурс:
- Базовая: любая
- Множитель: 0.0
- Итого: 0₽ (бесплатно)
Создание инструментов с биллингом
Декоратор @tool
from app.core.tool_decorator import tool
@tool(cost=0.1, billing_name="weather_api")
def get_weather(city: str) -> str:
"""Получить погоду"""
return f"Погода в {city}: солнечно"
@tool # Бесплатный инструмент (cost=0.0 по умолчанию)
def calculate(expression: str) -> str:
"""Калькулятор"""
return f"Результат: {eval(expression)}"
Параметры декоратора:
- cost - базовая стоимость в рублях (по умолчанию 0.0)
- billing_name - название для биллинга (по умолчанию имя функции)
- free_for_plans - не используется в текущей версии
- required_permissions - для будущего расширения
- max_calls_per_hour - для будущего расширения
Важно: Декоратор только добавляет метаданные к функции. Реальный биллинг происходит при вызове через ToolFactory.
LLM биллинг
LLM автоматически оборачиваются в ChatOpenAIWithBilling при создании через фабрику:
from app.core.llm_factory import get_llm
# Создание LLM (автоматически с биллингом)
llm = get_llm("anthropic/claude-sonnet-4.5")
# При вызове:
result = await llm.ainvoke("Привет!")
# Автоматически:
# 1. Проверяется баланс компании через can_use_resource()
# 2. Выполняется запрос к OpenRouter
# 3. Извлекаются токены из response
# 4. Рассчитывается стоимость (input + output)
# 5. Списываются средства через record_usage()
Файлы:
- app/core/llm_factory.py - фабрика LLM
- app/core/llm_billing_wrapper.py - биллинг обертка
Подробнее: LLM документация
Проверки перед использованием
Метод BillingService.can_use_resource() проверяет:
-
Баланс компании - достаточно ли средств
-
Месячный бюджет (если установлен)
Управление компанией
Создание компании
from app.identity.models import Company
company = Company(
company_id="my_company",
subdomain="mycompany",
name="My Company",
tariff_plan="premium",
balance=10000.0, # 10,000₽ начальный баланс
monthly_budget=5000.0, # Лимит 5,000₽/месяц (опционально)
current_month_spent=0.0
)
# Сохранить в БД
storage = Storage()
await storage.set(f"company:{company.company_id}", company.model_dump_json(), force_global=True)
Пополнение баланса
# Получить компанию
company_data = await storage.get("company:my_company", force_global=True)
company = Company.model_validate_json(company_data)
# Пополнить баланс
company.balance += 1000.0
# Сохранить
await storage.set(f"company:{company.company_id}", company.model_dump_json(), force_global=True)
Смена тарифа
company.tariff_plan = "enterprise"
await storage.set(f"company:{company.company_id}", company.model_dump_json(), force_global=True)
Статистика использования
from app.services.billing_service import BillingService
billing_service = BillingService()
stats = await billing_service.get_company_usage_stats("company_id")
# Результат:
{
"total_cost": 1250.50, # Общая стоимость за месяц
"total_calls": 15420, # Общее количество вызовов
"by_resource": {
"llm:anthropic/claude-sonnet-4.5": {
"cost": 900.0,
"calls": 300
},
"tool:weather_api": {
"cost": 350.5,
"calls": 15120
}
},
"by_user": {
"user_123": {
"cost": 800.0,
"calls": 8000
}
}
}
Обработка ошибок
TariffError
Выбрасывается когда ресурс недоступен для тарифа:
from app.exceptions import TariffError
try:
result = await llm.ainvoke("test")
except TariffError as e:
# Предложить повысить тариф
print(f"Доступ запрещен: {e}")
BillingError
Выбрасывается при проблемах с балансом или бюджетом:
from app.exceptions import BillingError
try:
result = await tool.ainvoke(...)
except BillingError as e:
# Предложить пополнить баланс
print(f"Ошибка биллинга: {e}")
Миграция
При запуске приложения Migrator сканирует все функции с декоратором @tool и сохраняет метаданные в БД.
Файл: app/core/migrator.py
Настройка базовых цен
Для LLM цены настраиваются в конфигурации conf.json:
{
"llm": {
"models": {
"anthropic/claude-sonnet-4.5": {
"input_cost_per_token": 0.00003,
"output_cost_per_token": 0.00015
}
}
}
}
Подробнее: LLM документация
Настройка тарифных множителей
app/models/billing_models.py → TARIFF_PRICES
TARIFF_PRICES = {
TariffPlan.BASIC: {
"openai": {
"gpt-4": 0.9, # Меньшая скидка (10% вместо 20%)
}
}
}
Сброс месячного биллинга
В начале каждого месяца нужно сбрасывать счетчик current_month_spent:
Это нужно делать через cron или планировщик задач.
Тестирование
# Все тесты биллинга
uv run pytest tests/billing/ -v
# Конкретный тест
uv run pytest tests/billing/test_billing_service.py::test_can_use_resource -v
Тесты:
- tests/billing/test_billing_service.py - тесты BillingService
- tests/billing/test_simple_billing.py - тесты моделей
- tests/billing/test_tool_billing.py - тесты биллинга инструментов
- tests/billing/test_tariff_prices.py - тесты тарифных множителей
Примеры использования
Проверка доступа к ресурсу
billing_service = BillingService()
can_use, reason = await billing_service.can_use_resource(
user=current_user,
company=current_company,
resource_name="llm:anthropic/claude-sonnet-4.5"
)
if not can_use:
print(f"Доступ запрещен: {reason}")
Запись использования вручную
await billing_service.record_usage(
user=current_user,
company=current_company,
resource_name="tool:custom_api",
cost=0.5,
usage_type=UsageType.TOOL_CALL,
quantity=1,
metadata={"custom_field": "value"}
)
Архитектурные особенности
- Контекст - биллинг использует глобальный контекст для определения текущей компании
- Storage - все записи хранятся в единой таблице с префиксами
- Составные ключи -
usage:{company_id}:{resource_name}:{usage_id}для эффективного поиска - force_global=True - биллинговые данные не привязаны к компаниям через префикс контекста
Будущие улучшения
- Интеграция с платежными системами
- API для управления балансом
- Веб-интерфейс мониторинга
- Алерты при низком балансе
- Экспорт отчетов в CSV/Excel
- Поддержка нескольких валют
- Система скидок и промокодов
См. также
- Архитектура - общая архитектура платформы
- Identity System - пользователи и компании
- Configuration - настройка LLM с ценами