П/ВИН

API ключи нейросетей: строю AI-враппер GPTWEB на Next.js

·11 мин чтения

Самое муторное в AI-враппере - не код, а десяток провайдеров моделей, у каждого своя цена, своя наценка и свой способ пополнения баланса из России. Я держу такую штуку в продакшене: это наш проект GPTWEB, который собирает api ключи нейросетей в один удобный интерфейс и продаёт к ним доступ, плюс биллинг, тарифы и блог для SEO. Звучит просто ровно до того момента, пока сам не начнёшь это поддерживать: от переписанной с нуля админки до починки автогенератора статей, который тихо умер после переезда на новый сервер.

Это не история про одну киллер-фичу. Это история про то, как живёт реальный продукт: его ломает миграция, его экономику диктуют цены провайдеров, а новые возможности приходится прикручивать так, чтобы они работали из коробки и ничего не стоили. Если вы строите или думаете строить свой враппер вокруг GPT/Claude/Gemini — здесь много граблей, на которые я уже наступил за вас.

Что такое GPTWEB и из чего он состоит

GPTWEB (ask.gptweb.ru) — это мульти-репозиторный проект, то есть продукт собран не из одного приложения, а из нескольких независимых частей, каждая в своём репозитории:

  • landing — публичный лендинг, где посетитель видит тарифы и регистрируется;
  • aggregator — собственно чат с моделями, форк LobeChat (популярный open-source UI для работы с LLM);
  • admin — внутренняя админка для управления пользователями, биллингом и контентом;
  • bot — телеграм-бот как альтернативная точка входа.

Вся эта конструкция написана на Next.js (App Router, React 19, TypeScript) и развёрнута на самостоятельно поднятой инфраструктуре — без Vercel, который в России с 2025-го заблокирован. Данные лежат в self-hosted Supabase, доступ к таблицам идёт через PostgREST.

Почему так сложно? Потому что у враппера три источника нагрузки на разработку одновременно: продуктовая часть (чат, удобство), коммерческая часть (тарифы, оплаты, аналитика расходов) и маркетинговая часть (SEO-блог, который должен приводить трафик). И всё это надо держать живым параллельно.

Большой апдейт админки и переработка биллинга

Первый крупный блок работы — это Admin v3.0: мы переписали админку в четыре фазы, и самой тяжёлой оказалась финансовая. Раньше админка показывала разрозненные цифры, а нужен был нормальный финансовый контур: выбор диапазона дат, KPI-карточки, графики на recharts, детализация расходов по каналам маркетинга и операционным статьям. Это десяток коммитов только в одной фазе, потому что финансовая аналитика — это не «нарисовать график», а аккуратно посчитать выручку, расходы и маржу так, чтобы цифры сходились.

И вот тут случилась первая классическая ловушка рефакторинга. В ходе переработки старая страница управления тарифами app/(admin)/billing/page.tsx была превращена в редирект на новый финансовый раздел:

// Было: полноценная CRUD-страница тарифов
// Стало:
import { redirect } from "next/navigation";
 
export default function BillingPage() {
  redirect("/finance/payments");
}

Логика была такая: «биллинг переезжает в финансы». Но переехала только аналитика платежей, а самой страницы создания и редактирования тарифов в новом разделе так и не появилось. Ссылку на /billing из сайдбара тоже убрали. В итоге получилось коварное состояние: бэкенд полностью рабочий — API /api/billing/plans (GET/PUT) отвечает, данные тарифов (free / basic / pro) лежат в таблице billing_plans целыми, — а интерфейса, через который ими можно управлять, просто нет. Тарифы «пропали» не из базы, а из UI.

Урок номер один, который я записал себе крупными буквами: редирект — это не миграция функциональности. Если ты заменяешь страницу на redirect(), ты обязан убедиться, что вся её функциональность реально доступна в новом месте, а не только её половина. CRUD тарифов и просмотр платежей — это разные задачи, и схлопывать их в один редирект нельзя.

Переезд сервера, который тихо всё сломал

Второй блок — это разбор последствий миграции на новый сервер. Симптомов было два, и оба пользователь сформулировал так: «из админки куда-то пропали тарифы, и сломались статьи в блоге после переезда».

Про тарифы я уже рассказал выше — это оказался не баг переезда, а след рефакторинга. А вот блог — это чистая история про то, как миграция ломает невидимые связи. Здесь я работал строго по методике систематической отладки: сначала найти корневую причину, и только потом чинить. Никаких «давайте просто перезапустим и посмотрим».

Оказалось, что у блога был автогенератор статей — он сам писал черновики по расписанию. И он перестал создавать новые драфты, как будто статьи «перестали писаться». Копаем.

Первая находка — ошибка от Supabase/PostgREST:

PGRST205 — Could not find the table 'ai_aggregator.posts'
... Perhaps you meant 'ai_aggregator.blog_posts'

