* tui: show a ChatGPT account's credits beside its windows The Usage page printed only the balance for an account that had one, so a ChatGPT account holding credits lost its 5-hour and weekly windows there. The balance now follows the windows, as on the app's cards and in magpie quota. * accounts: tell a ChatGPT account's credits magpie accounts left out the credits an account holds, which magpie quota and the Usage page show. Its rows now carry them, in the line and as balance in --json. * codex: a switch for whether an account spends its credits A ChatGPT account holding credits keeps answering once a usage window is used up: the vendor spends the credits, and a task goes on. That stays the default. Each Codex account now has a say in it (Usage card and menu bar panel: Use credits; magpie quota credits <account> on|off; settings' codexNoCredits). Turned off, the gateway holds the account at 100% of a window, as a usage cap holds it at its share, so the other accounts, groups and fallbacks take the request and no credits are spent. With none left the request gets a 429 naming what holds each account and how to go on, unless the account spends its resets by itself and its week is used up: then a reset is spent first, whether every candidate was held or the last one tried was refused. A cap set on the account takes precedence. A reset made now forgets the account's cached allowance, so neither a cap nor this hold outlives the windows it started again. The switch is routing's only: which account Codex is signed in to is unchanged. The Routing page tells why such an account was left out, and magpie quota, magpie accounts and the TUI mark credits not spent. * usage: an auto-reset switch keeps by its first line The auto-reset row centred its switch on its words, so the switch moved when they rewrapped on a click, and it sat lower than the Use credits switch beneath it. Both now keep the switch by the first line. * gui tests: quota-curve expects the Use credits row to close a Codex account's section The credits switch adds a row after the Auto-use one, so the account after a Codex account follows the curve, the Auto-use row and then the Use credits row.
99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
package gateway
|
|
|
|
// A Codex rate-limit reset spent by itself: when everyone a request could
|
|
// go to is out of their allowance, a ChatGPT account the user lets spend
|
|
// its resets, its weekly window used up, spends one and is asked again
|
|
// (provider.AutoUseCodexReset has the rules).
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yetone/magpie/internal/provider"
|
|
)
|
|
|
|
// autoResetTimeout bounds the look at an account's week and the reset.
|
|
var autoResetTimeout = 20 * time.Second
|
|
|
|
// autoReset spends a reset of one of cands' Codex accounts, c — the last
|
|
// one, just out of its allowance — first: only when every other one of
|
|
// them sits out too. held are accounts left out as they won't spend their
|
|
// credits (planned.held): they sit out, and are out of their allowance as
|
|
// c is, so one of them may spend one after cands'. It says which account it
|
|
// was and what spending did.
|
|
func (s *Server) autoReset(ctx context.Context, cands []candidate, c candidate, held []candidate) (candidate, provider.ResetOutcome, bool) {
|
|
for _, x := range cands {
|
|
if x.restKey() == c.restKey() {
|
|
continue
|
|
}
|
|
_, resting := restOf(x.restKey())
|
|
if !resting && x.restID() != x.restKey() {
|
|
_, resting = restOf(x.restID())
|
|
}
|
|
if !resting {
|
|
return candidate{}, provider.ResetOutcome{}, false
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, x := range append(append([]candidate{c}, cands...), held...) {
|
|
a := x.p.Account
|
|
if a == nil || a.User == "" || seen[x.restKey()] || !provider.AutoResets(a.Agent, a.User) {
|
|
continue
|
|
}
|
|
seen[x.restKey()] = true
|
|
if out, ok := autoResetOf(ctx, a.Agent, a.User); ok {
|
|
s.Unrest(x.restKey())
|
|
x.capped = nil // its windows started again: held no more
|
|
return x, out, true
|
|
}
|
|
}
|
|
return candidate{}, provider.ResetOutcome{}, false
|
|
}
|
|
|
|
// autoResetSignedIn is autoReset for a request relayed with Codex's own
|
|
// sign-in, the only account there is.
|
|
func (s *Server) autoResetSignedIn(ctx context.Context) (string, provider.ResetOutcome, bool) {
|
|
who, ok := provider.CodexSignedIn()
|
|
if !ok || !provider.CodexAutoReset(who) {
|
|
return "", provider.ResetOutcome{}, false
|
|
}
|
|
out, ok := autoResetOf(ctx, "codex", who)
|
|
if ok {
|
|
s.Unrest("codex@" + strings.ToLower(who))
|
|
}
|
|
return who, out, ok
|
|
}
|
|
|
|
// autoResetOf spends one of agent's account user's resets if its rules
|
|
// let it, and says whether the windows started again.
|
|
func autoResetOf(ctx context.Context, agent, user string) (provider.ResetOutcome, bool) {
|
|
ctx, cancel := context.WithTimeout(ctx, autoResetTimeout)
|
|
defer cancel()
|
|
out, err := provider.AutoUseCodexReset(ctx, user)
|
|
switch {
|
|
case err != nil:
|
|
log.Printf("%s reset for %s not used: %v", agent, user, err)
|
|
case out.Code == "reset":
|
|
log.Printf("%s reset used for %s, its week used up: %s", agent, user, out.Text())
|
|
case out.Code != "":
|
|
log.Printf("%s reset for %s not used: %s", agent, user, out.Text())
|
|
}
|
|
return out, err == nil && out.Code == "reset"
|
|
}
|
|
|
|
// unhold moves the account c of a plan's left, held as it won't spend its
|
|
// credits, to its order: a reset started its windows again, and it is
|
|
// tried.
|
|
func unhold(order, left []Weighed, c candidate) ([]Weighed, []Weighed) {
|
|
i := slices.IndexFunc(left, func(w Weighed) bool { return w.ID == c.rest && w.NoCredits })
|
|
if i < 0 {
|
|
return order, left
|
|
}
|
|
w := left[i]
|
|
w.Unlisted, w.Capped, w.CapBack, w.NoCredits = false, 0, nil, false
|
|
return append(slices.Clip(order), w), slices.Delete(slices.Clone(left), i, i+1)
|
|
}
|