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

278 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура 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`
```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>
```
- Токен задаётся в `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 отменяется
```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**: тестирование сквозного потока с реальным клиентом