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

@ -14,13 +14,44 @@ import (
"time"
)
// Account is a single Ollama Cloud API key with an optional alias.
// ProviderType identifies which upstream service an account belongs to.
type ProviderType string
const (
// ProviderOllamaCloud is the Ollama Cloud API at https://ollama.com.
ProviderOllamaCloud ProviderType = "ollama-cloud"
// ProviderOpenCodeGo is the OpenCode Go API at https://opencode.ai/zen/go/v1.
ProviderOpenCodeGo ProviderType = "opencode-go"
)
// DefaultProviderBaseURL returns the canonical upstream root for a provider.
func DefaultProviderBaseURL(p ProviderType) string {
switch p {
case ProviderOpenCodeGo:
return "https://opencode.ai/zen/go"
default:
return "https://ollama.com"
}
}
// ValidProvider reports whether p is a recognised provider type.
func ValidProvider(p ProviderType) bool {
switch p {
case ProviderOllamaCloud, ProviderOpenCodeGo:
return true
default:
return false
}
}
// Account is a single upstream API key with an optional alias.
type Account struct {
ID string `json:"id"`
Name string `json:"name"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url,omitempty"` // overrides ServerConfig.BaseURL; empty = use default
Created time.Time `json:"created"`
ID string `json:"id"`
Name string `json:"name"`
Provider ProviderType `json:"provider,omitempty"` // empty = ollama-cloud (backward compat)
APIKey string `json:"api_key"`
BaseURL string `json:"base_url,omitempty"` // overrides the provider default; empty = use default
Created time.Time `json:"created"`
}
// AccountsFile is the on-disk JSON structure.
@ -73,6 +104,13 @@ func LoadAccounts() (*AccountsFile, error) {
if err := json.Unmarshal(data, &af); err != nil {
return nil, fmt.Errorf("parse accounts: %w", err)
}
// Backward compat: accounts without an explicit provider default to
// ollama-cloud (the original behaviour before multi-provider support).
for i := range af.Accounts {
if af.Accounts[i].Provider == "" {
af.Accounts[i].Provider = ProviderOllamaCloud
}
}
return &af, nil
}