forked from templates/template-go-orm
Go-сервис-прокси между Codex CLI и Ollama. Добавляет Bearer-авторизацию, LLM-маршрутизатор (deepseek классифицирует запросы: code/doc/general), поддержку OpenAI Responses API для Codex CLI, стриминг SSE, кеш модели. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
14 KiB
Архитектура Ollama Proxy
1. Общее описание
Ollama Proxy — Go-сервис, прозрачно встающий между Codex CLI и локальной Ollama. Для Codex CLI прокси выглядит как обычная Ollama (те же эндпоинты, тот же формат). Внутри прокси:
- Проверяет авторизацию (токен из env)
- Классифицирует запрос через Router LLM (gemma:1b)
- Подменяет модель в запросе на подходящую
- Проксирует запрос в реальную Ollama
- Стримит ответ обратно в Codex CLI
Сервис stateless — нет БД, нет сессий, нет хранения истории. История сообщений хранится на стороне Codex CLI.
2. Архитектурная схема
┌─────────────────────────────────────────────────────────────────────┐
│ CODEX CLI │
│ (отправляет запросы как к обычной Ollama) │
└────────────────────────────┬────────────────────────────────────────┘
│ HTTP model: "auto" или "qwen2.5-coder:1.5b"
▼
┌─────────────────────────────────────────────────────────────────────┐
│ OLLAMA PROXY (:11435) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Token Auth Middleware │ │
│ │ Authorization: Bearer <AUTH_TOKEN> → 401 если неверный │ │
│ └───────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼─────────────────────────────────────┐ │
│ │ Router LLM │ │
│ │ │ │
│ │ Если model == "auto" или пустой: │ │
│ │ → gemma:1b (синхронный вызов, stream=false) │ │
│ │ → промпт: "Classify: code / document / general" │ │
│ │ → ответ: одно слово │ │
│ │ │ │
│ │ Если model указана явно → пропустить, использовать как есть │ │
│ └────────────────────────┬─────────────────────────────────────┘ │
│ │ model подменён на целевую │
│ ┌────────────────────────▼─────────────────────────────────────┐ │
│ │ Proxy │ │
│ │ POST /api/chat → Ollama /api/chat │ │
│ │ Стриминг NDJSON построчно (bufio.Scanner + http.Flusher) │ │
│ └────────────────────────┬─────────────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────────────┘
│ HTTP model: "qwen2.5-coder:1.5b"
▼
┌─────────────────────────────────────────────────────────────────────┐
│ OLLAMA (:11434) │
│ │
│ gemma:1b qwen2.5-coder:1.5b (другие модели) │
│ (router + docs) (код) │
└─────────────────────────────────────────────────────────────────────┘
3. Структура проекта
.
├── cmd/server/main.go # Точка входа: загрузка конфига, запуск HTTP-сервера
├── internal/
│ ├── config/
│ │ └── config.go # Парсинг env-переменных (caarlos0/env)
│ ├── model/
│ │ └── ollama.go # Go-типы Ollama API: ChatRequest, ChatResponse,
│ │ # GenerateRequest, GenerateResponse, TagsResponse, Message
│ ├── handler/
│ │ ├── middleware.go # Проверка Bearer-токена (простое сравнение строк)
│ │ └── proxy.go # HTTP-хендлеры: HandleChat, HandleGenerate, HandleTags
│ ├── service/
│ │ ├── ollama_client.go # HTTP-клиент к Ollama:
│ │ │ # ProxyChat — стриминг /api/chat
│ │ │ # ProxyGenerate — стриминг /api/generate
│ │ │ # GetTags — список моделей
│ │ │ # Complete — синхронный вызов (для роутера)
│ │ └── router.go # LLM-маршрутизатор:
│ │ # Route(ctx, model, messages) → целевая модель
├── router/
│ └── router.go # chi-роутер, подключение middleware и хендлеров
├── go.mod / go.sum # Зависимости
├── Makefile # build / run / test
├── .env # Локальные переменные (не в git)
├── .gitignore
├── .gitattributes # LF line endings
├── VERSION # Семантическая версия
└── docs/
└── architecture.md # Этот файл
4. Поток обработки запроса
Случай 1: model = "auto" (первый запрос в сессии)
1. Codex CLI → POST /api/chat {"model":"auto", "messages":[...]}
2. Middleware: проверить Authorization: Bearer <token>
→ если неверный: 401 Unauthorized
3. Handler: декодировать ChatRequest из JSON
4. Router.Route(ctx, "auto", messages):
a. Взять последнее user-сообщение
b. POST /api/chat к Ollama с model=gemma:1b, stream=false
Промпт: "Classify the following user request into exactly one category:
code, document, general.
Reply with ONLY the category name, nothing else.
User request: {текст}"
c. Получить ответ: "code"
d. Вернуть CODE_MODEL = "qwen2.5-coder:1.5b"
5. Подменить req.Model = "qwen2.5-coder:1.5b"
6. OllamaClient.ProxyChat(ctx, w, req):
a. POST /api/chat к Ollama с model=qwen2.5-coder:1.5b
b. Читать ответ построчно (bufio.Scanner)
c. Каждую строку NDJSON сразу писать в w + Flush()
7. Codex CLI получает стриминг, видит model="qwen2.5-coder:1.5b" в ответе
→ запоминает модель для последующих запросов
Случай 2: model = "qwen2.5-coder:1.5b" (последующие запросы)
1. Codex CLI → POST /api/chat {"model":"qwen2.5-coder:1.5b", "messages":[...]}
2. Middleware: проверить токен
3. Router.Route(ctx, "qwen2.5-coder:1.5b", messages):
→ модель указана явно, не "auto" → вернуть как есть
4. OllamaClient.ProxyChat(ctx, w, req):
→ прямой прокси к Ollama, Router LLM не вызывается
5. Ollama API — поддерживаемые эндпоинты
| Метод | URL | Описание |
|---|---|---|
| GET | /health |
Проверка работоспособности (без авторизации) |
| POST | /api/chat |
Чат. Streaming NDJSON |
| POST | /api/generate |
Генерация текста. Streaming NDJSON |
| GET | /api/tags |
Список моделей из реальной Ollama |
Формат запроса /api/chat
{
"model": "auto",
"messages": [
{"role": "user", "content": "напиши функцию на Go"}
],
"stream": true
}
Формат ответа /api/chat (NDJSON, одна строка = один чанк)
{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:00Z","message":{"role":"assistant","content":"func"},"done":false}
{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:01Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":1234567890,"eval_count":42}
6. Авторизация
Простая проверка Bearer-токена:
Authorization: Bearer <AUTH_TOKEN>
- Токен задаётся в
AUTH_TOKEN(.env) - Сравнение строк — никакого JWT, bcrypt, БД
- Неверный или отсутствующий токен →
401 Unauthorized /health— без авторизации
Это временная заглушка. В следующем этапе будет полноценная авторизация с JWT и БД.
7. Router LLM — детали реализации
Промпт классификатора:
Classify the following user request into exactly one category: code, document, general.
Reply with ONLY the category name, nothing else.
User request: {последнее user-сообщение}
Вызов: синхронный (stream: false), короткий timeout (5–10 сек).
Парсинг ответа:
- Взять ответ, привести к нижнему регистру, убрать пробелы
- Если содержит "code" → CODE_MODEL
- Если содержит "document" → DOC_MODEL
- Иначе → GENERAL_MODEL (fallback)
Маппинг категорий → модели:
| Категория | Тестовая модель | Production модель |
|---|---|---|
| code | qwen2.5-coder:1.5b | qwen2.5-coder:32b |
| document | gemma:1b | deepseek-r1:70b |
| general | gemma:1b | llama3.3:70b |
8. Streaming-прокси — детали реализации
Ключевые требования:
http.Flusher— сбрасывать буфер после каждой строкиbufio.Scannerс буфером 1MB (для длинных строк с кодом)http.Clientбез глобального таймаута (используется контекст запроса)- Отмена через
r.Context()— если Codex CLI отключился, upstream-запрос к Ollama отменяется
// Псевдокод
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
flusher := w.(http.Flusher)
for scanner.Scan() {
w.Write(scanner.Bytes())
w.Write([]byte("\n"))
flusher.Flush()
}
9. Переменные окружения
| Переменная | Описание | По умолчанию |
|---|---|---|
PROXY_PORT |
Порт прокси | 11435 |
AUTH_TOKEN |
Токен авторизации | — (обязательна) |
OLLAMA_URL |
URL реальной Ollama | http://localhost:11434 |
ROUTER_MODEL |
Модель-классификатор | gemma:1b |
CODE_MODEL |
Модель для кода | qwen2.5-coder:1.5b |
DOC_MODEL |
Модель для документов | gemma:1b |
GENERAL_MODEL |
Общая модель | gemma:1b |
10. Технологический стек
| Компонент | Технология |
|---|---|
| Язык | Go |
| HTTP-фреймворк | chi/v5 |
| Конфигурация | caarlos0/env/v11 |
| Streaming | bufio.Scanner + http.Flusher |
| LLM-инфраструктура | Ollama (локально) |
11. Следующие этапы (после MVP)
- Авторизация: полноценный JWT + PostgreSQL + регистрация/логин
- История чатов: хранение сообщений в БД (User → Chat → Message)
- Production-модели: qwen2.5-coder:32b, deepseek-r1:70b
- Codex CLI: тестирование сквозного потока с реальным клиентом