usage: magpie usage and the TUI's Usage page show the agents' own calls, read from their session files, beside the gateway's, and a Codex-based client's calls aren't counted twice (Kumo31 on Discord: Codex used directly, not through magpie, was on the window's Usage tab but not in magpie usage) — magpie usage and the TUI only ever summed the gateway's log (usage.Summarize); the calls the agents' session files record that the gateway never saw (Codex on the user's own ChatGPT sign-in, Claude Code, Claude Desktop…) were merged only into the ledger the window's Requests tab and the tray panel read (#239). usage.Direct sums those rows of the ledger (Source "log", this computer's, rejected ones left out) by agent, model, account and session, priced as the ledger prices them, and magpie usage prints them after the gateway's tables under "not through magpie · the agents' own requests, read from their session files" (a model whose provider the file doesn't tell reads "local session/<model>"); the TUI's Usage page shows the same block under the gateway's, each with its totals and agents' and models' tables. The two parts are separate, so nothing is counted in both; a period with only the agents' own calls shows them under "no calls through magpie". All of magpie usage's output now goes to the writer it is given. Checking it against the owner's real data showed the ledger counted a Codex-based client's calls twice: miyi-helper's requests through the gateway are logged under its own name while its Codex session file names Codex, and the gateway/session-file matcher keyed on the agent, so they never matched; the matcher now keys on the session, tokens and time alone (a session id names one conversation), which also corrects the Requests tab. With read-only copies of the owner's ~/.config/magpie and four days of ~/.codex/sessions in a sandbox HOME: magpie usage week printed 177 calls not through magpie, in 1.3M, out 52.4K, cache read 3.9M, gpt-6-astra 154 and gpt-6-sol 23 — the same rows, to the token, as usage.LedgerOf (what /api/usage/requests serves) gave for the period; before the matcher fix, today's 7 miyi-helper calls showed in both parts. TestDirectCountsOnlySessionFileCalls (the miyi-helper case included), TestUsageShowsCallsNotThroughMagpie and TestUsagePageShowsCallsNotThroughMagpie fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
This commit is contained in:
1 parent
2f5ff69d3d
commit
7909de22ac
8 files changed
+277
-32
No files matched your search
+34
-9
@@ -723,7 +723,7 @@ func (m model) updateUsage(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
m.sum = usage.Summarize(m.period)
|
||||
m.sum, m.direct = usage.Summarize(m.period), usage.Direct(m.period)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -806,10 +806,36 @@ func (m model) viewUsage() string {
|
||||
if len(ql) > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if s.Calls == 0 {
|
||||
d := m.direct
|
||||
if s.Calls == 0 && d.Calls == 0 {
|
||||
b.WriteString(pad + " " + sMuted.Render("no calls in this time · route an agent through magpie and its usage shows up here"))
|
||||
return b.String()
|
||||
}
|
||||
names := map[string]string{}
|
||||
for _, a := range agent.All() {
|
||||
names[a.ID] = a.Name
|
||||
}
|
||||
room := max(2, (m.h-16-len(ql))/2)
|
||||
if s.Calls > 0 && d.Calls > 0 {
|
||||
room = max(2, room/2)
|
||||
}
|
||||
if s.Calls == 0 {
|
||||
b.WriteString(pad + " " + sMuted.Render("no calls through magpie in this time") + "\n\n")
|
||||
} else {
|
||||
m.usageBlock(&b, s, room, names)
|
||||
}
|
||||
// the calls the agents made on their own, as the app's Requests tab
|
||||
// counts them beside the gateway's (Kumo31 on Discord)
|
||||
if d.Calls > 0 {
|
||||
b.WriteString(pad + " " + sFaint.Render("not through magpie · the agents' own requests, read from their session files") + "\n")
|
||||
m.usageBlock(&b, d, room, names)
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
// usageBlock is one summary's totals, timeline and agents' and models'
|
||||
// tables, the gateway's or the agents' own calls.
|
||||
func (m model) usageBlock(b *strings.Builder, s usage.Summary, room int, names map[string]string) {
|
||||
line := sName.Render(fmtTokens(s.Tokens())+" tokens") + sMuted.Render(fmt.Sprintf(" · %d call%s · ", s.Calls, plural(s.Calls))) + sOK.Render(fmtCost(s.Totals))
|
||||
if s.Errors > 0 {
|
||||
line += sMuted.Render(" · ") + sBad.Render(fmt.Sprintf("%d error%s", s.Errors, plural(s.Errors)))
|
||||
@@ -835,11 +861,6 @@ func (m model) viewUsage() string {
|
||||
b.WriteString(pad + " " + sCursor.Render(sb.String()) + " " + sFaint.Render(first+" – "+last) + "\n\n")
|
||||
}
|
||||
|
||||
names := map[string]string{}
|
||||
for _, a := range agent.All() {
|
||||
names[a.ID] = a.Name
|
||||
}
|
||||
room := max(2, (m.h-16-len(ql))/2)
|
||||
table := func(head string, gs []usage.Group, name func(usage.Group) string) {
|
||||
b.WriteString(pad + " " + sFaint.Render(head) + "\n")
|
||||
w := 0
|
||||
@@ -862,8 +883,12 @@ func (m model) viewUsage() string {
|
||||
}
|
||||
return g.ID
|
||||
})
|
||||
table("models", s.Models, func(g usage.Group) string { return g.ID })
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
table("models", s.Models, func(g usage.Group) string {
|
||||
if g.Provider == usage.UnknownProvider {
|
||||
return g.Model // a session file's call whose provider isn't known
|
||||
}
|
||||
return g.ID
|
||||
})
|
||||
}
|
||||
|
||||
// quotaLines is the accounts' allowances, a line each as the app's cards
|
||||
|
||||
+2
-1
@@ -113,6 +113,7 @@ type model struct {
|
||||
gsel int // its member or rule picked: the members, then the rules
|
||||
period usage.Period
|
||||
sum usage.Summary
|
||||
direct usage.Summary // the agents' own calls, read from their session files
|
||||
quotas []provider.SubscriptionQuota // nil while the vendors are asked
|
||||
qasked bool // quotas were asked for
|
||||
qleft bool // windows read as what is left, not what is used
|
||||
@@ -187,7 +188,7 @@ func (m *model) reload() {
|
||||
m.gsel = clamp(m.gsel, len(g.Members)+len(g.Rules))
|
||||
}
|
||||
case pageUsage:
|
||||
m.sum = usage.Summarize(m.period)
|
||||
m.sum, m.direct = usage.Summarize(m.period), usage.Direct(m.period)
|
||||
case pageSessions:
|
||||
m.reloadSessions()
|
||||
case pageLibrary:
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/fx"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/sessions"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/usage"
|
||||
)
|
||||
@@ -109,3 +110,27 @@ func TestUnlimitedQuotaCell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Usage page shows, under the gateway's calls, those the agents made
|
||||
// on their own, read from their session files, as the app's Usage page
|
||||
// counts them (Kumo31 on Discord: Codex used outside magpie wasn't in the
|
||||
// TUI).
|
||||
func TestUsagePageShowsCallsNotThroughMagpie(t *testing.T) {
|
||||
home(t)
|
||||
now := time.Now()
|
||||
usage.Append(usage.Record{Time: now.Add(-time.Minute), Agent: "claude", Provider: "relay", Model: "m", Input: 5, Status: 200})
|
||||
old := usage.LogCalls
|
||||
usage.LogCalls = func(time.Time) []sessions.Call {
|
||||
return []sessions.Call{{Time: now.Add(-2 * time.Minute), Agent: "codex", Session: "c1", Model: "gpt-6-luna", Tokens: sessions.Tokens{Input: 1200, Output: 300}}}
|
||||
}
|
||||
t.Cleanup(func() { usage.LogCalls = old })
|
||||
m := press(t, model{w: 160, h: 60, page: pageUsage}, "t")
|
||||
v := m.viewUsage()
|
||||
gw, own, ok := strings.Cut(v, "not through magpie")
|
||||
if !ok || !strings.Contains(gw, "relay/m") || strings.Contains(gw, "gpt-6-luna") || !strings.Contains(own, "gpt-6-luna") {
|
||||
t.Fatal(v)
|
||||
}
|
||||
if strings.Contains(own, usage.UnknownProvider) {
|
||||
t.Fatalf("the unknown provider's id is shown:\n%s", own)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Direct is a period's calls the agents made on their own, which the gateway
|
||||
// never saw: those their session files record (Codex, Claude Code, Claude
|
||||
// Desktop…), summed up as Summarize sums the gateway's, by agent, model and
|
||||
// account. The Usage page's Requests tab and the tray panel count them
|
||||
// beside the gateway's; magpie usage and the TUI show them in a part of
|
||||
// their own (Kumo31 on Discord: Codex used outside magpie was on the
|
||||
// window's Usage tab but not in magpie usage).
|
||||
func Direct(p Period) Summary {
|
||||
return direct(p, time.Now(), LedgerOf(p, Filter{}).Rows)
|
||||
}
|
||||
|
||||
func direct(p Period, now time.Time, rows []Row) Summary {
|
||||
s := Summary{Period: p, Since: p.Since(now), Bucket: "day", Agents: []Group{}, Models: []Group{}, ProviderKeys: []Group{}, Accounts: []Group{}, CallerKeys: []Group{}, Sessions: []Group{}, Series: []Point{}}
|
||||
if p != Today && p != Week && p != Month {
|
||||
s.Period = All
|
||||
}
|
||||
agents, models, accounts, sessions := map[string]*Group{}, map[string]*Group{}, map[string]*Group{}, map[string]*Group{}
|
||||
put := func(m map[string]*Group, k string, g Group, r Row) {
|
||||
if m[k] == nil {
|
||||
m[k] = &g
|
||||
}
|
||||
m[k].addRow(r)
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.Source != "log" || r.IsRejected() || r.Time.Before(s.Since) {
|
||||
continue
|
||||
}
|
||||
s.Totals.addRow(r)
|
||||
id := AgentOf(r.Agent)
|
||||
put(agents, id, Group{ID: id}, r)
|
||||
put(models, r.Provider+"/"+r.Model, Group{ID: r.Provider + "/" + r.Model, Provider: r.Provider, Model: r.Model}, r)
|
||||
if who := r.Account(); who != "" {
|
||||
put(accounts, r.Provider+"@"+who, Group{ID: r.Provider + "@" + who, Provider: r.Provider, Account: who}, r)
|
||||
}
|
||||
if r.Session != "" {
|
||||
put(sessions, id+"|"+r.Session, Group{ID: r.Session, Agent: id}, r)
|
||||
}
|
||||
}
|
||||
list := func(m map[string]*Group) []Group {
|
||||
gs := []Group{}
|
||||
for _, g := range m {
|
||||
gs = append(gs, *g)
|
||||
}
|
||||
sort.SliceStable(gs, func(i, j int) bool {
|
||||
if a, b := gs[i].Tokens(), gs[j].Tokens(); a != b {
|
||||
return a > b
|
||||
}
|
||||
if gs[i].Calls != gs[j].Calls {
|
||||
return gs[i].Calls > gs[j].Calls
|
||||
}
|
||||
return gs[i].ID < gs[j].ID
|
||||
})
|
||||
return gs
|
||||
}
|
||||
s.Agents, s.Models, s.Accounts, s.Sessions = list(agents), list(models), list(accounts), list(sessions)
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/sessions"
|
||||
)
|
||||
|
||||
// Direct sums up only the calls the agents made on their own, as their
|
||||
// session files tell them — a Codex turn on the user's own ChatGPT sign-in,
|
||||
// say — by agent, model and session, priced by their model; the gateway's
|
||||
// calls stay Summarize's (Kumo31 on Discord: Codex used outside magpie was
|
||||
// on the window's Usage tab but not in magpie usage).
|
||||
func TestDirectCountsOnlySessionFileCalls(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
|
||||
os.WriteFile(catalog.CachePath(), []byte(`{"openai":{"id":"openai","models":{"gpt-6":{"id":"gpt-6","cost":{"input":2,"output":10}}}}}`), 0o644)
|
||||
|
||||
t0 := time.Now()
|
||||
now := time.Date(t0.Year(), t0.Month(), t0.Day(), 12, 0, 0, 0, t0.Location())
|
||||
Append(Record{Time: now.Add(-10 * time.Minute), Agent: "claude", Provider: "relay", Model: "m", Input: 5, Output: 5, Status: 200, Session: "g1"})
|
||||
// a client built on Codex, which the gateway logs under its own name
|
||||
// while its Codex session file names Codex: the same call, not one of
|
||||
// the agents' own
|
||||
Append(Record{Time: now.Add(-30 * time.Minute), Agent: "miyi-helper", Provider: "codex", Model: "gpt-6", Input: 700, Output: 70, CacheRead: 300, Millis: 9000, Status: 200, Session: "h1"})
|
||||
old := LogCalls
|
||||
LogCalls = func(time.Time) []sessions.Call {
|
||||
return []sessions.Call{
|
||||
{Time: now.Add(-time.Hour), Agent: "codex", Session: "c1", Model: "gpt-6", Tokens: sessions.Tokens{Input: 1000, Output: 100}},
|
||||
{Time: now.Add(-2 * time.Hour), Agent: "codex", Session: "c1", Model: "gpt-6", Tokens: sessions.Tokens{Input: 3000, Output: 300}},
|
||||
{Time: now.Add(-30*time.Minute + 9500*time.Millisecond), Agent: "codex", Session: "h1", Model: "gpt-6", Tokens: sessions.Tokens{Input: 700, Output: 70, CacheRead: 300}},
|
||||
}
|
||||
}
|
||||
t.Cleanup(func() { LogCalls = old })
|
||||
|
||||
d := direct(Month, now, LedgerOf(Month, Filter{}).Rows)
|
||||
if d.Calls != 2 || d.Input != 4000 || d.Output != 400 {
|
||||
t.Fatalf("direct totals %+v", d.Totals)
|
||||
}
|
||||
if len(d.Agents) != 1 || d.Agents[0].ID != "codex" || d.Agents[0].Calls != 2 {
|
||||
t.Fatalf("agents %+v", d.Agents)
|
||||
}
|
||||
if len(d.Models) != 1 || d.Models[0].Model != "gpt-6" || len(d.Sessions) != 1 || d.Sessions[0].ID != "c1" {
|
||||
t.Fatalf("models %+v sessions %+v", d.Models, d.Sessions)
|
||||
}
|
||||
if d.Cost == 0 || d.Unpriced != 0 {
|
||||
t.Fatalf("not priced: %+v", d.Totals)
|
||||
}
|
||||
// the gateway's calls are Summarize's alone
|
||||
if s := Summarize(Month); s.Calls != 2 || len(s.Agents) != 2 {
|
||||
t.Fatalf("summary %+v", s.Totals)
|
||||
}
|
||||
// a period that starts after the calls has none of them
|
||||
if d := direct(Today, now.Add(48*time.Hour), LedgerOf(Month, Filter{}).Rows); d.Calls != 0 {
|
||||
t.Fatalf("today two days on: %+v", d.Totals)
|
||||
}
|
||||
}
|
||||
@@ -535,10 +535,14 @@ func visibleLocal(chunks []*rowChunk) map[rowRef]bool {
|
||||
|
||||
// matchKey narrows candidates before comparing their end times. Request IDs
|
||||
// are consumed first; fallback matching still requires uniqueness both ways.
|
||||
// The agent isn't in it: a session's id names the one conversation, and a
|
||||
// client built on Codex (miyi-helper, say) is logged under its own name by
|
||||
// the gateway while its Codex session file names Codex, so its calls were
|
||||
// counted twice, once through magpie and once as not.
|
||||
type matchKey struct {
|
||||
session, agent string
|
||||
tokens [3]int64
|
||||
failed, hasID bool
|
||||
session string
|
||||
tokens [3]int64
|
||||
failed, hasID bool
|
||||
}
|
||||
|
||||
// matchTokens is what a call's tokens are matched by: its input with what it
|
||||
@@ -623,7 +627,7 @@ func matchedBlocks(gateways []*rowChunk, chunks []*rowChunk, skip map[rowRef]boo
|
||||
if session == "" {
|
||||
continue
|
||||
}
|
||||
key := matchKey{session, gateway.Strings[p.Text[0]], matchTokens(p.Tokens), p.Status >= 400 || p.Text[9] != 0, p.Text[11] != 0}
|
||||
key := matchKey{session, matchTokens(p.Tokens), p.Status >= 400 || p.Text[9] != 0, p.Text[11] != 0}
|
||||
g := groups[key]
|
||||
if g == nil {
|
||||
g = &matchGroup{}
|
||||
@@ -647,7 +651,7 @@ func matchedBlocks(gateways []*rowChunk, chunks []*rowChunk, skip map[rowRef]boo
|
||||
if p.Tokens[0]+p.Tokens[1]+p.Tokens[2]+p.Tokens[3] == 0 && !failed {
|
||||
continue
|
||||
}
|
||||
key := matchKey{session: c.Strings[p.Text[13]], agent: c.Strings[p.Text[0]], tokens: matchTokens(p.Tokens), failed: failed}
|
||||
key := matchKey{session: c.Strings[p.Text[13]], tokens: matchTokens(p.Tokens), failed: failed}
|
||||
count, candidate := 0, (rowRef{})
|
||||
// Without a local ID either gateway partition can match. With an ID,
|
||||
// only an unnamed gateway call can match (different IDs stay distinct).
|
||||
|
||||
+45
-17
@@ -81,28 +81,47 @@ func usageTo(out io.Writer, args []string) error {
|
||||
}
|
||||
loadCostCurrency()
|
||||
s := stats.Summarize(period)
|
||||
// the calls the agents made on their own, read from their session files:
|
||||
// the window's Requests tab counts them too (Kumo31 on Discord)
|
||||
d := stats.Direct(period)
|
||||
title := map[stats.Period]string{stats.Today: "today", stats.Week: "last 7 days", stats.Month: "last 30 days", stats.All: "all time"}[s.Period]
|
||||
if s.Calls == 0 {
|
||||
fmt.Println(muted.Render("no calls "+title+" ·"), "route an agent through magpie and its usage shows up here")
|
||||
fmt.Println(faint.Render(" " + stats.Path()))
|
||||
if s.Calls == 0 && d.Calls == 0 {
|
||||
fmt.Fprintln(out, muted.Render("no calls "+title+" ·"), "route an agent through magpie and its usage shows up here")
|
||||
fmt.Fprintln(out, faint.Render(" "+stats.Path()))
|
||||
return nil
|
||||
}
|
||||
fmt.Println(bold.Render(fmtTokens(s.Tokens())+" tokens"), muted.Render(title+" ·"), plural(s.Calls, "call"), muted.Render("·"), cost(s.Totals))
|
||||
fmt.Println(muted.Render(" in "+fmtTokens(s.Input)+" out "+fmtTokens(s.Output)+" cache read "+fmtTokens(s.CacheRead)+" cache write "+fmtTokens(s.CacheWrite)+" reasoning "+fmtTokens(s.Reasoning)),
|
||||
names := map[string]string{}
|
||||
for _, a := range agent.All() {
|
||||
names[a.ID] = a.Name
|
||||
}
|
||||
if s.Calls == 0 {
|
||||
fmt.Fprintln(out, muted.Render("no calls through magpie "+title))
|
||||
} else {
|
||||
printUsage(out, s, title, names)
|
||||
}
|
||||
if d.Calls > 0 {
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintln(out, muted.Render("not through magpie · the agents' own requests, read from their session files"))
|
||||
printUsage(out, d, title, names)
|
||||
}
|
||||
fmt.Fprintln(out, faint.Render(" "+stats.Path()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// printUsage is a summary's totals and its tables: agents, models,
|
||||
// upstream keys, accounts, gateway keys and the top sessions.
|
||||
func printUsage(out io.Writer, s stats.Summary, title string, names map[string]string) {
|
||||
fmt.Fprintln(out, bold.Render(fmtTokens(s.Tokens())+" tokens"), muted.Render(title+" ·"), plural(s.Calls, "call"), muted.Render("·"), cost(s.Totals))
|
||||
fmt.Fprintln(out, muted.Render(" in "+fmtTokens(s.Input)+" out "+fmtTokens(s.Output)+" cache read "+fmtTokens(s.CacheRead)+" cache write "+fmtTokens(s.CacheWrite)+" reasoning "+fmtTokens(s.Reasoning)),
|
||||
func() string {
|
||||
if s.Errors > 0 {
|
||||
return muted.Render(" · ") + plural(s.Errors, "error")
|
||||
}
|
||||
return ""
|
||||
}())
|
||||
|
||||
names := map[string]string{}
|
||||
for _, a := range agent.All() {
|
||||
names[a.ID] = a.Name
|
||||
}
|
||||
table := func(head string, gs []stats.Group, name func(stats.Group) string) {
|
||||
fmt.Println()
|
||||
fmt.Println(faint.Render(" " + head))
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintln(out, faint.Render(" "+head))
|
||||
w := 0
|
||||
for _, g := range gs {
|
||||
w = max(w, len(name(g)))
|
||||
@@ -112,16 +131,27 @@ func usageTo(out io.Writer, args []string) error {
|
||||
if s.Tokens() > 0 {
|
||||
share = fmt.Sprintf("%3.0f%%", 100*float64(g.Tokens())/float64(s.Tokens()))
|
||||
}
|
||||
fmt.Println(" "+pad(name(g), w), muted.Render(share), pad(fmtTokens(g.Tokens()), 7), faint.Render(pad(plural(g.Calls, "call"), 10)), cost(g.Totals), faint.Render(speed(g.Totals)))
|
||||
fmt.Fprintln(out, " "+pad(name(g), w), muted.Render(share), pad(fmtTokens(g.Tokens()), 7), faint.Render(pad(plural(g.Calls, "call"), 10)), cost(g.Totals), faint.Render(speed(g.Totals)))
|
||||
}
|
||||
}
|
||||
provider := func(id string) string {
|
||||
if id == stats.UnknownProvider {
|
||||
return "local session"
|
||||
}
|
||||
return id
|
||||
}
|
||||
table("agents", s.Agents, func(g stats.Group) string {
|
||||
if n := names[g.ID]; n != "" {
|
||||
return n
|
||||
}
|
||||
return g.ID
|
||||
})
|
||||
table("models", s.Models, func(g stats.Group) string { return g.ID })
|
||||
table("models", s.Models, func(g stats.Group) string {
|
||||
if g.Provider == stats.UnknownProvider {
|
||||
return provider(g.Provider) + "/" + g.Model
|
||||
}
|
||||
return g.ID
|
||||
})
|
||||
if len(s.ProviderKeys) > 0 {
|
||||
table("upstream provider keys", s.ProviderKeys, func(g stats.Group) string {
|
||||
name := g.ProviderKeyName
|
||||
@@ -140,7 +170,7 @@ func usageTo(out io.Writer, args []string) error {
|
||||
if who == "" {
|
||||
who = "account not recorded"
|
||||
}
|
||||
return g.Provider + " / " + who
|
||||
return provider(g.Provider) + " / " + who
|
||||
})
|
||||
}
|
||||
if len(s.CallerKeys) > 0 {
|
||||
@@ -165,8 +195,6 @@ func usageTo(out io.Writer, args []string) error {
|
||||
return n + " " + g.ID
|
||||
})
|
||||
}
|
||||
fmt.Println(faint.Render(" " + stats.Path()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// speed is how long the timed calls took to their first token, and how
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/sessions"
|
||||
stats "github.com/yetone/magpie/internal/usage"
|
||||
)
|
||||
|
||||
@@ -71,3 +72,35 @@ func TestUsageProviderKeys(t *testing.T) {
|
||||
t.Fatal(csv.String())
|
||||
}
|
||||
}
|
||||
|
||||
// magpie usage shows, beside the gateway's calls, those the agents made on
|
||||
// their own, read from their session files, as the window's Usage page
|
||||
// does (Kumo31 on Discord: Codex used outside magpie wasn't in it).
|
||||
func TestUsageShowsCallsNotThroughMagpie(t *testing.T) {
|
||||
groupsHome(t)
|
||||
now := time.Now()
|
||||
stats.Append(stats.Record{Time: now.Add(-time.Minute), Agent: "claude", Provider: "relay", Model: "m", Input: 5, Status: 200})
|
||||
old := stats.LogCalls
|
||||
stats.LogCalls = func(time.Time) []sessions.Call {
|
||||
return []sessions.Call{{Time: now.Add(-2 * time.Minute), Agent: "codex", Session: "c1", Model: "gpt-6-luna", Tokens: sessions.Tokens{Input: 1200, Output: 300}}}
|
||||
}
|
||||
t.Cleanup(func() { stats.LogCalls = old })
|
||||
var b strings.Builder
|
||||
if err := usageTo(&b, []string{"usage", "today"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := b.String()
|
||||
gw, own, ok := strings.Cut(out, "not through magpie")
|
||||
if !ok || !strings.Contains(gw, "relay/m") || strings.Contains(gw, "gpt-6-luna") || !strings.Contains(own, "gpt-6-luna") || !strings.Contains(own, "1.5K") {
|
||||
t.Fatal(out)
|
||||
}
|
||||
|
||||
// a call of the agents' own from before this month shows under all
|
||||
stats.LogCalls = func(time.Time) []sessions.Call {
|
||||
return []sessions.Call{{Time: now.AddDate(0, 0, -40), Agent: "codex", Model: "gpt-6-luna", Tokens: sessions.Tokens{Input: 7}}}
|
||||
}
|
||||
b.Reset()
|
||||
if err := usageTo(&b, []string{"usage", "all"}); err != nil || !strings.Contains(b.String(), "not through magpie") || !strings.Contains(b.String(), "gpt-6-luna") {
|
||||
t.Fatal(b.String(), err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user