# Архитектура Ollama Proxy ## 1. Общее описание **Ollama Proxy** — Go-сервис, прозрачно встающий между Codex CLI и локальной Ollama. Для Codex CLI прокси выглядит как обычная Ollama (те же эндпоинты, тот же формат). Внутри прокси: 1. Проверяет авторизацию (токен из env) 2. Классифицирует запрос через Router LLM (gemma:1b) 3. Подменяет модель в запросе на подходящую 4. Проксирует запрос в реальную Ollama 5. Стримит ответ обратно в 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 → 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 → если неверный: 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` ```json { "model": "auto", "messages": [ {"role": "user", "content": "напиши функцию на Go"} ], "stream": true } ``` ### Формат ответа `/api/chat` (NDJSON, одна строка = один чанк) ```json {"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` (.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 отменяется ```go // Псевдокод 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**: тестирование сквозного потока с реальным клиентом