feat: multi-provider support — Ollama Cloud + OpenCode Go

- Account.Provider field (ollama-cloud | opencode-go), backward compatible
- Model-based routing: common models served by combined pool, unique models
  routed to their provider only
- /go/v1/* path forces OpenCode Go provider (prefix stripped upstream)
- Merged /v1/models endpoint returns union of both catalogs (44 models)
- Failover: 429/402 → cooldown + failover; 5xx → retry without cooldown
- CLI: accounts add --provider flag, list shows provider column
- Body buffering: request body buffered (8 MiB cap) for failover replay
- opencode integration: unified provider 'oc' with all merged models
- 64 tests pass (unit + integration)
- Verified: glm-5 → ollama, mimo-v2.5 → go, gpt-oss:20b → ollama
This commit is contained in:
Atte149 2026-06-24 15:37:28 +03:00
parent 98bbc96bf5
commit 4fe15324c8
11 changed files with 973 additions and 125 deletions

View file

@ -37,16 +37,21 @@ func RunAccounts(args []string) int {
func printAccountsUsage() {
fmt.Print(`Usage:
ollama-proxy accounts add [API_KEY] [--name <alias>]
ollama-proxy accounts add [API_KEY] [--name <alias>] [--provider ollama-cloud|opencode-go]
ollama-proxy accounts list
ollama-proxy accounts remove <id|name>
ollama-proxy accounts set-base-url <url>
Providers:
ollama-cloud (default) Ollama Cloud at https://ollama.com — native + OpenAI API
opencode-go OpenCode Go at https://opencode.ai/zen/go/v1 — OpenAI API only
`)
}
// accountsAdd implements `accounts add [API_KEY] [--name <alias>]`.
// accountsAdd implements `accounts add [API_KEY] [--name <alias>] [--provider P]`.
func accountsAdd(args []string) int {
var apiKey, name string
provider := config.ProviderOllamaCloud
for i := 0; i < len(args); i++ {
switch args[i] {
case "--name":
@ -56,8 +61,15 @@ func accountsAdd(args []string) int {
}
name = args[i+1]
i++
case "--provider":
if i+1 >= len(args) {
fmt.Fprintln(os.Stderr, "--provider requires an argument")
return 2
}
provider = config.ProviderType(args[i+1])
i++
case "-h", "--help":
fmt.Println("usage: accounts add [API_KEY] [--name <alias>]")
fmt.Println("usage: accounts add [API_KEY] [--name <alias>] [--provider ollama-cloud|opencode-go]")
return 0
default:
if apiKey == "" {
@ -69,6 +81,11 @@ func accountsAdd(args []string) int {
}
}
if !config.ValidProvider(provider) {
fmt.Fprintf(os.Stderr, "unknown provider %q (use ollama-cloud or opencode-go)\n", provider)
return 2
}
if name == "" {
name = promptString("Account name (alias)", "acct"+config.NewID()[:4])
}
@ -78,7 +95,14 @@ func accountsAdd(args []string) int {
}
if apiKey == "" {
fmt.Print("Enter Ollama Cloud API key (input hidden): ")
label := "API key"
switch provider {
case config.ProviderOpenCodeGo:
label = "OpenCode Go API key"
case config.ProviderOllamaCloud:
label = "Ollama Cloud API key"
}
fmt.Printf("Enter %s (input hidden): ", label)
b, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
@ -106,17 +130,19 @@ func accountsAdd(args []string) int {
return 2
}
acct := config.Account{
ID: config.NewID(),
Name: name,
APIKey: apiKey,
Created: nowFn(),
ID: config.NewID(),
Name: name,
Provider: provider,
APIKey: apiKey,
Created: nowFn(),
}
af.Accounts = append(af.Accounts, acct)
if err := af.Save(); err != nil {
fmt.Fprintln(os.Stderr, "save accounts:", err)
return 1
}
fmt.Printf("added account %s (name=%s key=%s)\n", acct.ID[:8], acct.Name, maskKey(acct.APIKey))
fmt.Printf("added account %s (name=%s provider=%s key=%s)\n",
acct.ID[:8], acct.Name, acct.Provider, maskKey(acct.APIKey))
return 0
}
@ -133,15 +159,21 @@ func accountsList(args []string) int {
}
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
fmt.Fprintf(w, "%-10s %-16s %-18s %-22s\n", "ID", "NAME", "KEY", "CREATED")
fmt.Fprintf(w, "%-10s %-16s %-18s %-22s\n", strings.Repeat("-", 8), strings.Repeat("-", 14), strings.Repeat("-", 16), strings.Repeat("-", 20))
fmt.Fprintf(w, "%-10s %-14s %-16s %-18s %-22s\n", "ID", "PROVIDER", "NAME", "KEY", "CREATED")
fmt.Fprintf(w, "%-10s %-14s %-16s %-18s %-22s\n",
strings.Repeat("-", 8), strings.Repeat("-", 12), strings.Repeat("-", 14),
strings.Repeat("-", 16), strings.Repeat("-", 20))
for _, a := range af.Accounts {
id := a.ID
if len(id) > 8 {
id = id[:8]
}
prov := string(a.Provider)
if prov == "" {
prov = string(config.ProviderOllamaCloud)
}
created := a.Created.Format("2006-01-02 15:04 MST")
fmt.Fprintf(w, "%-10s %-16s %-18s %-22s\n", id, a.Name, maskKey(a.APIKey), created)
fmt.Fprintf(w, "%-10s %-14s %-16s %-18s %-22s\n", id, prov, a.Name, maskKey(a.APIKey), created)
}
if af.BaseURL != "" {
fmt.Fprintf(w, "\nDefault upstream: %s\n", af.BaseURL)