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:
parent
98bbc96bf5
commit
4fe15324c8
11 changed files with 973 additions and 125 deletions
116
internal/proxy/model_router.go
Normal file
116
internal/proxy/model_router.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/Atte149/ollama-proxy/internal/config"
|
||||
)
|
||||
|
||||
// ollamaCloudModels is the set of model IDs served by Ollama Cloud.
|
||||
// These overlap partly with the OpenCode Go catalog.
|
||||
var ollamaCloudModels = []string{
|
||||
"deepseek-v3.1:671b", "deepseek-v3.2", "deepseek-v4-flash", "deepseek-v4-pro",
|
||||
"devstral-2:123b", "devstral-small-2:24b",
|
||||
"gemini-3-flash-preview", "gemma3:12b", "gemma3:27b", "gemma3:4b", "gemma4:31b",
|
||||
"glm-4.7", "glm-5", "glm-5.1", "glm-5.2",
|
||||
"gpt-oss:120b", "gpt-oss:20b",
|
||||
"kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code",
|
||||
"minimax-m2.1", "minimax-m2.5", "minimax-m2.7", "minimax-m3",
|
||||
"ministral-3:14b", "ministral-3:3b", "ministral-3:8b",
|
||||
"mistral-large-3:675b",
|
||||
"nemotron-3-nano:30b", "nemotron-3-super", "nemotron-3-ultra",
|
||||
"qwen3-coder:480b", "qwen3-coder-next", "qwen3.5:397b",
|
||||
"rnj-1:8b",
|
||||
}
|
||||
|
||||
// openCodeGoModels is the set of model IDs served by OpenCode Go.
|
||||
var openCodeGoModels = []string{
|
||||
"deepseek-v4-flash", "deepseek-v4-pro",
|
||||
"glm-5", "glm-5.1", "glm-5.2",
|
||||
"hy3-preview",
|
||||
"kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code",
|
||||
"mimo-v2-omni", "mimo-v2-pro", "mimo-v2.5", "mimo-v2.5-pro",
|
||||
"minimax-m2.5", "minimax-m2.7", "minimax-m3",
|
||||
"qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus",
|
||||
}
|
||||
|
||||
// modelProviders maps a model ID to the providers that can serve it.
|
||||
// Built once from the two catalogs above.
|
||||
var modelProviders = func() map[string][]config.ProviderType {
|
||||
m := make(map[string][]config.ProviderType, len(ollamaCloudModels)+len(openCodeGoModels))
|
||||
add := func(id string, p config.ProviderType) {
|
||||
m[id] = append(m[id], p)
|
||||
}
|
||||
for _, id := range ollamaCloudModels {
|
||||
add(id, config.ProviderOllamaCloud)
|
||||
}
|
||||
for _, id := range openCodeGoModels {
|
||||
// Deduplicate: if already present (common model), append; else create.
|
||||
found := false
|
||||
for _, ex := range m[id] {
|
||||
if ex == config.ProviderOpenCodeGo {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
add(id, config.ProviderOpenCodeGo)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// ProvidersForModel returns the providers eligible to serve the given model.
|
||||
// Unknown models default to Ollama Cloud (the original behaviour).
|
||||
func ProvidersForModel(model string) []config.ProviderType {
|
||||
if ps, ok := modelProviders[model]; ok {
|
||||
return ps
|
||||
}
|
||||
return []config.ProviderType{config.ProviderOllamaCloud}
|
||||
}
|
||||
|
||||
// AllModels returns the deduplicated, sorted list of all model IDs known to the
|
||||
// router (union of both catalogs).
|
||||
func AllModels() []string {
|
||||
seen := make(map[string]struct{})
|
||||
for id := range modelProviders {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for id := range seen {
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// IsCommonModel reports whether the model is served by both providers.
|
||||
func IsCommonModel(model string) bool {
|
||||
ps := ProvidersForModel(model)
|
||||
if len(ps) < 2 {
|
||||
return false
|
||||
}
|
||||
hasOllama, hasGo := false, false
|
||||
for _, p := range ps {
|
||||
switch p {
|
||||
case config.ProviderOllamaCloud:
|
||||
hasOllama = true
|
||||
case config.ProviderOpenCodeGo:
|
||||
hasGo = true
|
||||
}
|
||||
}
|
||||
return hasOllama && hasGo
|
||||
}
|
||||
|
||||
// ProviderLabel returns a short human label for a provider, used to annotate
|
||||
// unique models in the merged /v1/models response and in opencode config names.
|
||||
func ProviderLabel(p config.ProviderType) string {
|
||||
switch p {
|
||||
case config.ProviderOpenCodeGo:
|
||||
return "go"
|
||||
case config.ProviderOllamaCloud:
|
||||
return "ollama"
|
||||
default:
|
||||
return string(p)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue