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
|
|
@ -27,9 +27,14 @@ type Balancer struct {
|
|||
// is applied uniformly to all accounts. At least one account is required;
|
||||
// otherwise Next returns ErrNoAccounts on every call.
|
||||
func NewBalancer(accounts []config.Account, cooldown time.Duration) *Balancer {
|
||||
// copy to avoid external mutation
|
||||
// copy to avoid external mutation; normalise empty provider to ollama-cloud.
|
||||
accts := make([]config.Account, len(accounts))
|
||||
copy(accts, accounts)
|
||||
for i, a := range accounts {
|
||||
if a.Provider == "" {
|
||||
a.Provider = config.ProviderOllamaCloud
|
||||
}
|
||||
accts[i] = a
|
||||
}
|
||||
return &Balancer{
|
||||
accounts: accts,
|
||||
cooldown: make(map[string]time.Time),
|
||||
|
|
@ -70,21 +75,41 @@ func (e ErrAllCooldown) Error() string {
|
|||
// rotates starting from the round-robin counter so consecutive calls land on
|
||||
// different accounts when possible. Returns ErrNoAccounts or ErrAllCooldown
|
||||
// when nothing is available.
|
||||
//
|
||||
// Next considers all accounts regardless of provider. Use NextFor to restrict
|
||||
// to a set of eligible providers (e.g. for model-based routing).
|
||||
func (b *Balancer) Next() (config.Account, error) {
|
||||
return b.NextFor(nil)
|
||||
}
|
||||
|
||||
// NextFor picks the next available account whose provider is in the eligible
|
||||
// list. When eligible is nil or empty, all providers are eligible (matches
|
||||
// the original Next behaviour). Accounts in cooldown are skipped.
|
||||
func (b *Balancer) NextFor(eligible []config.ProviderType) (config.Account, error) {
|
||||
if len(b.accounts) == 0 {
|
||||
return config.Account{}, ErrNoAccounts{}
|
||||
}
|
||||
allowed := make(map[config.ProviderType]bool, len(eligible))
|
||||
for _, p := range eligible {
|
||||
allowed[p] = true
|
||||
}
|
||||
allAllowed := len(eligible) == 0
|
||||
|
||||
now := time.Now()
|
||||
var earliest time.Time
|
||||
for i := 0; i < len(b.accounts); i++ {
|
||||
idx := int(b.rr.Add(1)) % len(b.accounts)
|
||||
acct := b.accounts[idx]
|
||||
// idx is computed from the *new* counter value; we Add-then-mod so each
|
||||
// call advances even if this loop iteration rejects the candidate.
|
||||
if !b.isCooldown(b.accounts[idx].ID, now) {
|
||||
return b.accounts[idx], nil
|
||||
if !allAllowed && !allowed[acct.Provider] {
|
||||
continue
|
||||
}
|
||||
if earliest.IsZero() || b.cooldownUntil(b.accounts[idx].ID).Before(earliest) {
|
||||
earliest = b.cooldownUntil(b.accounts[idx].ID)
|
||||
if !b.isCooldown(acct.ID, now) {
|
||||
return acct, nil
|
||||
}
|
||||
if earliest.IsZero() || b.cooldownUntil(acct.ID).Before(earliest) {
|
||||
earliest = b.cooldownUntil(acct.ID)
|
||||
}
|
||||
}
|
||||
return config.Account{}, ErrAllCooldown{Until: earliest}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue