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
91 lines
2.5 KiB
Go
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
|
|
}
|