ollama-proxy/internal/config/server.go
Atte149 3963eede70 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
2026-06-19 13:58:13 +03:00

91 lines
2.5 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strconv"
"time"
)
// ServerConfig holds the runtime options for the proxy server. Fields map to
// CLI flags and (for LogLevel and BaseURL) the OLLAMA_PROXY_* environment
// variables, so a systemd unit can set defaults without changing the binary.
type ServerConfig struct {
Addr string // listen address, e.g. "127.0.0.1:11435"
BaseURL string // upstream Ollama Cloud root, e.g. "https://ollama.com"
Cooldown time.Duration // per-account 429 cooldown window
Retries int // max failover attempts (= number of accounts)
LogLevel string // debug | info | warn | error
}
// DefaultServerConfig returns the canonical defaults used when a flag or env
// var is not set.
func DefaultServerConfig() ServerConfig {
return ServerConfig{
Addr: "127.0.0.1:11435",
BaseURL: "https://ollama.com",
Cooldown: 60 * time.Second,
Retries: 3,
LogLevel: "info",
}
}
// ApplyEnv overlays OLLAMA_PROXY_* environment variables on top of the current
// config. Empty / unparsable env vars are ignored (the existing value wins).
func (c *ServerConfig) ApplyEnv() {
if v := os.Getenv("OLLAMA_PROXY_ADDR"); v != "" {
c.Addr = v
}
if v := os.Getenv("OLLAMA_PROXY_BASE_URL"); v != "" {
c.BaseURL = v
}
if v := os.Getenv("OLLAMA_PROXY_COOLDOWN"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
c.Cooldown = d
}
}
if v := os.Getenv("OLLAMA_PROXY_RETRIES"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
c.Retries = n
}
}
if v := os.Getenv("OLLAMA_PROXY_LOG_LEVEL"); v != "" {
switch v {
case "debug", "info", "warn", "error":
c.LogLevel = v
}
}
}
// Validate returns an error when a field has an obviously invalid value.
func (c *ServerConfig) Validate() error {
if c.Addr == "" {
return errors.New("addr is required")
}
if c.BaseURL == "" {
return errors.New("base_url is required")
}
if !startsWithScheme(c.BaseURL, "http://") && !startsWithScheme(c.BaseURL, "https://") {
return fmt.Errorf("base_url must start with http:// or https://, got %q", c.BaseURL)
}
if c.Cooldown < 0 {
return errors.New("cooldown must be >= 0")
}
if c.Retries < 1 {
return errors.New("retries must be >= 1")
}
switch c.LogLevel {
case "debug", "info", "warn", "error":
default:
return fmt.Errorf("log_level must be one of debug|info|warn|error, got %q", c.LogLevel)
}
return nil
}
func startsWithScheme(s, scheme string) bool {
if len(s) < len(scheme) {
return false
}
return s[:len(scheme)] == scheme
}