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:
commit
3963eede70
23 changed files with 2534 additions and 0 deletions
91
internal/config/server.go
Normal file
91
internal/config/server.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue