ollama-proxy/internal/proxy/retry.go
Atte149 4fe15324c8 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
2026-06-24 15:37:28 +03:00

63 lines
1.9 KiB
Go

package proxy
import (
"net/http"
"strings"
)
// isRetriable reports whether an upstream status code should trigger failover
// to the next account before any bytes are streamed to the client.
//
// 429 — rate limit / usage limit (retryable cooldown)
// 402 — payment required; treated as retryable when the body text matches a
//
// usage-window pattern ("usage limit", "limit reached", "exhausted",
// "daily/weekly/monthly limit"). A bare "insufficient credits" is also
// retriable (billing disable) — we still fail over, just with a longer
// cooldown.
//
// 5xx — transient server error
// 408 — request timeout
// Other 4xx are not retriable: the request is malformed or the model is
// unknown, so retrying on another account would fail identically.
func isRetriable(status int) bool {
switch {
case status == http.StatusTooManyRequests:
return true
case status == http.StatusPaymentRequired:
return true
case status >= 500:
return true
case status == http.StatusRequestTimeout:
return true
default:
return false
}
}
// shouldCooldown reports whether an account should be marked cooldown after a
// retriable failure. 5xx does not trigger cooldown (the error may be
// transient and unrelated to this account's quota). 429 and 402 always do.
func shouldCooldown(status int, bodyPreview string) bool {
switch {
case status == http.StatusTooManyRequests:
return true
case status == http.StatusPaymentRequired:
return true
default:
return false
}
}
// isBillingDisable reports whether the error body indicates a billing/credit
// exhaustion (longer cooldown) rather than a transient usage-window limit.
// Used for logging/diagnostics; the cooldown duration is the same in v1.
func isBillingDisable(bodyPreview string) bool {
low := strings.ToLower(bodyPreview)
for _, marker := range []string{"insufficient credits", "credit balance", "billing"} {
if strings.Contains(low, marker) {
return true
}
}
return false
}