main
88
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3e5bc3d8fe |
gateway, usage: conversation text is kept only under a session the client named, and a client's "request-…" session counts like any other (#1355)
What changed: with recording on, a request that names no session used to get a made-up "request-…" id and its text saved under it. Nothing listed that id and GatewaySessionByID turned it away, so the text could never be opened. magpie now makes up no id: such a request isn't recorded, and the X-Magpie-Session reply header only echoes the client's own session. With no made-up ids left, the "request-" prefix checks in appendUsage, OTel, Usage's sessions, Routing's vias and the gateway Sessions projection only dropped sessions clients named themselves (appendUsage wiped them from the ledger). They are gone. The tests that held the old rule now say the new one, and gateway-sessions.md says it. Run: TestGatewayConversationNeedsTheClientsSession and TestGatewayConversationClientRequestPrefixedSession fail on the PR's code (a made-up id is returned; the ledger keeps 0 of 2 calls) and pass here; putting appendUsage's prefix check back turns the second red again. gofmt -l clean; go vet; GOOS=linux and GOOS=windows go vet -tags nogui on gateway, gui, sessions, settings, usage; go test -race -timeout 900s on those five: ok (gateway's TestCORSKeyThroughTheServer rerun after its fix). GUI: gateway-conversations, gateway-mode and all sessions-* tests, 216 cases in Chromium and WebKit: pass. |
||
|
|
c9b8263378 |
sessions, gateway: Sessions lists gateway sessions, and can keep their text on this computer when turned on (#1355, mu-zi-lee)
What changed: the Sessions page lists sessions seen only through the gateway (from the usage ledger, read-only, a native session of the same id wins), and shows in gateway mode. A Sessions switch, off by default and confirmed before it turns on, records each gateway exchange's text under the config dir (scrubbed, at most 7 days and 256 MiB, cleaned hourly). Settings sync and restore can't turn it on; POST /api/sessions/recording turns it on or off and can clear what was kept. Why: users of magpie as a gateway (gateway mode, LAN clients) had no way to see their conversations; the owner chose this design on the PR. Squashed from PR head 6a343ba1 onto origin/main. Conflicts: gui-shell.md (kept main's placeMenu text, took the PR's Sessions in gateway mode) and settings_keep_test.go (main's fields plus GatewayConversations). Run: go build ./..., go vet on gateway, gui, sessions, settings, usage; go test -race -timeout 900s on those five packages: all ok. |
||
|
|
ee657c0969 |
usage: a Kimi Code membership's k3 is counted at Kimi's API price for kimi-k3, not ≈¥0.000, and kimi-for-coding is unpriced rather than free (#1370, maicent: Kimi Code 的模型计费都是 0)
Why it was 0: magpie already counts a subscription's calls at the API list
price (a Claude or Codex account at its maker's,
|
||
|
|
d1d4b7ed2a |
usage: a heatmap of the last 53 weeks on the Requests tab, by tokens, requests or cost (#1369, nianlee-official)
What changed: the Requests tab of the Usage page shows a heatmap card like CC Switch's: a cell per day for the last 53 weeks, Monday on top, month names over the weeks and weekday names at the edge, a Tokens / Requests / Cost switch (kept in localStorage), a five-shade Less…More legend, and a tip on each day with its date, tokens, requests and cost. How: GET /api/usage/heatmap (usage.HeatmapOf) reads QueryPage's cached request index over a hidden period, "heatmap", which starts on the Monday 52 weeks before this week's. Its rows are summed by local calendar day in both the compact and the ledger path. So no raw log is scanned per render, and the days count what the chart counts: - the page's agent/provider/model/key filters, but not the period or day picked; - the gateway's own rejections left out; - cost of priced calls only, with an unpriced count. The days are in the local time zone and the shades use theme tokens (.l0–.l4, as in the sessions calendar). There are no left-border accents and no native <select>. A click never scrolls the page. A narrow window scrolls the card sideways inside itself, with the newest week in sight (kept there as it's laid out or resized, until the reader scrolls back), and the weekday names stay at the card's edge. The card is hidden when no day has calls. New strings are in zh, zh-TW, ja and de. Run: - go test -tags nogui ./internal/usage ./internal/gui (new TestHeatmapDays, TestHeatmapOf, TestUsageHeatmapRoute); gofmt -l clean; go vet darwin, GOOS=linux and GOOS=windows go vet -tags nogui on both packages; go build for darwin, linux and windows. - New internal/gui/tests/usage-heatmap.test.cjs in Chromium and WebKit, at 420 and 1100, in en, zh, zh-TW, ja and de, plus dark and the empty case. It checks: - the cells, labels, legend and the busiest and quiet days; - the newest week in sight and only the card scrolling sideways; - the tip's text and that it stays inside the card; - that a metric click moves nothing and is kept; - that the filters, but no period or day, are sent. - Breaking checks: with the keep-at-end guard reverted, 5 of its 11 subtests fail in WebKit; with the server's heatmap dropped, TestUsageHeatmapRoute fails. - i18n-keys, click-scroll, gui-de, gui-ja, gui-zh-tw, sessions-usage-open and all usage-*.test.cjs GUI tests pass in both engines (161 each). - A sandboxed `magpie web` (temp HOME, 2605 fake calls over 400 days, ports 3671/3672): the card fills 369 days at 1280 and 420 wide, and the tip reads "2026年10月1日周四 / 2.6M token · 23 个请求 · ≈$1.05+". |
||
|
|
f368f709c3 |
test(usage): TestLogChangeTrustedAppendReuse counts the prefix it re-hashes, not an empty fingerprint (CI red on Windows since 439cfc4a)
What changed. TestLogChangeTrustedAppendReuse (Windows-only) took
`after.hash != ""` to mean "the read after a trusted append re-read the
historical prefix". Since #1357 (
|
||
|
|
439cfc4ac4 |
usage: the parse is the log fingerprint, dropping the build's full re-read before it (#1357)
What changed. A usage-log snapshot's fingerprint is now produced by the parse itself: each complete line read is fed into a SHA-256. When a build continues the old snapshot's blocks, it continues the old snapshot's digest too (crypto/sha256's state, kept on the snapshot as `digest`); otherwise the digest starts fresh from byte 0 with the parse. The build's before-read logRecordHash over the whole file is gone. The fingerprint covers the bytes actually parsed (`off`), not info.Size(), so a half line another writer is in the middle of appending is no longer counted as read; the unchanged check, the prefix check and the after-read check all hash `off` bytes. What stays, as #1328's tests require (ggbdpq's analysis on #1357): the unsettled unchanged check, the prefix re-verification on a non-trusted continuation, and the after-read check, which now compares the parse's own digest against the file as it is after the read. A same-tick rewrite is still caught at the build, not at a later read. On the trusted, settled path nothing is re-read, as before; that snapshot now also carries a fingerprint (the continued digest), so a later non-trusted append can verify its prefix instead of rebuilding from byte 0. Test changed: TestUsageRewriteBeforeFingerprintIsNotCached counted logRecordHash calls and rewrote the file at call 1 (after the before-read hash, before the parse) or call 2 (after the read). With the parse being the fingerprint there is no call 1 any more, so the hash=2 case could no longer fire. Its guarantee still holds and is still tested at both boundaries: "before-read" rewrites at the build's first change-time look, after its stat and before it opens and parses; "after-read" rewrites at the after-read fingerprint check. Removing the after-read check makes after-read fail. No other test changed. New tests: - TestUsageAppendReadHashesNoPreReadScan: 20,000-row settled log (2,760,615 bytes), 5 x Append + Summarize(Today), counts the bytes logRecordHash re-reads. origin/main: 7,728,984 bytes per cycle (2.80x the log), FAIL; this commit: 4,968,615 (1.80x), PASS. Wall time per cycle on an M-series Mac, 3 runs each: 3.43-3.52 ms before, 2.28-2.52 ms after (~32% less). The remaining two passes are the prefix check and the after-read check, which the same-tick guarantee needs. - TestUsagePartialLineThenCompletion (settled and unsettled): another writer's half line, a read, then the line completed; the fingerprint covers the parsed bytes only and the completed record is counted. On origin/main it fails on the fingerprint check (it covered the half line). Break checks: no after-read check -> RewriteBeforeFingerprint/after-read fails; no prefix verification on a continuation -> SameSizeAndMtimeRewrite append=true, SameTickRewrite append=true and LogGrowingRewriteThenAppend fail; lines not fed into the digest -> SameTickRewrite fails. Run: go test -tags nogui ./internal/usage/... ./internal/gateway/... (ok, temp HOME, pinned caches, removed after); go test -race ./internal/usage/... (ok); -race -count=10 on the rewrite/append tests (ok); gofmt -l (empty); go vet, GOOS=linux and GOOS=windows go vet -tags nogui ./internal/usage/ (ok). Not tried on Windows or Linux hardware; the change is platform-independent hashing over the same read. |
||
|
|
3c629133c2 |
usage: refresh caches after same-tick log rewrites (#1328)
* usage: same-tick rewrites refresh cached totals and request pages Trust change stamps only when they were settled at the snapshot read's start. Fingerprint recent snapshots and append prefixes, and discard fingerprint trust when the content changes during a read. Fixes https://github.com/yetone/magpie/issues/1305 Verified: deterministic old-predicate FAIL/new PASS; usage tests with race; nogui vet; Linux nogui and Windows builds; usage test cross-compilation for Windows, Darwin and the unsupported-stamp fallback on FreeBSD. Full nogui suite exposed the independent sessions TestList empty-session limit failure. Native Linux GUI vet/build lack pkg-config and GTK/WebKit; native Windows/NTFS execution was not run. * usage: keep FILETIME stamp ages valid beyond UnixNano range Convert whole seconds and the 100ns fraction separately. Checked the epoch, current dates with fractional ticks and year 2500; Windows build and usage test cross-compilation pass. Native Windows execution was not run. * usage: keep history cached after an append settles An unchanged log now validates a recent snapshot's nonempty fingerprint before publishing a settled clone. Preserve its version and sealed blocks so the summary and page caches stay warm without mutating readers' snapshots. Follow-up to the review on https://github.com/yetone/magpie/pull/1328, for https://github.com/yetone/magpie/issues/1305. On e28e686, the real-clock append/read/settle/read tests fail with a version bump and 32,428 additional allocations over 4,000 versus 10 rows. With this change the targeted race run passes, with 165 extra allocations. The existing performance test now measures this path on the real clock. Verification uses Go 1.26.8, a temporary HOME and pinned Go caches. Native Linux GUI checks require missing pkg-config/GTK/WebKit libraries; native Windows/NTFS execution is not available. No GUI assets change. Implementation and verification used AI assistance. Final checks: usage race PASS (421 test results); nogui suite PASS (7,273 test results, 44 packages; 21 skipped tests, 3 packages without tests). Nogui vet, Linux nogui build, Windows build, and usage test cross-compilation for Windows amd64, Darwin arm64 and FreeBSD amd64 PASS. Native Linux GUI vet/build were attempted and failed at the missing pkg-config executable. |
||
|
|
ed31293c0e |
usage, gui: a reply its vendor let go in a burst over a couple of hundred milliseconds tells no speed, not 2,237 tok/s (John on Discord)
John's Kimi Code rows after v0.1.1119 (OpenCode, group/k3-glm, api.kimi.ai,
Chat, k3-256k): 550 tokens, 22 of them reasoning, in 16 s with the first
content at 5.2 s read 2,237 tok/s, and 395 tokens in 19 s 2,012. 528 answer
tokens at 2,237 tok/s is 236 ms: the burst the vendor let go at the end.
|
||
|
|
de3b1c646a |
usage: a subscription card's window says what the whole window holds, "Whole ≈ N tokens · ≈ $X", reckoned from what magpie routed through the account since the window began over the share used, at API list prices (Chiao on Discord)
usage.WithWindowHolds fills QuotaWindow.Holds on GET /api/usage/quotas and its refresh, from usage.jsonl: the card's provider (by its id now, so a moved plugin's card and a renamed provider count), its account (ProviderAccount, or an older record's Host), calls to the models the window counts (Claude's Opus-only or Fable week), not turned away, from the window's start up to when the share was read. Priced by catalog.Price.CostSplit at the maker's list price, else the provider's own list, never the user's own subscription price: Anthropic's 5-minute and 1-hour cache writes at 1.25x and 2x input, OpenAI's cached input and no cache write price. A call with no API price leaves the window tokens-only. The caller's cards are not written to. Not told where it wouldn't be honest: under 5% used, nothing routed in the window, magpie's log starting after the window did, no reset or no span (credits with no reset, Copilot's premium requests), Antigravity's per-model family and pool windows, Aside windows, a card with an error, a balance, or one read from another computer. The tooltip says how it was reckoned and that use of the account outside magpie makes it read low (the request said 偏高; such use raises the share without adding to what magpie saw, so the estimate is lower than the truth). en, zh, zh-TW, ja, de. Run: go test -tags nogui ./... in Docker (golang:1.26.8): all pass but internal/agent's Aside* chmod tests, which fail only as root and pass with --user 1000. The new tests fail with the log-start check removed, the model scope removed, the caller's windows not cloned, and 1-hour cache writes at the 5-minute price. Against a read-only copy of the owner's usage.jsonl, quotas.json and models.json: Claude's week (71%) ≈ 46K tokens / $14.16 from 165 calls, its Fable week and 5 hours (12%) nothing, every other card nothing (under 5%, or no reset). New quota-holds.test.cjs passes in Chromium and WebKit at 360, 440 and 1000px in en, zh (万), zh-TW (萬), ja, de, and fails with the new CSS removed; click-scroll, gui-ja, gui-de, i18n-keys, quota-* and usage-* pass (304/304). go vet -tags nogui (darwin, GOOS=linux, GOOS=windows) and gofmt -l on the touched packages pass. |
||
|
|
de5455b8fe |
gateway, usage, gui: a reply whose vendor sent its first words and held the rest to send in one burst tells no speed, not 1,367 tok/s (John on Discord)
John's request (OpenCode, a group served by Kimi Code at api.kimi.ai, Chat) showed 1,367 token/s: its tokens counted over the time from its first token to its end, though nothing came in between and the rest came at once. The 100 ms / 10,000 tok/s bounds catch a burst only when the first content comes with it. firstToken now reads on past the first text and marks when each content event's bytes came (one mark a millisecond, up to 65,536 marks and the 8 MB it reads), and flowMs is how long the content took to come from where the speed is counted: the span from a tenth of its bytes to nine tenths, scaled to the whole. A steady stream gets about its whole window; one let go in a burst about 1 ms. It is kept as Call/Try/Route .flow and usage.Record.flow_ms (and the Requests page's packed rows), and usage.DecodeOf, routing.js's decodeOf and app.js's ledDecode count no longer a window than it. A record without it reads as before. Verified: TestBurstTellsNoSpeed, a fake Chat vendor that sends a word, waits 600 ms, then 199 words and the end in one write (1,000 tokens), for a Chat and a Responses agent: no speed; before, 1636 and 1655 tok/s. Its steady case (a word every 60 ms) keeps 200 tokens over most of its window. TestCodexBackendBurstTellsNoSpeed is the same through Codex's own ChatGPT backend (before: 1,000 tokens in 601 ms). TestFirstTokenKinds now expects the stream read past its first text. Go tests ran on Linux (golang:1.26 in Docker; this Mac's syspolicyd held new test binaries at launch). GUI: routing-burst-speed (a route with John's 5.1 s first token, 6.7 s and a 2 ms flow: was "1367 tok/s", now none) and usage-speed (a row with flow_ms 2 shows "—") fail without the JS change and pass with it, with reasoning-speed, routing-request-metrics and usage-ledger-detail, in Chromium and WebKit (en/zh, ja/de where the tests have them); no strings changed. Not reproduced with Kimi Code itself: whether it holds the stream so is inferred from the numbers. |
||
|
|
b2432754e5 |
usage, gateway: Antigravity's gemini-3.8-flash answered as gemini-3.8-flash-n is no longer marked as another model (dumplings on Discord)
Google's Code Assist backend names the deployment serving Gemini 3.8 Flash "gemini-3.8-flash-n" in modelVersion, for every level it is asked at. On the owner's real Antigravity account (daily-cloudcode-pa, 2026-10-08) gemini-3.8-flash-low, -medium and -high all answered gemini-3.8-flash-n, with or without a thinking level. gemini-3.8-flash-n itself is a 404 and fetchAvailableModels doesn't list it. 3.7/3.6 Flash's levels answer the bare family name, which #462 already allowed. So the requests log's amber "actual model" and its "probably swapped" note were a false flag. usage.GeminiServing accepts the sent Gemini model's own name plus "-n". Swapped and the readers for kept rows and routes (request_page row(), history routedAgain) all use it, so rows already logged read correctly too. It covers Gemini only. gpt-5 -> gpt-5-mini, gpt-5 -> gpt-5-n, claude-sonnet-4-5 -> -n, gemini-3.8-flash -> -lite, -> gemini-3.7-flash-n, -> -nano and -high -> -low are still swaps. Verified: TestSwappedGeminiServingName, TestLedgerGeminiServingNameIsNoSwap, TestKeptRowGeminiServingNameNotSwapped and TestRouteKeptGeminiServingNameNotSwapped fail on the old code ("gemini-3.8-flash answered as gemini-3.8-flash-n is the model asked for", "still swapped") and pass with it. Sandbox magpie on a copy of the real Antigravity login: before, the routing log had swapped:true on every gemini-3.8-flash-n try. After, chat completions at none/low/medium/high and /v1/responses are not swapped, and the ledger reads the earlier rows as not swapped. go test -tags nogui ./... passed under a temp HOME. go vet ran for darwin/linux/windows, and the linux/windows builds passed. |
||
|
|
e95852e301 |
usage, gui: 用量页补上各 agent 会话文件里的用量,请求页按「经过 / 不经过 magpie」拆开 (#948)
* sessions: dsh、ZCode、WorkBuddy 的会话文件里有各自花掉的 token,用量台账从来不读
Sessions 页面一直都在读这些 agent 自己的会话文件,用量台账没有:用量页只显示网关本
身应答过的调用——也就是装上 magpie 之前的什么都没有,而一个直接指向厂商的 dsh 更
是什么都没有。本机上装 magpie 之前就已经在用这些 agent(用它们自带的模型),那段
用量一直对不上账。
* 新增三类"整文件读取"的调用来源,走 wholeCallReaders(它们的调用不是一行一条,
或者文件不能从中间接着读):dsh 的会话是 zstd 分帧追加的;ZCode 用的是 OpenCode
那几张表;WorkBuddy 的同一句回复会在多行里重复同一个 messageId。三者的 token
语义各自换算成 Call 的 Input/Output/CacheRead/CacheWrite,调用缓存跟着记一份(v9)。
* 每个来源都跳过经由 magpie 网关发出的调用:网关自己记过那一本账,还带着真正应答
的供应商和账号;dsh 发往网关的请求不带会话头,两边事后对不上(token 数每次不同、
又没有可配对的 id),硬读就是同一通调用记两次。dsh 认它被配置去的那个 provider
(dshGatewayProvider:magpie / dial),ZCode 和 OpenCode 认路由里写的 provider
(ocGatewayProvider)。WorkBuddy 那部分见下一个提交。
* WorkBuddy 的调用里,同一句回复的多行只记最后一次(它带这个回复的总量);而它从
旧历史搬过来的回复一条 messageId 都没有,每一条都是独立的一通调用——会话汇总的
读法(workbuddyLine)就是这么数的,这里照它来,否则台账会比 Sessions 页面少算,
正是本次要消灭的那类"两页对不上",只是方向相反。
* dsh 的一通调用保留它文件里记的两样东西:被配置去的供应商(Upstream,作为历史,
从来不当成路由的证据),以及厂商真正应答的模型——回复的 replayState 里那个和
请求模型不一致时,Model 记应答的、Requested 记被请求的,和 Claude Code 的文件
一样的读法,好让请求表的若干列有东西可填。
* 会话的 fork:dsh 的会话从另一个会话续起来时会带上对方已写入的事件(isSeeded /
seedLength,session/end-seed 标记之后的不算),parser 跳过的这里同样跳过,否则
同一通调用会在每个 fork 上再花一次。
* WorkBuddy 的 subagents/ 子目录是父会话自己的调用来源(它带自己的消息 id,会话
归到父会话名下);调用这条路不读 file.main,那个标记是 Sessions 页面给"会话自己的
文件、不是子代理的"用的。
夹具:dsh 的三个会话(普通、带供应商与应答模型、以及一通经网关的),ZCode 的两条
(其中 ses_zcA 那条的 providerID 是 magpie,应被跳过),WorkBuddy 的既有会话(m1 的
usage 同时落在 assistant message 和它最后一个 function_call 上,只能记一次;旧历史
三条无 messageId 的回复各记一次)和一通经网关、一通走用户自建端点的会话。
调用缓存升到 v9:v8 里既没有 dsh 的供应商和应答模型,也没有"跳过网关调用"这件事,
旧缓存会继续把旧账 Serve 出来。
TestDshCalls、TestZCodeCalls、TestWorkBuddyCalls 是新的;把跳过网关调用那几行、以及
WorkBuddy 旧历史那条判据去掉,它们必挂、加上必过(都实际跑过反向验证)。
验证:go vet;go test ./internal/sessions(只余 TestCursor,Windows 路径前缀问题,
改动前同样失败);go build(windows)。
* usage: 请求页把"magpie 转的"和"它只读到的"分成两个数,GUI 跟着拆成两栏
用量页一直只显示网关自己应答过的调用,而现在它还要读 agent 自己会话文件里的调用:
两本账混在一行里,读者分不清哪一段是 magpie 转的、哪一段只是它从文件里读到的,
而总数事实上几乎全部来自后者(本机 30 天 847M token 里,网关只转了 2.4%)。Summary
里的两个来源卡片也加不出这个数来:它们只算 input 和 output,上面的 KPI 把缓存的
token 也算在内,于是"全部请求"读作 13.6M,而"Tokens"是 847M。
* usage.RequestPage 和 usage.Filter 各多一样东西:前者在 Sum 之外带上 Through 和
Direct(网关应答的,和从会话文件读到的),后者多一个 Through 维度。两种建页路径
(流式的 queryPage 和治疗桌用的 pageFromLedger)都做同一个拆分,两边一致——
TestCompactPageMatchesLedger 现在也覆盖两个来源筛选。
* 每一行本来就是按 Source 区分这两类的("log" 是会话文件里的调用,其余走的网关),
拆分就落在它上面;网关自己拒掉的调用两边都不算(页面在总数下面也这么说),所以
SourceOf 对它返回 "",点哪一栏都同样把它排除,两栏自己的计数本来也不含它。
* 两栏自己的数字是在"来源"这个挑选被清空的情况下数的,和各维度分组一样:三栏是读者
来回切换的地方,各自得一直报着自己的数,否则点了其中一栏另一栏读作 0,且没有回
到全部的路。
* /api/usage/requests 支持 via=through|direct;GUI 那一侧的三个格子、计数单位、
方块样式在下一个提交里。
* Filter.Via 这个名字和 Record.Via(转发调用的那台电脑,#542)重了,改叫 Filter.Through。
Sessions 页列表长度那件事(sessionListLimit)拆到单独的 PR 里,不在这里。
验证:go vet;go test ./internal/usage(只余 TestOTelRetryAfterBound,计时的 ±30ms,
origin/main 同样失败);go build(windows)。
* gui: 请求页把来源拆成三栏,汇总那一排也改成方块
用途页的请求栏一直只有一本账(Sum),而现在它既显示网关应答的调用,也显示从各 agent
自己会话文件里读到的调用。三个格子画在上方汇总那一排方块里,样式一致:三张各自带
边框、彼此留缝的卡片,窄屏(≤620px)下竖排。
* 每栏是一个 button:点一下就只列这一来源的请求,再点一下、或点"全部请求"回到
全部;被选中的那一栏底下有一道本主题色的线。aria-pressed 标出当前选中的是哪一栏。
* 计数后面跟单位("147 请求"),免得一个光秃秃的数字被读成下面的 token;token 是
四项合计,和上面 KPI 的算法一致。
* 三个格子在重绘之间是同一批元素、原地改(class / aria / 文本 / onclick),不是每次
replaceChildren 重建——同一份 app.js 里 5810 行的规定就是这么写的("The switch is
the same element across redraws, changed in place, so one being pressed or focused is
never swapped out"),正在点的那个不该被换掉。
* 汇总那一排本身(五张 KPI 卡)改成同样的方块:各自一张卡、有缝,边框写成
var(--line) 而不是 --card-edge(那是面板用的变量,这些卡在独立窗口里也有)。
≤860px 两列排布时卡片之间的那几条线照旧:奇数位去掉左边框、第三张起加上边框,
是 e0eaf5b4 带 #799 的老规则,这里只是跟着搬了位置,没有删。三张来源格是 button,
不参与这套分隔线。
* 供应商·账号 一栏:来自会话文件的行显示它文件里记的那个供应商 id(Codex 一向
如此),有账号时仍是账号优先,什么都没有才说"本地会话"。"本地会话"角标的条件
跟着放宽到账号或供应商任一存在,提示分两种说法:文件记了供应商的,说明这是它
被配置去调用的那个记录、不是 magpie 看到的路由(不等于官方供应商);只认得出
账号的,沿用原来那句"仅当本地元数据能明确识别时显示账号"。
* 窗口参数支持 via,用后从地址栏清掉,和 tab/provider/agent 等一致。
* 新增的串子在 en/zh/ja/de 四种语言里都有("{n} tokens" 上游已有,不重复加,
i18n-keys 绿)。
验证:node --check(app.js、i18n.js);go vet;go build(windows)。
界面效果在本机用改动后的源码构建的二进制上过目:三栏的切换与计数、方块样式、
窄屏竖排、请求表新增的会话文件行(含供应商 id)。
浏览器套件(chromium 与 webkit、en/zh):session-identity、usage-ledger、
ledger-protocol、number-units、usage-ledger-chart、usage-ttft-rounding、
usage-chart-axis、usage-fold、web-page、usage-accounts、i18n-keys 全绿;
ledger-hscroll、usage-speed、click-scroll 全绿;gui-ja、gui-de 各一条失败、
usage-ledger-detail 六条失败,均在本机 origin/main 上同样失败,与本次改动无关。
usage-ledger-chart 新钉了 ≤620px 下 KPI 卡与来源格的分隔线(把 app.css 换回上游版本
它必挂,实测过)。
---------
Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
|
||
|
|
395c4bc51e |
tests: a test that sets time.Local does it through testenv.Zone, which first waits out the stand-ins' first runs — their os.Stat reads time.Local, and TestDsh -race alone raced it every run
testenv.StandIns (
|
||
|
|
0bf9d77d3f |
agent tests: TestDriftBypassed's request reaching the gateway is kept to its run, so -count=2 passes
The test stood for a request with usage.Saw("codex"). Saw's record lasts
the process, which the gateway relies on (LastSeen answers from it before
a request is written to the log), so a second run started with a request
seconds old, newer than its prompt five minutes back, and found nothing
bypassed ("bypassed: <nil>", also on origin/main).
Drift reads LastSeen through a package variable the test replaces with its
own clock; production still reads usage.LastSeen. That Saw is LastSeen's
answer at once moves to usage's own TestSawIsLastSeen, which removes its
record afterwards.
-run 'TestDrift|TestSawIsLastSeen' -race -count=5 passes; before, -count=2
failed.
|
||
|
|
e961836fb0 |
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
* usage: the usage tests pass at any time of day, midnight included The usage tests stamped their calls with the machine's time and then asked for today, 7 or 30 days, or a cached answer, which the code worked out from its own time.Now a moment later. A minute after midnight a call made "a minute ago" was yesterday's, and when midnight fell between the test's read and the code's, the call and the period were of two days. magpie usage, the TUI's Usage page, the two Codex cache-write tests and the computers' shared days failed in the first minutes after midnight (the TUI's after midnight UTC, the zone its package's tests run in since |
||
|
|
e277f5c5ef |
usage: four tests pass when run alone and in any order — one saves no account copy mid-test, three price with the catalogue they write (#1045)
* usage: TestQueryPageSourceAndIdentityInvalidation passes when run alone
Run alone, the test failed at any time of day:
request_page_test.go:224: unchanged source rebuilt
In a package run it passed only because an earlier test had looked at
the accounts less than 30 seconds before it. QueryPage calls
provider.All, whose rememberLogins saves a copy of the account Codex is
signed in to into logins.json, at most once every 30s (loginsSeenAt, a
package variable in provider). Alone, the test's first QueryPage made
that save. SessionIdentities then listed "one" twice (auth.json and the
saved copy), the request cache's key changed and every chunk was
rebuilt, so the pointer check failed. The test also expects only
auth.json's identity later on: with a saved "one" sharing a/u with
"two", the "two" step resolves to "" (two members of one workspace).
With the first check let through, it failed at request_page_test.go:237
"identity {... SessionAccount: ...}". The same failure hits a package
run whenever more than 30s pass between the last earlier look and this
test.
The test now calls provider.ForgetAccounts and provider.Accounts before
it signs Codex in, as an earlier test in a package run does, so no copy
is saved while it runs and the result no longer depends on test order.
This holds while the test runs within rememberLogins' 30s of that look;
it takes well under a second, -race and at.sh included. No other usage
test fails run alone: each test in the package was run alone, each
under a fresh HOME, and only this one failed.
Verified (go test -tags nogui under a temp HOME, Go caches pinned):
- before: -count=1 -run '^TestQueryPageSourceAndIdentityInvalidation$'
./internal/usage: --- FAIL ... request_page_test.go:224: unchanged
source rebuilt; -race -count=20: 1 of 20 failed (the first; the
later ones ran inside the 30s window); the same at local 12:00:00.
With its neighbours (-run '^(TestQueryPage|TestRequestPage)') it
passed, and failed again at :224 with a scratch 31s sleep at the
test's start.
- after: alone -count=1 ok, -count=20 ok, -race -count=20 ok; at local
12:00:00 and 00:00:30 PASS; with neighbours plus the scratch 31s
sleep ok; go test ./internal/usage ok; go vet ok; gofmt clean.
Without the change the run-alone command fails with the FAIL line above.
* usage: tests that write their own catalogue price with it, in any order
TestDirectCountsOnlySessionFileCalls and TestLedgerWithSessionLogCalls
write a catalogue to catalog.CachePath() and expect calls priced from
it, but never called catalog.Reset. The catalog package reads the cache
once and keeps what it read until Reset, so each test priced with
whatever catalogue an earlier test had left loaded. In file order they
passed only because no earlier test had. In another order the calls
came out unpriced. Run as
go test -run '^(TestDirectCountsOnlySessionFileCalls|TestLedgerUsesTheStatedPrice|TestLedgerWithSessionLogCalls)$' ./internal/usage
TestDirectCountsOnlySessionFileCalls ran first and left its gpt-6
catalogue loaded, and TestLedgerWithSessionLogCalls failed:
ledger_test.go:228: logged call {... Provider:session-unknown ...
Cost:0 Priced:false ... Source:log}
With -shuffle=1, TestLedgerWithSessionLogCalls ran first and left its
claude-sonnet-5 catalogue loaded, and the other one failed:
direct_test.go:55: not priced: {Calls:2 ... Cost:0 Unpriced:2 ...}
Each passes alone, so running every test alone doesn't find this. It
was found in review with go test -shuffle=on ./internal/usage, which
failed 5 runs of 6.
Both tests now call catalog.Reset after writing the cache, and again
on cleanup, as TestLedger and TestSummaryByAccount do. So does
TestLedgerUsesTheStatedPrice, the only other test that writes the
cache with no Reset there. It passes in any order, since its stated
prices win, but it checks that no tier is counted at the catalogue's
price, which needs its own catalogue. Every other test in the
repository that writes catalog.CachePath() already resets the
catalogue there.
Verified (go test -tags nogui under a temp HOME, Go caches pinned):
- before, on main: the run above fails as shown. All six orders of the
three tests (-shuffle=1, 2, 5, 7, 13 and 16) fail: at
ledger_test.go:228 when TestDirectCountsOnlySessionFileCalls runs
before TestLedgerWithSessionLogCalls, at direct_test.go:55 when it
runs after. go test -shuffle=N ./internal/usage fails for the
review's seeds 1791276105858112000, 1791276117280881000,
1791276129005822000, 1791276140549291000 and 1791276151835143000,
and for 101, 202, 505, 808 and 1010 of the seeds 101, 202, ... 1010;
each failure is in one or both of these two tests.
- after: the run above ok; all six orders ok; -race -count=20 on the
three tests ok; at local 12:00:00 PASS; go test ./internal/usage ok,
and ok with each of those 15 seeds; each test in the package
passes run alone; go vet ./internal/usage ok; gofmt clean.
Without the change the run above fails with the ledger_test.go:228
line, and with -shuffle=1 it fails with the direct_test.go:55 line.
|
||
|
|
117b6c7fb4 |
usage tests: TestReasoningReplyTimedByItsAnswer's timeline rows are made now, as LedgerSeries buckets today by the clock
The test gave LedgerSeries records dated 2026-10-06 14:30 UTC while
LedgerSeries reads today from time.Now(), so it passed only on 2026-10-06
and has failed on every machine since ("part: {Calls:0 ...}" just after
midnight CST on 10-07). The summary and row checks keep the fixed date;
the rows given to LedgerSeries take the clock's. go test -race
./internal/usage passes; gofmt and vet are clean.
|
||
|
|
e6dd5d20bb | usage, gateway, gui: a reply that reasoned is timed by its answer, from its first text, not by all its output from its first token (tony on Discord #general: Usage › Requests read "gpt-6.1-sol 163 token/秒 · 8,036 个请求 · 首字 14 秒" and asked whether subagents were added up) — speed was every output token over the time after the first content (text, reasoning or a tool call). Output counts the reasoning, and a model whose reasoning is hidden (OpenAI's encrypted reasoning, a summary sent when it is done, Claude's omitted thinking) writes it before the stream shows anything, inside the TTFT; so a 2,000-token think and a 200-token answer over the 1.5 s after the first token read 2,200 in 1.5 s, 1,467 tok/s. Concurrency was not the cause: the aggregate is Σ tokens over Σ per-request windows, so subagents running at once never add up. In the owner's own usage.jsonl codex/gpt-6-luna turns read up to 600 tok/s (out 496, reasoning 224, 11,055 ms, ttft 10,228) and its aggregate 133; it is 80 now. usage.DecodeOf is the one rule: a reply with reasoning counts out − reasoning over ms − firstText, one that reasoned and wrote only tool calls tells no speed, one without reasoning is timed from its TTFT as before; DecodeWindow's burst cut (#731) still applies. Record.Decode feeds summarize, the ledger's Totals and its timeline Parts; a route keeps its reply's reasoning (Route.Reasoning, from gateway.go and codex_backend.go), and routing.js's decodeOf/speedOf times its rows, the story and a session's average by it (an older route takes its served usage's reasoning); app.js's ledDecode does the Usage page's rows, whose tooltip says the reasoning left out, and the Output speed help says the rule, in en, zh, ja and de. History is read the same way, as it keeps the reasoning and the first text. Routing never ranks by speed. TestReasoningReplyTimedByItsAnswer (the 2200/2000 turn, a summary 10 ms before the text, a concurrent subagent, tool calls only, reasoning over output, no reasoning, not streamed: 1,200 tokens in 11 s through summarize, addRow, LedgerSeries and Decode) fails without the change and passes -race -count=10; TestUsageKeepsTimeToFirstToken now has its paced replies report reasoning and checks the route keeps it. reasoning-speed.test.cjs (Routing rows 133, 100, none and 100, the story never 1,467, the session 105; the Usage rows, tooltip and KPI the same; no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de, and fails against the old assets (1467 tok/s); routing-request-metrics, routing-burst-speed, usage-speed, usage-ledger-detail, i18n-keys, gui-ja, gui-de, ledger-hscroll and click-scroll pass on both. go test -race -tags nogui ./internal/... under a temp HOME passes but internal/proc's TestUserPath ("the login shell's PATH never came"), which failed under the full run's load, passes 3/3 alone and is not touched here; go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with tony's data. docs/reference.md says how output speed is counted. | ||
|
|
0a49ffa9df | usage tests: TestOTelRetryAfterBound times the retries from the first attempt (#973) — on Windows every attempt came 30ms late in the synctest bubble: a settings file not there is waited on (steady, 1ms × 15 per path) before it is taken as gone, and the bubble's clock counts those sleeps, so the first attempt was at 30ms, not 0. The spacing between attempts, what the test is about, was exact. No app change. Passes on the Windows box and on the Mac (-race -count=5). | ||
|
|
88586975b2 | usage test: the append budget is what 8000 records add, not a fixed count (#972, TryWorld2026) — TestRequestPageAppendDoesNotRepriceHistory failed on Windows at 3005–3028 allocations against 3000. pprof of the measured call on the Windows box: the extra over the Mac (2076 → ~2475 there) is a fixed per-query cost, provider sign-in lookups reading the environment and registry (syscall.Getenv, UTF16FromString, registry.ExpandString, LoginPath), not per block, so it varies with the machine. The test now subtracts the same append+query over a 10-record history: re-reading the 8000-record history costs ~65000 more (8 per record), the incremental path ~720 (~50 per block of 500); the budget is 2000 more. The sealed-block pointer check (no re-pricing) is unchanged. No app change. With the log snapshot forced to re-read, the test fails (66865 vs 1448); it passes on the Mac (-race -count=20) and on the Windows box (count 10). The sibling budgets in incremental_log_test.go measure 528/633 and 50/52 (Mac/Windows) against 1500 and are left. | ||
|
|
c5b57b9077 |
gateway, usage, gui: a relay's reply of tool calls ended "end_turn" or "tool_calls" goes on to the calls, a tool_use whose start carries its input keeps it, an Anthropic stream that just stops is an error, and every request says its protocol and the upstream's stop reason (蓝猫 on Discord)
蓝猫's custom Anthropic-protocol provider in front of Grok ended the agent's
turn a few words in with no error, where Alma on the same endpoint ran on.
Replayed from a fake upstream, three ways an Anthropic-shaped relay's
stream was read wrong when magpie translated it for an agent of another
protocol:
- a reply of tool calls whose stop_reason said end_turn, stop or OpenAI's
tool_calls was handed on as finish_reason "stop": the agent ended its
turn instead of running the calls. stopFromAnthropic takes tool_calls,
function_call, length and content_filter; collector.finish makes a
"stop" beside tool calls "tool".
- a tool_use block whose content_block_start carries its whole input,
with no input_json_delta after, lost the input (the call went with {}).
anthropicDecoder emits it, and skips deltas for that block after.
- a stream that ended cleanly with no stop_reason and no message_stop was
finished as a whole reply. streamTranslated now takes it as cut: asked
to go on where it can, else ended with "the reply ended before it was
complete", as the passthrough already did.
Each request now keeps the upstream's own stop reason (Usage.Stop /
usage.Record.stop: stop_reason, finish_reason, a Responses status), from
the passthrough's sniffer and the translated stream, shown as "Upstream
stop reason" in a request's details on Usage › Requests. Each row there
is tagged with the protocol it was sent upstream in, "Chat → Anthropic"
when translated, told from its endpoint, so older rows have it too; the
details say Protocol. en/zh/ja/de, no left border, no select, a click
moves nothing.
relay_stop_test.go's four tests fail without the change (finish_reason
"stop" for tool calls under end_turn/tool_calls/stop, arguments "" for a
whole-input start, a cut stream finished "stop", no stop recorded) and pass
with it; the passthrough case sends "data:" without a space and CRLF.
ledger-protocol.test.cjs passes in Chromium and WebKit, en and zh.
go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test
-tags nogui ./... under a temp HOME pass.
|
||
|
|
71d2da1f42 |
catalog, usage, gateway, gui: a price can have a long-context tier and a 1-hour cache write, and the price boxes take decimals (PAMI on Discord)
OpenAI bills gpt-6-astra, gpt-6.1-sol and gpt-6-luna over 272K input at 2× input and cache and 1.5× output, for the whole request. A price now has tiers: `magpie model price <m> 10,50,1,12.5 --tier 272k 20,75,2,25` (--tier can repeat). A tier applies when input + cache reads + cache writes is over its size, so 272,000 is not over and 272,001 is. The built-in prices take tiers only where models.dev lists one (cost.tiers of type "context"), and it does list one for those three. The ledger, each request and OTLP are costed at the tier the call reached. Session and day totals sum many calls, so they stay at the base price.
Anthropic bills a cache write kept for an hour at 2× input and one kept for 5 minutes at 1.25×. Its usage reports which is which in cache_creation.ephemeral_5m/1h_input_tokens. The gateway (API and Claude subscription) and Claude Code's transcripts now keep that split. A price's fifth part is the 1-hour write. When it isn't given, it is 2× input: that is Anthropic's rule, and only Anthropic reports 1-hour writes. Claude list prices carry the 2× too. cache_write_tokens is still the total, and the usage CSV adds cache_write_5m_tokens and cache_write_1h_tokens at the end. The app shows the split on a request's Cache and on the usage tooltips. Old prices and old records, which have no 1-hour part, read as before. The sessions cache (18) and the call cache (calls-v8) are re-read once.
In the GUI, the price boxes were number inputs and dropped what they didn't parse ("0,25"). They are now decimal text boxes that take both "0.25" and "0,25". A model's price row has a 1-hour box (empty means 2× input) and a quiet Long-context price row. That row shows the list's tier greyed in, or sits behind a link when the list has none. A bad size is refused by name. en/zh/ja/de.
Tests:
- New: TestPriceTiers, TestCostSplit, TestPriceJSON, TestModelPriceTiers, TestModelPriceTierCmd, TestAnthropicCacheWriteSplit and TestClaudeCacheWriteSplit. The ledger CSV test expects the new columns.
- model-price-tiers.test.cjs is new; model-price-edit and provider-price-rate are updated. All of them fail against the old assets and pass in WebKit. Chromium couldn't be launched here.
- go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
|
||
|
|
5604c411a9 | usage, gui: Usage › Requests' All opens at once after magpie has been up a few seconds, and in half the time before (the owner) — Requests on All stayed a skeleton for 39 s on the owner's magpie right after it started: the first All parses every session file (12.3k here: 11k Codex sessions, 1.2k Claude Code projects' files), one after the other, each a wait on the disk. Those a query has to parse are now read 8 at a time ahead of the loop that takes them in order (prefetchSources), at most 32 waiting, so the history is never all held at once; and magpie reads the whole history in the background once it is up, right after the Sessions page's own warm-up and not beside it on the disk, so the page opens on the kept index. Tested in a sandbox HOME on a copy of the owner's real ~/.codex and ~/.claude history (magpie app, dev UI): the first All right at startup took 10.6 s before and 4.6–4.9 s now; opened 20 s after startup it took 10.0 s before and 0.01 s now; later Alls 0.3–0.4 s as before. TestPrefetchSources (each source handed back as its own, read in parallel, no more than the window ahead, every one read once, an unwanted one read when asked) is new. go vet, go test -race ./internal/usage, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. | ||
|
|
f506d08d28 | usage, gui: the Requests tab ranks one model at each provider it went to (inaction on Discord) — the trend's ranking already gave each model its speed, first token and requests, but "Model" put a model's requests at every provider together, so glm-5.3 at Zhipu and at ZCode couldn't be told apart. The split has "Model · provider" now: a "modelAt" dimension (provider/model) whose rows are named by both with the provider's icon, each with its own tokens/s, first token and requests, ranked by any metric (Speed: the fastest first). A click lists that provider's requests of that model, again all of them; the others stay ranked. The trend's bar wraps at the narrowest window, "Split by" with its choices. The key of each provider and model is built once per page (TestRequestPageAppendDoesNotRepriceHistory's allocations). TestRequestPageRanksModelAtEachProvider, TestLedgerNamesModelAtProvider and usage-model-at-provider (WebKit, en and zh) fail without the change. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestUserPath failed under load and pass alone). | ||
|
|
e957230629 | fix: keep purpose-filtered routing state consistent (#784) | ||
|
|
e5981040dc | gateway, gui: a request an aggregator passed on says which provider answered it behind it, "Upstream: DeepInfra" (leslie_luo on Discord: 像 CPA 那样指出实际供应商) — through OpenRouter a request was only "OpenRouter · deepseek/deepseek-chat", with no word of the provider (DeepInfra, StreamLake, Novita…) OpenRouter had sent it on to. OpenRouter names it in its replies, as tried with the owner's real key: Chat's body and every chunk of its stream have a top-level "provider", Messages' body has one and its stream's message_start a message.provider; its Responses replies and every reply's headers name none. The gateway now reads it (upstreamOf) from a body relayed as it came and from each event of one it translated, Chat, Messages or Responses, streamed or not, and keeps it on the request's try and route (upstream) and in the usage ledger (Record.Upstream; the CSV export's last column, searched as the rest). The Routing page's Requests list says Upstream: DeepInfra beside where it went, giving way after the provider's name when short of room, with its story saying the aggregator passed it on; a request's details on the Usage page have Upstream provider. A reply naming none says nothing. zh, ja, de strings. ClinePass and Cline weren't tried (no account); Vercel's gateway documents no such field in its replies, and a vendor whose reply has a top-level "provider" string is read the same way. In a sandbox magpie with the owner's real OpenRouter key: Chat, Messages and Responses (translated to Chat), streamed and not, all six said StreamLake in the trace, the ledger and on the Routing page. TestUpstreamRecorded (6 paths × streamed or not: the route, its try, Recent and the ledger) fails on the old code, TestUpstreamOf; routing-upstream.test.cjs (Chromium and WebKit, en and zh: the tag inside its row, the story, the Usage details, a click moves nothing, no left-border accent) fails on the old assets. ledger-hscroll, request-archive-usage, usage-ledger and usage-ledger-detail pass on WebKit; routing-served fails the same on origin/main (its swap wording). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass but for TestTranslatedStreamKeepsClientAlive, which passes alone 5 times here and on origin/main. | ||
|
|
5464a362f6 |
usage: the Requests tab says how fast replies came, and fits the reporter's window; a wide window keeps Agents and Settings readable (huoranxuanyuan, #860)
The ledger kept each reply's duration, first-token time and output tokens, but the page only showed a duration. Speed is now shown in four places, measured as output tokens a second after the first token (usage.DecodeWindow, the same guard Routing's tok/s uses): - Each row has a Speed column. It shows "—" for a reply that wasn't streamed, and the row's details say it too. - A fifth KPI, Output speed, gives the period's speed with "first token in … on average". - The trend has a Speed metric. Each model, provider or agent gets a mark at its speed for every hour or day, with a faint column for the hour's overall speed. The tooltip gives each one's tok/s and TTFT, and the ranking sorts fastest first. - The ranking shows each one's tok/s and TTFT under the other metrics. usage.Part (a series point's per-model, per-provider and per-agent sums) now carries timed, ttft_ms, decode_ms and decode_out for this. Narrow window: the reporter's window is 975px wide (1220 at 125%). The table was 1222px in a 941px box, so the Status and Speed columns needed scrolling sideways. Where the table doesn't fit, it now leaves out Sent, Effort and the two cache columns. A line under the totals says those are in each row's details, and the details list them. The check runs again when the width changes or the table grows (a font arriving). At 975 the table now fits. Wide window: at 1670px the Agents, Providers, Gateway, Library, Plugins and Settings rows stretched to 1636px. They now stop at 1200px and are centred. Routing, Usage and Sessions keep the full width. zh, ja and de strings. Tests: - usage-speed.test.cjs (WebKit, en and zh) fails on the old code (no Speed column). At 975×778 it checks: the Speed column (75 tok/s, 10, "—"), the table fitting with no sideways bar, the hint, and Sent and Speed in the details. It also checks the KPI (72 tok/s, first token in 600 ms), the ranking's tok/s, the Speed trend (fastest first, a mark per model per hour), and Agents and Settings rows at most 1200 wide and centred at 1670. - TestLedgerSeriesSpeed. - usage-ledger, usage-ledger-detail and ledger-hscroll updated for the new column and details. ledger-hscroll now runs at 700px, still narrower than the tight table. Results: usage-*, ledger-hscroll, agent-*, settings-*, click-scroll, gui-ja, gui-de, import-row, member-model-setting, routing-sessions, routing-columns and sessions-overview/toolbar pass on WebKit. remote-group-served's zh run timed out waiting for Routing's By request tab in 2 of 9 runs and passed in the rest. go vet, the builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. |
||
|
|
4a87b30610 | usage, gateway: a model the vendor spells with other separators isn't marked as another one (vincentzhang on Discord: 这个说明有点奇怪。看不明白。) — Volcengine Ark answers deepseek-v4.1-flash as deepseek-v4-1-flash, the dots written as hyphens, and the Usage page's Requests and the Routing page marked every such row amber 「实际 deepseek-v4-1-flash」, saying another model had answered, not the one asked for under a dated name. Swapped now compares the bare names without their separators (. - _ and spaces) and case, so deepseek-v4-1-flash, deepseek_v4_1_flash, GLM-4-6 for glm-4.6 and gpt-4-1-2025-04-14 for gpt-4.1 are the model asked for, while deepseek-v4-flash or deepseek-v4.1-pro for deepseek-v4.1-flash still are another. Rows and routes kept marked swapped before are read back as not swapped when the names differ only so (usage.SameSpelled, in rowChunk.row and Route.routedAgain). The tag's explanation says it in plainer words: asked for {sent}, the reply says {served} answered, likely another model; the same model under a dated name or spelled otherwise isn't marked (zh, ja, de). TestSwappedIgnoresSeparators fails on the old code; TestKeptRowSameSpelledNotSwapped, TestRouteKeptSpelledOtherwiseNotSwapped. usage-ledger (its expected title updated), remote-group-served, panel-routing, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestCodexVersion timed out once under load and passes alone). | ||
|
|
258eae592d |
agent: Cursor Private Inference, which magpie finds by its product.json and starts on magpie with a command its row copies, and whose /models gives each model's api_types and capabilities (#299, mamba on Discord) — the build (Cursor 3.23.x, the same app name, bundle id and ~/.cursor as Cursor) runs its agent locally and asks the endpoint in CURSOR_LOCAL_AGENT_BASE_URL / CURSOR_LOCAL_AGENT_API_KEY when its Open configuration has none; mamba confirmed it works with any account pointed at magpie. magpie writes nothing of Cursor's (its settings share Cursor's state.vscdb): the Agents page lists it when an app's product.json says "Cursor Private Inference" (/Applications, ~/Applications; %LOCALAPPDATA%\Programs on Windows; /opt and the like on Linux), and its row's one control, Copy launch command (复制启动命令), copies CURSOR_LOCAL_AGENT_BASE_URL=<gateway>/v1 CURSOR_LOCAL_AGENT_API_KEY=magpie-cursor-local '<app>' (PowerShell on Windows); in the tray panel the row copies it. Its notice says to quit it first (a second start hands over to the running one) and that a base URL in Open configuration comes first. /v1/models asked with its key gives each model api_types (the APIs its provider serves it on, all three for a group or a translated one; it asks Anthropic Messages only when no OpenAI one is listed) and capabilities (context_length, max_output_tokens, supports_vision); any other client's list is unchanged. usage.AgentOf matches a name before a UA prefix, so cursor-local isn't counted as Cursor. TestCursorLocal, TestCursorLocalModels and cursor-local-launch.test.cjs (WebKit, en and zh) fail on the old code; agy-launch and agent-* GUI tests pass on WebKit. Not tried with the real app (not installed here). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
|
||
|
|
21702d01ff |
Add German translation; harden paths against unset env vars (#770)
* Add German translation and harden paths against unset env vars Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows. * Makefile: know magpie's folder by its go.mod, so paths with spaces work The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs. * i18n: German for the strings main added since, with a parity test 75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system. * appdir: ignore relative folder variables when reading, keep them for children CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder. * Sandbox the tests main added since internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE. * agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over. * i18n: German for the Library's multi-select and System One's decision API The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes. * appdir: tidy the import into magpie's group; say what Go makes of /c/x The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x. |
||
|
|
7909de22ac | 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. | ||
|
|
57e80efbf4 | feat: filter usage and routing requests by purpose (#771) | ||
|
|
6f64956f27 | routing: a group can let a member go that hasn't begun answering after a set time and ask the next, and a request's detail splits its first token into magpie's time and the vendor's (Chen on X: answers through magpie felt slow, with no way to tell whether magpie or the vendor was the wait) — a group's editor has a Slow to start row (Wait, 30 s, 1 min, 2 min; Wait, the default, keeps today's behaviour). With a time set, a streamed request to a member that has sent no text, reasoning or tool call by then is cancelled before any of its bytes reach the agent (the held start is dropped; keepalives go on meanwhile) and the next member is asked; the try is marked slow ("slow to start", narrated in the route), and the member doesn't rest, since nothing is wrong with it. The last member left is always waited for. The card says "Next after 1 min without a first token". Each request now records when it went to the vendor (httptrace WroteRequest, sent_ms), and its detail shows In magpie and Vendor's first token beside First token; after a failover In magpie includes the time spent on the members let go. With the owner's real DeepSeek key in a sandbox, a group [a local member that sends its role chunk then stalls 60 s, deepseek-flash] with 3 s: the answer came from DeepSeek in 4.4 s, none of the stalled member's stream reached the client, its try read slow / "no first token in 3s" with no rest, and the ledger had ttft 3622, sent 3003. TestGroupFirstTokenFailsOver (failover, the last one waited for, one in time kept; passes -race) and group-first-token.test.cjs and usage-ledger-detail.test.cjs (WebKit, en and zh) fail on the old code. group-from-model-add's narrow-window case fails the same on origin/main; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone). | ||
|
|
91fbf15fec | usage: a reply that came in one burst at its end tells no tok/s, and is left out of the summed speed (#731, Arcadia822: a Gemini turn through Antigravity that was one write tool call, out 8264, ms 24360, ttft_ms 24359, read 8,264,000 tok/s and lifted the Usage page's average) — a reply's speed is out ÷ (ms − ttft_ms) since #196, and the sums are Σout ÷ Σ(ms − ttft_ms) (Totals.DecodeOut/DecodeMs). codeassist hands a native functionCall over whole, so a turn whose only content is one big tool call has its first content event a millisecond or two before the end: the window covers delivery, not writing, and its tokens over next to no time read millions a second and pull the summed speed up (the test's three calls: 100 tok/s each in truth, 616 with the burst counted). A decode window now counts only when it timed the writing: usage.DecodeWindow gives 0 when it is under MinDecodeMs (100 ms) or the tokens over it would be past MaxDecodeSpeed (10,000 tok/s), both far from any real stream (the fastest vendors write a few thousand a second); Totals.add (records) and Totals.addRow (ledger rows, which the Usage page sums) both go through it. Such a reply still counts as timed and its TTFT in the mean; only its speed is left out, so a model with only bursts shows no tok/s (the Usage page and the CLI already skip a speed with no decode window). The Routing page's story, which works out a try's speed itself (routing.js speedOf), applies the same bounds, so the burst says "first token in 24 s" and no tok/s. app.js's figure is decode_out ÷ decode_ms from the Go sums and needed no change. Tests: TestBurstTellsNoSpeed (the report's record with two 100 tok/s calls: 100 tok/s summed through summarize and addRow, TTFT mean keeps the burst; DecodeWindow's edges: 1 ms, 0 ms, 3 tokens in 50 ms, 8264 in 500 ms, exactly 10,000 tok/s over 100 ms, nothing written, not streamed) fails on the old rule with 616 tok/s; TestSummarizeTimesFirstTokens unchanged. routing-burst-speed.test.cjs (webkit, en and zh: the report's burst and a 500 ms one tell no speed, 100 and 50 tok/s replies keep theirs, picking a request doesn't move the page) fails on the old routing.js with "8264000 tok/s". routing-sessions, routing-served, routing-effort-change, usage-accounts, panel-usage and click-scroll GUI tests pass in webkit (routing-served failed once in a parallel run and passes on its own twice; its fixture has no TTFT). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with the reporter's ledger or a live Gemini stream; a short reply under 100 ms (a title, a few tokens) no longer counts toward the speed either, which moves a summed figure by next to nothing. | ||
|
|
cab8a8e450 | fix: refresh first-request discovery and retain late Pi timing (#708) | ||
|
|
d079dc6223 |
usage: an OpenCode session's row shows the effort and the model it asked for (#680, Sun1090: 数据不完整,推理强度是有的)
OpenCode rows read from its own data (opencode.db or storage/) had 请求模型 and 推理强度 as —. OpenCode keeps both on each reply (packages/schema/src/v1/session.ts, AssistantMessage): variant, the effort picked in its model menu for the prompt (high, max, low, xhigh, none, thinking … as the provider's variants name them; prompt.ts copies lastUser.model.variant onto every reply), and modelID, the model the reply was sent to. OpenCode 2 (session_message) keeps the variant in model.variant (Model.Ref). It records no model the response named, so modelID is what was asked for, as a Codex rollout's model is.
Now a call's Effort is the reply's variant ("default" or none is no effort picked); a compaction in OpenCode 2 takes the effort last replied with, as it takes the model. In the ledger an OpenCode call is told as a Codex one: requested = its model, served left empty rather than claimed. The call cache moves to calls-v5 so calls read before carry the effort.
TestOpenCodeCallsJSON/DB/V2 (fixtures with variant high, default, xhigh and none) and TestOpenCodeSessionCallIsWhatItAskedFor fail on the old code and pass. go vet ./internal/..., builds (nogui, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real OpenCode database with variants (this machine's OpenCode data predates them).
|
||
|
|
c9d0d15b94 | fix: 记录网关实际返回的响应 ID (#693) | ||
|
|
ee68b96788 | fix: avoid duplicate Pi interaction and gateway traces (#692) | ||
|
|
d5591b8f8d | usage: export effective model costs to Langfuse (#684) | ||
|
|
48d524d65a | feat: select a day in usage charts (#683) | ||
|
|
2f937f3f39 | fix: defer unfinished session trace roots until completion (#633) | ||
|
|
7d7175cfb1 | fix: preserve Langfuse coverage for first and auxiliary calls (#599) | ||
|
|
5422306fac | usage: guard cache transitions and Windows long-path reads (#598) | ||
|
|
38b4a9d984 | feat: export gateway and agent conversation waterfalls to Langfuse (#581) | ||
|
|
3d8c6f24c5 |
usage: capture Windows change time for cached logs (#594)
* usage: capture Windows change time for cached logs * ci: show native Windows usage test outcomes |
||
|
|
f1ca6139b5 | usage: a Codex call through the gateway that wrote to its upstream's cache is no longer listed a second time as a local session call (laowangba233 on #589: 用量中的统计是不是重复了 … 为啥会一个是提供商的一个是本地会话的) — the Usage page drops a Codex rollout's call when it matches a gateway record by session, end time within 2s and equal tokens. Codex reads input_tokens_details.cache_write_tokens from a Responses reply and keeps it as cache_write_input_tokens; the gateway's rUsage read only cached_tokens, so for a call whose upstream told a write the record had CacheWrite 0 and the rollout didn't, the tokens never matched, and the call stayed listed under Local session beside its provider's row. Calls without a write matched, so only some calls were counted twice (Maru Code 92 calls plus 22 "本地会话" in the report). Also, sessions' spent took only the cache read out of Codex's input_tokens, which holds the write too, so such a call's write was counted twice in its own input. Now: the gateway reads cache_write_tokens (input = input_tokens - cached - written) and tells it to Codex when it translates a reply; spent takes the write out of the input as well; and the matcher compares a call's input plus cache writes, its output and its cache reads, so a record and a rollout that split the write differently — an older Codex without the field, or records already in usage.jsonl from before this change — still match, and the duplicates already on a user's page go too. The session caches' versions go up (sessions.json 11, calls-v4) so kept parses are read again, and older calls-v* folders are removed. Under a provider filter the provider ranking still lists the other providers, as it is a picker; the duplicates were what made it read as double counting. TestCodexCacheWriteMatchesGatewayCall (a Codex rollout and gateway records for two calls, one with a 9000-token cache write; with the rollout naming the write and the record not, both naming it, and an older Codex without it: two rows, no Local session row, the provider ranking maru-code alone, unfiltered and filtered to the provider, by QueryPage and LedgerOf; fails without the change with a third row from the log), TestCodexCacheWriteNotInInput and TestResponsesUsageCacheWrite (parse and round trip through what Codex is told) are new and fail without the change; TestCallsCodex's call with a write now expects input 200 (2500 - 2000 read - 300 written). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside, which passes alone). | ||
|
|
0879894af3 | gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside). | ||
|
|
e41cacb42a | fix(gateway): account for streamed failures (#573) | ||
|
|
7464ee5075 | usage: bound retained chunks and invalidate same-stat rewrites (#563) | ||
|
|
66844ab0f9 | usage: cover growing rewrites followed by local and external appends (#559) |