Files
Local_Perplexity/docs/architecture.md
fedos 8e74e53b3d feat: add Ollama proxy with LLM router and Codex CLI support
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>
2026-03-07 15:25:15 +03:00

14 KiB
Raw Blame History

Архитектура 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 <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 (510 сек).

Парсинг ответа:

  • Взять ответ, привести к нижнему регистру, убрать пробелы
  • Если содержит "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: тестирование сквозного потока с реальным клиентом