- 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
63 lines
1.9 KiB
Go
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
|
|
}
|