mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Real Ollama Cloud 429s are driven by ollama.com usage windows, not by response headers that OpenAI/Anthropic/CN rate-limit parsing understands. Handle them separately: apply a never-shortening temporary cooldown first, then schedule an async probe that learns the true usage-window reset point with a controlled, coalesced per-api_key refresh. - ratelimit_service: route real Ollama Cloud usage accounts to a dedicated 429 path before the CN-provider branch. - ollama_cloud_usage: add a background probe coordinator (started in Start, waited by Stop) that fans out model-429 probes to one controlled refresh per api_key group, reusing the recent group snapshot for coalesced members. - repository/account_repo: add SetRateLimitedIfUnchanged, a write-back CAS counterpart to ClearRateLimitIfObserved, so an async reset never overwrites a newer 429, an admin clear, a re-armed generation, or a key/state change observed by another writer between read and write. - wire: inject OllamaCloudUsageService into RateLimitService. Tests: unit tests for service and repository pass; targeted race tests pass; build and vet clean; git diff --check clean. The CAS integration test compiles but is not run (no Docker available).
66 lines
2.7 KiB
Go
66 lines
2.7 KiB
Go
package service
|
|
|
|
import "time"
|
|
|
|
// ollamaCloudUsageExhaustionResetAt returns the wall-clock time at which an
|
|
// exhausted Ollama Cloud usage window is expected to roll over, i.e. the
|
|
// earliest moment a quota-driven upstream block (a 429 or equivalent) can be
|
|
// expected to clear on its own. It is a pure helper and performs no I/O and no
|
|
// routing; it does not itself trigger anything.
|
|
//
|
|
// Snapshot usability (conservative): the observation is trusted only when it is
|
|
// successful (Status == OK) and fresh. Freshness is expressed as a caller-chosen
|
|
// validity bound minFetchedAt: the snapshot must carry a FetchedAt that is
|
|
// present and not older than minFetchedAt. Passing now.Add(-freshness) keeps
|
|
// old snapshots out of the decision without reading any configuration here.
|
|
//
|
|
// Window handling:
|
|
// - Only windows with UsedPercent >= 100 count as exhausted.
|
|
// - A window that is not exhausted (or absent from Data) never affects the
|
|
// result and is ignored even when its ResetAt is missing or in the past.
|
|
// - The reset of every exhausted window must be known (non-nil, non-zero) and
|
|
// strictly in the future relative to now. If any exhausted window cannot
|
|
// produce such a reset, no complete recovery time can be claimed and ok is
|
|
// false — the caller must not assume a known recovery horizon.
|
|
// - When more than one window is exhausted, the later of their resets is
|
|
// returned: the account cannot recover until the last exhausted window rolls.
|
|
//
|
|
// ok is false (zero time) when there is nothing exhausted or the recovery time
|
|
// cannot be determined completely.
|
|
func ollamaCloudUsageExhaustionResetAt(
|
|
snapshot *OllamaCloudUsageSnapshot,
|
|
now time.Time,
|
|
minFetchedAt time.Time,
|
|
) (reset time.Time, ok bool) {
|
|
if snapshot == nil || snapshot.Status != OllamaCloudUsageStatusOK || snapshot.Data == nil {
|
|
return time.Time{}, false
|
|
}
|
|
if snapshot.FetchedAt == nil || snapshot.FetchedAt.IsZero() || snapshot.FetchedAt.Before(minFetchedAt) {
|
|
return time.Time{}, false
|
|
}
|
|
|
|
var latest time.Time
|
|
for _, window := range []*OllamaCloudUsageWindow{
|
|
snapshot.Data.FiveHour,
|
|
snapshot.Data.SevenDay,
|
|
} {
|
|
if window == nil || window.UsedPercent < 100 {
|
|
// Not exhausted: must not influence the recovery decision.
|
|
continue
|
|
}
|
|
if window.ResetAt == nil || window.ResetAt.IsZero() || !window.ResetAt.After(now) {
|
|
// Exhausted but without a known future reset: cannot claim a complete
|
|
// recovery time, so give up on the whole snapshot.
|
|
return time.Time{}, false
|
|
}
|
|
candidate := window.ResetAt.UTC()
|
|
if latest.IsZero() || candidate.After(latest) {
|
|
latest = candidate
|
|
}
|
|
}
|
|
if latest.IsZero() {
|
|
return time.Time{}, false
|
|
}
|
|
return latest, true
|
|
}
|