Files
sub2api/backend/internal/service/ollama_cloud_usage_reset.go
T
alfadb a1d5968b24 feat(ollama): async rate-limit reset for Ollama Cloud usage windows
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).
2026-09-13 10:41:47 +08:00

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
}