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

@ -77,6 +77,60 @@ func TestValidateName(t *testing.T) {
}
}
func TestProviderType_Constants(t *testing.T) {
if ProviderOllamaCloud != "ollama-cloud" {
t.Errorf("ProviderOllamaCloud = %q", ProviderOllamaCloud)
}
if ProviderOpenCodeGo != "opencode-go" {
t.Errorf("ProviderOpenCodeGo = %q", ProviderOpenCodeGo)
}
if !ValidProvider(ProviderOllamaCloud) || !ValidProvider(ProviderOpenCodeGo) {
t.Error("ValidProvider returned false for known providers")
}
if ValidProvider(ProviderType("bogus")) {
t.Error("ValidProvider returned true for bogus provider")
}
}
func TestDefaultProviderBaseURL(t *testing.T) {
if DefaultProviderBaseURL(ProviderOllamaCloud) != "https://ollama.com" {
t.Errorf("ollama base url = %q", DefaultProviderBaseURL(ProviderOllamaCloud))
}
if DefaultProviderBaseURL(ProviderOpenCodeGo) != "https://opencode.ai/zen/go" {
t.Errorf("go base url = %q", DefaultProviderBaseURL(ProviderOpenCodeGo))
}
}
// TestLoadAccounts_BackwardCompatProvider verifies that accounts.json entries
// without a "provider" field are normalised to ollama-cloud on load.
func TestLoadAccounts_BackwardCompatProvider(t *testing.T) {
p := withTempAccountsPath(t)
// Write an old-style file with no provider field.
old := `{
"base_url": "https://ollama.com",
"accounts": [
{"id":"abc","name":"legacy","api_key":"sk-xyz","created":"2026-06-19T10:00:00Z"},
{"id":"def","name":"go1","provider":"opencode-go","api_key":"sk-go","created":"2026-06-19T10:00:00Z"}
]
}`
if err := os.WriteFile(p, []byte(old), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
af, err := LoadAccounts()
if err != nil {
t.Fatalf("load: %v", err)
}
if len(af.Accounts) != 2 {
t.Fatalf("len = %d", len(af.Accounts))
}
if af.Accounts[0].Name != "legacy" || af.Accounts[0].Provider != ProviderOllamaCloud {
t.Errorf("legacy account provider = %q, want ollama-cloud", af.Accounts[0].Provider)
}
if af.Accounts[1].Provider != ProviderOpenCodeGo {
t.Errorf("go account provider = %q, want opencode-go", af.Accounts[1].Provider)
}
}
func TestLoadAccounts_MissingFile(t *testing.T) {
withTempAccountsPath(t)
af, err := LoadAccounts()