YooKassa Billing и 9 фич за одну итерацию: миграция AI Aggregator на V3.0

Когда продукт переходит из стадии MVP в зрелый сервис, наступает момент, который я называю «долговым спринтом» — когда накопившиеся дизайн-решения, которые откладывались ради скорости, нужно реализовать разом. Именно это случилось с AI Aggregator (ask.gptweb.ru) при переходе с V2.0 на V3.0. Аудит выявил 9 нереализованных фич из дизайн-документов, и мы решили закрыть всё за одну большую итерацию — параллельно с третьей фазой миграции LobeChat.
Результат получился внушительным: платёжная система через YooKassa, загрузка файлов с поддержкой vision-моделей, OpenAI-совместимый API для разработчиков, сравнение моделей, ветвление диалогов, голосовые сообщения в боте, генерация изображений через DALL-E и Telegram Mini App. Всё это в одном коммите ee7c698 Implement V3.0: 9 new features. Расскажу, как мы к этому пришли и что узнали по дороге.
Контекст: что такое AI Aggregator и зачем нужен биллинг
AI Aggregator — это мультимодельный чат-сервис, который агрегирует доступ к разным языковым моделям через единый интерфейс. Монорепозиторий состоит из четырёх пакетов: packages/core (бизнес-логика и БД), web (Next.js фронтенд), bot (Telegram-бот) и litellm (прокси к моделям).
До V3.0 весь биллинг существовал только на бумаге: таблицы в БД были, роуты зарегистрированы, но внутри — пустота. Пользователи пользовались сервисом без ограничений, что в боевых условиях означало прямые убытки на токены. Пришло время закрыть этот долг.
При этом мы параллельно работали над форком LobeChat — Phase 3 плана миграции, где нужно было внедрить аналогичный биллинг уже в LobeChat-инфраструктуру. Опыт от AI Aggregator V3.0 напрямую питал решения для LobeChat, и наоборот.
Phase 0: Батчевая DB-миграция как фундамент
Первое правило больших фичевых спринтов — не мигрировать базу по кусочкам. Мы объединили все изменения схемы в одну миграцию, чтобы не плодить зависимые ALTER TABLE в продакшне.
Схема расширилась тремя ключевыми блоками:
Биллинговые таблицы:
-- billing_plans: Free/Basic/Pro
CREATE TABLE billing_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price_rub INTEGER NOT NULL,
tokens_limit BIGINT NOT NULL,
features JSONB DEFAULT '{}'
);
-- billing_payments: история транзакций
CREATE TABLE billing_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
type TEXT NOT NULL, -- 'subscription' | 'topup'
amount_rub INTEGER NOT NULL,
tokens_added BIGINT,
yookassa_payment_id TEXT UNIQUE,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);Поля для файловых вложений в сообщениях:
ALTER TABLE messages ADD COLUMN attachments JSONB DEFAULT '[]';Поля для ветвления диалогов:
ALTER TABLE conversations
ADD COLUMN parent_conversation_id UUID REFERENCES conversations(id),
ADD COLUMN branch_from_message_id UUID REFERENCES messages(id);Этот подход — «батчевая миграция перед фичами» — сэкономил нам как минимум три потенциальных инцидента с несовместимостью схемы в разных окружениях.
Phase 1: YooKassa Billing — от заглушки к реальным платежам
YooKassa — очевидный выбор для российского рынка. В отличие от Stripe, который требует зарубежного юрлица, YooKassa работает с российскими ИП и ООО из коробки. Документация YooKassa API довольно подробная, хотя и менее полирована, чем у Stripe.
Архитектура биллинга получилась трёхуровневой:
1. Core-модуль (packages/core/src/billing.ts) — чистая бизнес-логика без зависимости от HTTP:
export async function createYooKassaPayment(params: {
userId: string;
type: 'subscription' | 'topup';
planId: string;
returnUrl: string;
}) {
const plan = BILLING_PLANS[params.planId];
const payment = await yooKassaClient.createPayment({
amount: { value: plan.price_rub.toFixed(2), currency: 'RUB' },
confirmation: { type: 'redirect', return_url: params.returnUrl },
capture: true,
description: `${plan.name} — WebGPT`,
metadata: { userId: params.userId, planId: params.planId, type: params.type }
});
await db.insert(billingPayments).values({
userId: params.userId,
type: params.type,
amountRub: plan.price_rub,
yookassaPaymentId: payment.id,
status: 'pending'
});
return payment;
}2. API Routes (/api/billing/) — тонкие роуты, которые только валидируют и делегируют:
/api/billing/create-payment— создаёт платёж, возвращает URL для редиректа/api/billing/webhook— принимает уведомления от YooKassa, обновляет статус/api/billing/payments— история платежей пользователя
3. Webhook-обработчик — самая критичная часть. YooKassa присылает уведомления асинхронно, поэтому логика начисления токенов живёт именно здесь:
// app/api/billing/webhook/route.ts
export async function POST(req: Request) {
const event = await req.json();
if (event.object.status === 'succeeded') {
const { userId, planId, type } = event.object.metadata;
if (type === 'subscription') {
const expiresAt = new Date();
expiresAt.setMonth(expiresAt.getMonth() + 1);
await updateUser(userId, {
subscription_plan: planId,
subscription_expires_at: expiresAt,
tokens_balance: BILLING_PLANS[planId].tokens_limit
});
} else {
// topup — просто добавляем токены
await addTokensToUser(userId, TOPUP_PACKAGES[planId].tokens);
}
await updatePaymentStatus(event.object.id, 'succeeded');
}
return new Response('ok');
}Тарифная структура получилась простой и понятной для пользователя: Free (0₽, 50K токенов/мес), Basic (490₽, 500K), Pro (1490₽, 5M). Плюс разовые пополнения: 199₽/500K, 699₽/2M, 1499₽/5M.
Phase 2: File Upload с поддержкой Vision-моделей
Загрузка файлов — одна из самых трудоёмких фич, потому что затрагивает весь стек: хранилище, API, модели, UI и бот.
Для хранения выбрали Supabase Storage — он уже был в инфраструктуре, добавлять новый сервис не пришлось. Документация Supabase Storage хорошо описывает политики доступа и подписанные URL.
Ключевое решение — добавить флаг supportsVision к моделям в конфиге, чтобы UI знал, когда показывать кнопку прикрепления файла:
// packages/core/src/models.ts
export const MODELS = [
{ id: 'gpt-4o', name: 'GPT-4o', supportsVision: true },
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', supportsVision: true },
{ id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', supportsVision: true },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', supportsVision: false },
// ...
];При отправке сообщения с вложениями, API-роут чата формирует multimodal-контент в формате OpenAI:
// Если есть вложения — конвертируем в multimodal формат
const content = attachments.length > 0
? [
{ type: 'text', text: message },
...attachments.map(url => ({
type: 'image_url',
image_url: { url, detail: 'auto' }
}))
]
: message;Телеграм-бот получил обработчики фото и документов — теперь пользователь может прислать картинку в бот, и она уйдёт в модель как attachment.
Phase 3: OpenAI-совместимый API для разработчиков
Эта фича превращает AI Aggregator из пользовательского сервиса в платформу. Разработчики могут подключить свои приложения через стандартный OpenAI API, просто поменяв base_url.
Реализация — тонкая обёртка над внутренним роутингом:
// app/v1/chat/completions/route.ts
export async function POST(req: Request) {
// Аутентификация по API-ключу
const apiKey = req.headers.get('Authorization')?.replace('Bearer ', '');
const user = await validateApiKey(apiKey);
if (!user) return new Response('Unauthorized', { status: 401 });
// Проверка лимитов биллинга
await checkUserLimits(user.id);
// Проксируем запрос через litellm
const body = await req.json();
return streamingChatResponse(user.id, body);
}Управление API-ключами вынесено в настройки — пользователь может создавать и отзывать ключи, видеть их использование. Сами ключи хранятся хешированными в БД, в интерфейсе показываются только при создании.
Phase 4–9: Быстрые победы и интеграции
Сравнение моделей (Phase 4) — по сути одна страница /chat/compare с двумя колонками чата, но её часто просят пользователи, которые хотят понять разницу между GPT-4o и Claude. Добавили ссылку в сайдбар.
Ветвление диалогов (Phase 5) — кнопка «Branch» на пузыре ассистента создаёт форк разговора от этого сообщения. Полезно, когда хочешь попробовать другой промпт, не теряя оригинальный диалог. В сайдбаре ветки визуально отделены индикатором.
Голосовые сообщения в боте (Phase 6) — интеграция с Whisper через litellm для Speech-to-Text. Пользователь присылает голосовое, бот транскрибирует и отвечает как на текстовое.
Inline-режим бота (Phase 7) — позволяет использовать бота в любом чате через @webgpt_bot запрос.
Генерация изображений (Phase 9) — добавили dall-e-3 как отдельную модель в селектор с категорией «image». Чат-роут определяет, что выбрана image-модель, и вместо streaming-текста вызывает generateImage, возвращая URL картинки:
if (selectedModel.category === 'image') {
const imageUrl = await generateImage({
model: selectedModel.id,
prompt: lastMessage,
size: '1024x1024'
});
return Response.json({
type: 'image',
url: imageUrl
});
}Telegram Mini App (Phase 8) — кнопка Web App в стартовом сообщении бота открывает полноценный веб-интерфейс прямо внутри Telegram. Аутентификация через initData от Telegram, верификация подписи на сервере.
Параллельная работа: замена маскота LobeChat
Параллельно с V3.0 шла работа над форком LobeChat. Одна из первых задач — дебрендинг: замена маскота LobeChat на логотип WebGPT.
Маскот жил в нескольких местах одновременно:
public/avatars/lobe-ai.png— оранжевый персонаж в чатахpublic/avatars/agent-default.png— серый робот для агентовpublic/avatars/agent-builder.pngиdoc-copilot.png— тематические вариации- Хардкод в
packages/builtin-agents/src/agents/inbox/index.ts
Решение оказалось элегантным: заменить все 4 файла аватаров на logo.png WebGPT, а хардкод в коде заменить на константу BRANDING_LOGO_URL из @lobechat/business-const. Это покрыло все 40+ мест использования автоматически, без правки каждого компонента.
// До: хардкод
avatar: '/avatars/lobe-ai.png'
// После: через константу брендинга
import { BRANDING_LOGO_URL } from '@lobechat/business-const';
avatar: BRANDING_LOGO_URLРезультат: что получилось
Билд прошёл успешно, все новые роуты появились в Next.js output:
/api/billing/create-payment,/api/billing/payments,/api/billing/webhook/api/upload/v1/chat/completions,/v1/models/api/settings/api-keys/chat/compare/api/conversations/[id]/branch/api/auth/telegram-webapp,/tg-app
TypeScript-ошибки в боте оказались pre-existing проблемами монорепо с rootDir — не связанными с нашими изменениями. Все новые модули типизированы чисто.
Гит-история отражает объём работы: ee7c698 Implement V3.0: 9 new features — один коммит, закрывший весь долг. Следом пошли исправления критических багов в c997bc7 fix: 4 critical bugs, потом UI-редизайн в стиле glass-morphism.
Выводы и уроки
Главный урок этой итерации — батчевая миграция схемы перед фичами стоит своего времени. Когда несколько фич трогают одну и ту же таблицу (сообщения получили и attachments для Phase 2, и поля ветвления для Phase 5), гораздо чище сделать одну миграцию в самом начале. Альтернатива — цепочка ALTER TABLE в продакшне с рисками блокировок и несогласованного состояния между деплоями.
Второй урок — разделение бизнес-логики и HTTP-слоя. Всё, что можно вынести в packages/core — нужно выносить. YooKassa-логика в core-модуле означает, что её можно использовать из API-роута, из бота, из webhook-обработчика без дублирования. Это особенно важно в монорепо, где несколько приложений делят одну кодовую базу.
Третий урок — флаги возможностей на уровне модели. Добавив supportsVision: boolean к конфигу моделей, мы решили сразу несколько проблем: UI знает, когда показывать кнопку прикрепления файла; API знает, как форматировать запрос; тесты могут покрыть оба пути. Это лучше, чем хардкодить список model ID в каждом компоненте.
Четвёртый урок касается дебрендинга форков: никогда не хардкодьте брендинговые ассеты в коде. В LobeChat маскот был захардкожен в нескольких десятках мест, но ключевая проблема была в одном файле с прямым путём к PNG вместо использования константы. Один такой хардкод может пережить сотни рефакторингов и всплыть именно тогда, когда ты уверен, что всё заменил.
V3.0 закрыл технический долг и открыл путь к монетизации. Следующий шаг — аналитика использования по тарифам, A/B-тестирование onboarding-флоу для конверсии в платные планы и более глубокая интеграция LobeChat как white-label фронтенда поверх той же биллинговой инфраструктуры.
Полезные ссылки

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.
Связанный проект
WebGPT Telegram Bot →