При переезде таблицы переименовали, добавив префикс blog_: posts стала blog_posts. Часть кода уже знала новые имена (API админки обращался к blog_posts корректно), а часть — нет. Это типичная беда миграций: схема меняется в одном месте, а все её потребители обновить забывают.

Но главная причина была глубже — в инфраструктуре. Генератор запускается не из приложения, а через системные таймеры на сервере. Проверяю их статус и вижу картину:

КомпонентСостояние
blog-generate.timer✅ срабатывает по расписанию
blog-generate.service❌ failed: status=203/EXEC
blog-keywords.service❌ failed: status=203/EXEC
blog-sync.service❌ failed: status=203/EXEC

Ключ здесь — код ошибки 203/EXEC. В systemd это означает ровно одно: служба не смогла запустить исполняемый файл, потому что его нет по указанному пути. Таймеры исправно тикали и дёргали сервисы, а сервисы падали мгновенно, не найдя свои скрипты.

Почему не нашли? Потому что при переезде проект переименовали из ai-aggregator в ai-aggregator-lobechat, а unit-файлы systemd так и указывали на старый путь вида /home/deploy/projects/ai-aggregator/scripts/*.sh. Папки с таким именем больше не существует — значит, и скриптов по этому адресу нет.

Это очень показательный момент. Таймер «зелёный», расписание соблюдается, в логах приложения тишина — со стороны кажется, что всё работает. А по факту три фоновых процесса (генерация статей, сбор ключевых слов, синхронизация) каждый запуск умирали за миллисекунды. Если бы я доверился симптому «таймеры срабатывают» и не дошёл до статуса самих сервисов, root cause так и остался бы скрытым.

Чинил я это в правильном порядке: восстановил три скрипта (blog-generate, blog-keywords, blog-sync) из приватного репозитория, проверил их зависимости, а потом тестировал по нарастающей — сначала самый лёгкий blog-keywords вручную, затем blog-sync (он сразу импортировал статью), и только в конце тяжёлый blog-generate, который ходит в LLM и стоит денег за каждый запуск. Такой порядок не случаен: дешёвое и быстрое проверяем первым, дорогое и медленное — последним, когда уже уверены, что вся обвязка на месте.

Урок номер два: миграция переименовывает не только то, что видно. Имя проекта зашито в пути systemd-юнитов, имена таблиц — в коде, который к ним обращается. Чек-лист переезда обязан включать «найти все жёстко прописанные пути и имена» — иначе тебя ждут отложенные поломки, которые всплывут не в момент переезда, а через сутки, когда сработает первый таймер.

Экономика враппера: как выбирать провайдеров моделей

Третий блок — стратегический. Враппер живёт на марже между тем, сколько он платит провайдеру за токены, и тем, сколько берёт с клиента. Поэтому встал вопрос: через кого вообще работать — OpenRouter, WaveSpeed, Fal.ai и так далее? Причём заранее было понятно, что для текстовых моделей логичен один провайдер, а для картиночных/видео — другой.

Я запустил параллельное исследование по двум направлениям — текстовые LLM и генерация изображений/видео/аудио. И тут вылезла чисто инженерная проблема harness'а: субагенты, которым я раздал задачи, упёрлись в запрет на веб-поиск — WebSearch и WebFetch были недоступны по политике, хотя в главной сессии работали. Лечится это добавлением нужных инструментов в права субагентов, после чего оба исследования прошли нормально.

Итоговый расклад по текстовым моделям (апрель 2026, цены за 1M токенов вход/выход) получился такой:

МодельПрямой доступOpenRouterАгрегатор с криптойРоссийский реселлер (₽)
Claude Opus5 / 255 / 256.5 / 32.5 (+30%)~17 / 84 (~3x)
Claude Sonnet3 / 153 / 153.9 / 19.5~8.6 / 43
GPT-51.25 / 10= прямой+30%~8 / 50

Главный вывод: OpenRouter в большинстве случаев отдаёт модели по цене, равной прямому доступу, но решает проблему оплаты из России и даёт единый OpenAI-совместимый эндпоинт на все модели сразу. Российские реселлеры удобны рублёвой оплатой, но платишь за это наценкой в 2-3 раза. Для картинок и видео расклад другой — там свои специализированные площадки (Fal, Replicate, WaveSpeed, Runware), и смешивать их с текстовым провайдером — нормальная архитектура, а не костыль.

Урок номер три: в AI-сервисе выбор провайдера — это не техническое, а экономическое решение. Разница между «платить напрямую» и «платить через реселлера» может утроить себестоимость, а значит — съесть всю маржу. Это надо считать на цифрах до запуска, а не после.

Бесплатная генерация дизайна через собственный LLM-роутер

Четвёртый блок — пример того, как новая фича прикручивается к экосистеме почти бесплатно. Задача: поднять self-hosted инструмент генерации дизайна (open-design от nexu-io) на отдельном домене и заставить его работать через нашу же бесплатную модель из внутреннего LLM-роутера.

У нас уже крутился собственный роутер на базе 9Router — OpenAI-совместимый прокси с бесплатными моделями: Cerebras и Groq (быстрые free-tier) плюс локальная Ollama. Сначала я зафиксировал модель cerebras/qwen-3-235b, но при живой проверке она вернула 404 — на нашем аккаунте Cerebras её просто нет. Это ровно та причина, по которой нельзя «выбрать модель по списку и расслабиться»: список моделей роутера и список реально доступных на аккаунте моделей — это два разных множества. Перебрал работающие варианты и взял дефолтом cerebras/zai-glm-4.7 (GLM 4.7 — один из сильнейших free-вариантов именно под вёрстку и дизайн).

Дальше всплыл ключевой риск, который я проверил до деплоя: умеет ли open-design генерировать через обычную OpenAI-совместимую модель (BYOK — bring your own key), или ему обязательно нужен CLI-агент вроде Claude Code, которого в образе нет. Веб-документация ответа не давала, поэтому я разобрал исходники — и подтвердил, что генерация работает по чистому /v1/chat/completions, без всякого CLI-агента. Наш роутер подходит.

Ещё пара находок при деплое оказались важными:

  • готовый образ ghcr.io/nexu-io/od:latest отдавал 403 (приватный), пришлось собирать из исходников — и это даже к лучшему, потому что при сборке можно зашить наш роутер в DEFAULT_CONFIG, чтобы бесплатная модель работала из коробки в любом браузере;
  • дефолтный провайдер модели у open-design хранится в localStorage браузера, а не в образе — то есть «вшить» его надо именно в конфиг сборки, иначе каждому пользователю пришлось бы настраивать вручную;
  • роутер валидирует API-ключ живым запросом к базе, так что выданный ключ начинает работать сразу, без перезапуска сервиса.

Доступ к инструменту закрыли basic-auth на уровне Caddy (наш фронтенд-прокси), а сам сервис запустили в режиме доверенного прокси. Отдельно отмечу здравую защиту: когда я попытался зашить реальный API-ключ роутера прямо в исходник, классификатор это заблокировал — и правильно, потому что такой ключ попал бы в клиентский бандл и был бы виден любому в браузере. Секреты в публичный фронтенд-код не зашивают, точка.

Урок номер четыре: своя инфраструктура окупается на новых фичах. Потому что у нас уже был LLM-роутер с бесплатными моделями, подключение нового инструмента генерации дизайна обошлось практически в ноль по операционным расходам — не пришлось платить ни за один внешний токен.

Выводы

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

Главный сквозной урок — про обманчивость зелёных индикаторов. Тарифы «пропали», хотя данные были целы и API отвечал — сломался только UI. Блог «перестал писаться», хотя таймеры исправно срабатывали — падали сами сервисы с кодом 203/EXEC. В обоих случаях поверхностный сигнал говорил «всё нормально», а реальная картина была противоположной. Вывод простой: доверяй не статусу обёртки, а результату. Таймер сработал — это не значит, что задача выполнилась; страница есть в роутинге — это не значит, что функциональность доступна.

Второй обобщённый урок — про миграции. Переезд на новый сервер ломает ровно те связи, которые нигде явно не записаны: жёстко прописанные пути в systemd-юнитах, имена таблиц в коде, дефолты в localStorage, приватность Docker-образов. Чек-лист миграции должен быть направлен именно на эти невидимые зависимости, потому что они не падают в момент переезда — они падают позже, отложенно, когда уже кажется, что всё прошло гладко. Самое опасное в миграции — не то, что сломалось сразу, а то, что сломается через сутки.

Третий урок — про экономику и собственную инфраструктуру. В AI-продукте каждое архитектурное решение имеет ценник: какой провайдер, какая модель, прямой доступ или реселлер — всё это напрямую влияет на маржу. И ровно поэтому собственный LLM-роутер с бесплатными моделями оказался стратегическим активом: он позволил подключить новый инструмент генерации дизайна с нулевой себестоимостью токенов. Инвестиции в свою инфраструктуру не видны в день, когда ты их делаешь, — они окупаются каждый раз, когда новая фича садится на готовую бесплатную базу.

И последнее. Хороший инженерный процесс — это не бюрократия, а защита от собственных ошибок. Систематическая отладка не дала мне починить симптом вместо причины. Проверка моделей вживую поймала несуществующий qwen-3-235b до деплоя, а не после жалоб пользователей. Классификатор не дал зашить секрет в клиентский бандл. Каждый из этих барьеров сэкономил часы разбора последствий — и в этом, по большому счёту, и состоит разница между «оно вроде работает» и «оно работает, и я знаю почему».

Паша Вин
Паша Вин

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