feat: ollama-proxy — multi-account Ollama Cloud reverse proxy

Go reverse proxy for ollama.com that balances requests across multiple
API keys with round-robin and failover on 429/5xx. Exposes both native
Ollama API (/api/*) and OpenAI-compatible (/v1/*) passthrough.

- Round-robin balancer with per-account cooldown (60s default)
- Pre-stream failover: 429 → cooldown + next account; 5xx → next account
- Streaming invariant: once 2xx starts streaming, no account switch
- SSE (text/event-stream) and NDJSON passthrough with http.Flusher
- CLI: accounts add/list/remove/set-base-url, serve, version
- Accounts stored in ~/.config/ollama-proxy/accounts.json (chmod 0600)
- systemd unit (User=dueattendant149, 127.0.0.1:11435, Restart=always)
- 43 tests (unit + integration with httptest upstream)
- opencode integration: custom provider 'ocp' with explicit model list
- Requires NO_PROXY=127.0.0.1,localhost when HTTP_PROXY is set
This commit is contained in:
Atte149 2026-06-19 13:58:13 +03:00
commit 3963eede70
23 changed files with 2534 additions and 0 deletions

91
AGENTS.md Normal file
View file

@ -0,0 +1,91 @@
# AGENTS.md — ollama-proxy
## Контекст
`ollama-proxy` — Go-прокси для Ollama Cloud (`https://ollama.com`). Балансирует запросы по нескольким API-ключам с round-robin и failover на 429/5xx. Слушает `127.0.0.1:11435`, экспонирует нативный Ollama API (`/api/*`) и OpenAI-совместимый (`/v1/*`) passthrough. Деплой — systemd unit `ollama-proxy.service`. Используется opencode через кастомный провайдер `ocp`.
## Стек
- Go 1.22 (используется `log/slog`, `net/http`, `net/http/httputil`)
- Единственная внешняя зависимость: `golang.org/x/term` (prompt без echo в `accounts add`)
- Go 1.18 из apt НЕ подходит — нужен `~/.local/go/bin/go` (Go 1.22.10). Перед сборкой: `export PATH="$HOME/.local/go/bin:$PATH"`
- Сборка: `make build``./ollama-proxy`
- Тесты: `go test ./...` (43 теста, включая integration-тесты с `httptest`)
- Линт: `go vet ./...`, `gofmt -l .`
## Структура
```
main.go — точка входа, делегирует в cli.Run
internal/config/
accounts.go — Account, AccountsFile, JSON load/save (chmod 0600, atomic)
server.go — ServerConfig (addr, base_url, cooldown, retries, log_level)
internal/log/logger.go — slog JSON logger с configurable level
internal/proxy/
balancer.go — round-robin + per-account cooldown (RWMutex)
handler.go — HTTP handler: reverse-proxy, Authorization подмена, SSE flush
retry.go — документация стриминг-инварианта
internal/cli/
root.go — dispatch подкоманд (serve / accounts / version)
accounts.go — add / list / remove / set-base-url
serve.go — запуск HTTP-сервера с signal handling
systemd/ollama-proxy.service — systemd unit
```
## Конфигурация
- Аккаунты: `~/.config/ollama-proxy/accounts.json` (XDG_CONFIG_HOME, chmod 0600, atomic write)
- Флаги `serve`: `--addr`, `--base-url`, `--cooldown`, `--retries`, `--log-level`
- Env vars: `OLLAMA_PROXY_ADDR`, `OLLAMA_PROXY_BASE_URL`, `OLLAMA_PROXY_COOLDOWN`, `OLLAMA_PROXY_RETRIES`, `OLLAMA_PROXY_LOG_LEVEL`
## Типичные задачи
- **Добавить аккаунт:** `ollama-proxy accounts add --name <alias>` (промпт) или `... add <key> --name <alias>`
- **Перезапустить после правки аккаунтов:** `sudo systemctl restart ollama-proxy` (прокси читает accounts.json при старте; runtime-обновления не поддерживаются)
- **Посмотреть логи:** `journalctl -u ollama-proxy -f` — JSON-структурированные (account, account_id, path, status, latency_ms, stream)
- **Проверить, что прокси жив:** `curl http://127.0.0.1:11435/api/version` → должен вернуть JSON от ollama.com
- **Проверить список моделей:** `curl http://127.0.0.1:11435/v1/models | jq '.data | length'` (~36 моделей)
## Интеграция с opencode
В `~/.config/opencode/opencode.json` в блоке `provider`:
```json
"ocp": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama Cloud (pool)",
"options": {
"baseURL": "http://127.0.0.1:11435/v1",
"apiKey": "proxy"
},
"models": {
"gpt-oss:20b": { "name": "gpt-oss:20b" },
"...": { "name": "..." }
}
}
```
Модель выбирается через `/models` в TUI. Пул не сделан дефолтной моделью.
**ВАЖНО: NO_PROXY.** Если в окружении установлен `HTTP_PROXY`/`HTTPS_PROXY` (например `http://127.0.0.1:2080`), opencode (Bun runtime) направит запросы к прокси через HTTP-прокси → 502 Bad Gateway. Нужно явно исключить localhost:
```sh
# В ~/.config/environment.d/no-proxy-local.conf (systemd/desktop sessions):
NO_PROXY=127.0.0.1,localhost,::1
no_proxy=127.0.0.1,localhost,::1
```
Или запускать opencode с `NO_PROXY=127.0.0.1,localhost opencode ...`.
**Модели в конфиге:** opencode НЕ умеет динамически резолвить модели через `/v1/models` для `@ai-sdk/openai-compatible``models: {}` даёт `ProviderModelNotFoundError`. Нужно явно перечислить модели. Обновить список: `curl -sS http://127.0.0.1:11435/v1/models | jq -r '.data[].id'` → вставить в `models` блок.
## WARN
- **Никогда** не запускай прокси на `0.0.0.0` без auth — он не имеет аутентификации, любой может использовать твои ключи
- `accounts.json` содержит ключи в plaintext — файл chmod 0600, не коммить в git
- После правки `accounts.json` нужен `sudo systemctl restart ollama-proxy` (прокси не перечитывает файл в рантайме)
- Если локальная `ollama` запущена на 11434 — это НЕ конфликтует (прокси на 11435), но `OLLAMA_HOST=127.0.0.1:11435` нужно явно указывать
- Стриминг-инвариант: после первого flush-а 2xx ответа переключение аккаунта НЕ происходит. Mid-stream ошибки → truncated response клиенту. Это by design (нельзя дублировать вывод)
- 5xx НЕ вызывает cooldown аккаунта (только 429). 5xx → failover на следующий аккаунт, но без penalty
## План отката
- `sudo systemctl stop ollama-proxy && sudo systemctl disable ollama-proxy`
- `sudo rm /usr/local/bin/ollama-proxy /etc/systemd/system/ollama-proxy.service && sudo systemctl daemon-reload`
- Убрать блок `ocp` из `~/.config/opencode/opencode.json`