AI-стартер (OpenAI-совместимый стриминг)

Стартер включает готовый потоковый чат к любому OpenAI-совместимому эндпоинту: сам OpenAI, локальный сервер (vLLM, Ollama в режиме OpenAI), OpenRouter, Together или российский прокси — всё настраивается переменными окружения, код провайдера менять не нужно.

Из чего состоит

ФайлНазначение
src/lib/ai/openai.tsOpenAI-совместимый клиент: сборка запроса, разбор SSE, streamChat (async-генератор токенов). Транспорт — нативный fetch, fetchImpl инъектируется (мок в тестах).
src/app/api/ai/chat/route.tsPOST-роут, отдаёт text/event-stream (SSE через ReadableStream).
src/components/ai/chat.tsxКлиентский UI на DaisyUI, читает поток и дописывает ответ по токенам.
src/app/[locale]/ai/page.tsxДемо-страница /ai/en/ai).

Переменные окружения

AI_API_KEY=""                            # без ключа роут отвечает 503, демо показывает подсказку
AI_BASE_URL="https://api.openai.com/v1"  # любой OpenAI-совместимый базовый URL
AI_MODEL="gpt-4o-mini"                   # id модели у выбранного провайдера

Значения проходят валидацию в src/lib/env.ts (Zod). Ключ опционален, чтобы pnpm build, тесты и launchkit init работали без него: без AI_API_KEY функция resolveAiConfig бросает понятную ошибку, а роут возвращает 503.

Формат потока (SSE)

Роут /api/ai/chat принимает { messages: [{ role, content }] } и отдаёт события:

data: {"token":"..."}   # инкремент текста
data: {"error":"..."}   # ошибка в середине потока
data: [DONE]            # конец потока

Ручная проверка с реальным ключом

Тесты замоканы (никакой сети и ключа). Чтобы проверить вживую:

echo 'AI_API_KEY="sk-..."' >> .env      # ключ вашего провайдера
# при необходимости: AI_BASE_URL="https://ваш-эндпоинт/v1", AI_MODEL="..."
pnpm dev
# откройте http://localhost:3000/ai и отправьте сообщение — ответ придёт по токенам

Проверить роут напрямую:

curl -N -X POST http://localhost:3000/api/ai/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"Привет!"}]}'

Тесты

  • src/lib/ai/openai.test.ts — сборка запроса, разбор SSE, стриминг на мок-fetch (сборка ответа из потока, разрыв события между чанками, ошибки статуса/пустого тела).
  • src/app/api/ai/chat/route.test.ts — роут на замоканном streamChat: SSE-ответ собирается в полный ответ, [DONE] в конце, 400 на некорректном теле, 503 без ключа, кадр ошибки при сбое.

Дальше