usage: a Codex account's credits bought or given besides its plan show on the Usage page (#571: codex的套餐用完之后,可能会购买额外的信用点套餐…希望能做展示) — ChatGPT's /wham/usage answers, besides the 5-hour and weekly windows, the account's credits (credits: has_credits, unlimited, balance), which Codex spends once a window is used up; magpie read only the windows, so an account whose windows were spent showed spent though it still had credits to run on. codexWindows now reads them too, and a balance held (not unlimited, above 0) is the account's Balance, shown under its windows on the Usage page ("What is left on the account besides its windows") and in the panel card, for the account Codex is signed in to and for each saved one. TestCodexCreditsBalance (held, a few, none said, none held, spent, unlimited, unreadable). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... under a temp HOME pass.
This commit is contained in:
1 parent
bb42b47663
commit
4007beb9c0
4 files changed
+73
-9
No files matched your search
@@ -100,7 +100,7 @@ func AutoUseCodexReset(ctx context.Context, user string) (ResetOutcome, error) {
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
_, windows, resets, err := codexWindows(ViaLogin(ctx, "codex", who), tok, accountID)
|
||||
_, windows, resets, _, err := codexWindows(ViaLogin(ctx, "codex", who), tok, accountID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The credits a ChatGPT account holds besides its windows are its balance
|
||||
// (#571), shown when it has some to spend and they are not unlimited.
|
||||
func TestCodexCreditsBalance(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
credits any
|
||||
want string
|
||||
}{
|
||||
{"none said", nil, ""},
|
||||
{"held", map[string]any{"has_credits": true, "unlimited": false, "balance": "1234.5"}, "1234.5 credits"},
|
||||
{"a few", map[string]any{"has_credits": true, "unlimited": false, "balance": "42"}, "42 credits"},
|
||||
{"none held", map[string]any{"has_credits": false, "unlimited": false, "balance": "0"}, ""},
|
||||
{"spent", map[string]any{"has_credits": true, "unlimited": false, "balance": "0"}, ""},
|
||||
{"unlimited", map[string]any{"has_credits": true, "unlimited": true, "balance": ""}, ""},
|
||||
{"unreadable", map[string]any{"has_credits": true, "balance": "lots"}, ""},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := map[string]any{"plan_type": "plus", "rate_limit": map[string]any{
|
||||
"primary_window": map[string]any{"used_percent": 100, "limit_window_seconds": 18000}}}
|
||||
if c.credits != nil {
|
||||
body["credits"] = c.credits
|
||||
}
|
||||
json.NewEncoder(w).Encode(body)
|
||||
}))
|
||||
defer fake.Close()
|
||||
old := CodexBase
|
||||
CodexBase = fake.URL + "/backend-api/codex"
|
||||
defer func() { CodexBase = old }()
|
||||
plan, windows, _, credits, err := codexWindows(context.Background(), "tok", "acct-1")
|
||||
if err != nil || plan != "plus" || len(windows) != 1 {
|
||||
t.Fatalf("plan %q, windows %v, err %v", plan, windows, err)
|
||||
}
|
||||
if credits != c.want {
|
||||
t.Errorf("credits %q, want %q", credits, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func loginQuota(ctx context.Context, l Login) SubscriptionQuota {
|
||||
q.Windows, err = claudeWindows(ctx, l.User, true)
|
||||
} else {
|
||||
var plan string
|
||||
if plan, q.Windows, q.Resets, err = codexWindows(ctx, tok, accountID); plan != "" {
|
||||
if plan, q.Windows, q.Resets, q.Balance, err = codexWindows(ctx, tok, accountID); plan != "" {
|
||||
q.Plan = plan
|
||||
}
|
||||
q.Until = codexUntil(codexLoginAuth(l), time.Now())
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -641,7 +642,7 @@ func codexSubscriptionUsage(ctx context.Context, path string) SubscriptionQuota
|
||||
q.Error = err.Error()
|
||||
return q
|
||||
}
|
||||
q.Plan, q.Windows, q.Resets, err = codexWindows(ctx, token, accountID)
|
||||
q.Plan, q.Windows, q.Resets, q.Balance, err = codexWindows(ctx, token, accountID)
|
||||
if b, rerr := os.ReadFile(path); rerr == nil {
|
||||
q.Until = codexUntil(b, time.Now())
|
||||
}
|
||||
@@ -667,11 +668,20 @@ func codexUntil(auth []byte, now time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// codexWindows is the plan, allowance and rate-limit resets of the
|
||||
// ChatGPT account token signs in to.
|
||||
func codexWindows(ctx context.Context, token, accountID string) (plan string, out []QuotaWindow, resets *ResetCredits, err error) {
|
||||
// codexWindows is the plan, allowance, rate-limit resets and credits of
|
||||
// the ChatGPT account token signs in to. credits is what is left of the
|
||||
// credits the account bought or was given (#571), which Codex spends once
|
||||
// a window is used up, "" when it holds none or they are unlimited.
|
||||
func codexWindows(ctx context.Context, token, accountID string) (plan string, out []QuotaWindow, resets *ResetCredits, credits string, err error) {
|
||||
var data struct {
|
||||
PlanType string `json:"plan_type"`
|
||||
PlanType string `json:"plan_type"`
|
||||
// as Codex's /status reads it: {"has_credits":true,
|
||||
// "unlimited":false,"balance":"1234.5"}
|
||||
Credits *struct {
|
||||
Has bool `json:"has_credits"`
|
||||
Unlimited bool `json:"unlimited"`
|
||||
Balance string `json:"balance"`
|
||||
} `json:"credits"`
|
||||
RateLimit struct {
|
||||
Primary *codexWindow `json:"primary_window"`
|
||||
Secondary *codexWindow `json:"secondary_window"`
|
||||
@@ -683,7 +693,12 @@ func codexWindows(ctx context.Context, token, accountID string) (plan string, ou
|
||||
base := strings.TrimSuffix(CodexBase, "/codex")
|
||||
out = []QuotaWindow{}
|
||||
if err = accountJSON(ctx, base+"/wham/usage", token, map[string]string{"chatgpt-account-id": accountID}, &data); err != nil {
|
||||
return "", out, nil, err
|
||||
return "", out, nil, "", err
|
||||
}
|
||||
if c := data.Credits; c != nil && c.Has && !c.Unlimited {
|
||||
if n, perr := strconv.ParseFloat(strings.TrimSpace(c.Balance), 64); perr == nil && n > 0 {
|
||||
credits = fmt.Sprintf("%s credits", compactNumber(math.Round(n*100)/100))
|
||||
}
|
||||
}
|
||||
if data.Resets != nil {
|
||||
resets = codexResets(ctx, base, token, accountID, data.Resets.Available)
|
||||
@@ -694,7 +709,7 @@ func codexWindows(ctx context.Context, token, accountID string) (plan string, ou
|
||||
if data.RateLimit.Secondary != nil {
|
||||
out = append(out, data.RateLimit.Secondary.window())
|
||||
}
|
||||
return data.PlanType, out, resets, nil
|
||||
return data.PlanType, out, resets, credits, nil
|
||||
}
|
||||
|
||||
type codexWindow struct {
|
||||
|
||||
Reference in new issue
Block a user