Commit Graph
32 Commits
Author SHA1 Message Date
yetone febbf29b0f usage: an Antigravity call at a level answered under its model's own name is no swap (#462: gemini-3.8-flash at medium marked Swapped, answered by gemini-3.8-flash) — on an Antigravity account a call at an effort goes out as that level's id (AntigravitySentID: gemini-3.8-flash-medium), and Cloud Code's reply names the family in modelVersion (gemini-3.8-flash), so Swapped compared gemini-3.8-flash-medium with gemini-3.8-flash and the request list and the Routing view tagged the model as swapped for itself. A sent id whose last word is an effort word (provider.EffortFamily: low, medium, high, xhigh, minimal, extra-low, extra-high) now matches a reply naming the id without it; a reply naming another level, or another model, is still a swap. TestLedgerAntigravityReplyNamingTheFamilyIsNoSwap fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, a load flake, passes alone). 2026-10-02 02:24:48 +08:00
Aliceandraper 0260716c9d feat(usage): 可选的 OTLP 元数据导出(Trace/Metrics,支持 Langfuse) (#412)
* usage: add opt-in OTLP HTTP metadata export

Export gateway traces and optional duration/token histograms asynchronously with a bounded queue, retries, proxy support and a bounded shutdown. Add bilingual Settings controls and MAGPIE_OTEL_* overrides. Keep prompts, replies, sessions and provider credentials local; use the same OTLP path for Langfuse.

Validated: full Go tests (nogui), usage race tests, focused go vet, and Chromium/WebKit settings tests in English and Chinese.

Refs #213

* otel: protect collector credentials in backups and settings

Remove all OTLP headers from keyless backups and restrict both new and existing settings files to 0600 before saving. Clarify SIGTERM export behavior. Addresses the two blocking review findings in #412.

* otel: preserve local credentials and read-only settings on restore

---------

Co-authored-by: raper <raper@raperdeMacBook-Air.local>
2026-10-02 01:51:04 +08:00
yetone 3c04877cfc archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass. 2026-10-02 00:57:51 +08:00
yetone 0361189f9e test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass. 2026-10-01 23:15:07 +08:00
Ns2Kracy bb1aa3acbd feat(gateway): named gateway keys with per-client usage (#344)
* feat(gui): show and filter usage for each API key

* feat(gateway): issue per-user caller keys and attribute usage

* feat(gui): manage gateway users and filter caller-key usage

* test: verify caller-key failover and document user management

* refactor(gateway): use named caller keys without user accounts

* feat(gui): manage named caller keys with existing account styles

* fix: remove user breakdown from CLI and leftover gateway styling

* fix(usage): drop stale user field from request response

* feat(gateway): authenticate LAN calls with named caller keys

* fix(gateway): preserve LAN key rotation in the named caller store

* fix(gui): retain native LAN key copy and rotation controls

* fix(gui): inherit the global theme for API key inputs

* fix(usage): show client API keys using the existing terminology

* fix(gateway): rotate named keys without changing usage identity

* fix(gui): unify gateway keys and selectable connection examples

Keep sharing controls in Settings and manage credentials only in Gateway. Refresh the named-key list on navigation, ignore stale reads after key actions, and generate all API examples from the selected address and enabled key.

* fix(gui): keep caller key names literal in connection menus

* fix(access): use Magpie as default key and drop user migration

* refactor(gui): name caller key source and tests consistently

* refactor(usage): split provider-key accounting out of caller-key PR

* fix(gateway): preserve LAN compatibility and add headless key management

* fix(gui): clarify gateway keys and confirm destructive actions

* test(gateway): guard caller attribution under 120 concurrent requests

* fix(gateway): keep open access, tolerate migration errors and revoke legacy keys

* fix(backup): preserve named gateway keys and honor no-keys restores

* fix(gui): keep local API-key semantics and disambiguate shared credentials

* docs(gateway): keep review scope focused and append browser test documentation

* fix(backup): retain existing default keys when old backups lack their store

* fix(gateway): attribute video creation usage to the caller key

* fix(access): explain default key revocation blocked by read-only settings

* test(usage): preserve provider and caller identities after rebase

* test(gui): reconcile caller key fixtures with merged main

* docs(usage): clarify caller coverage and independent provider accounting

* fix(gui): initialize fast members for model-shortcut groups

Latest main introduced fast-member arrays but omitted the model shortcut's draft. The existing unlisted-models browser regression fails unchanged on main. Initialize fast to an empty list and assert a standard-speed group is saved, without relaxing existing assertions.

* fix(access): retain legacy LAN access when the key store cannot be created

Allow only the configured legacy credential before either migration marker exists. Refuse revoked mirrors, disabled matches and unreadable stores; keep the fallback's caller identity stable when migration later succeeds. Cover read-only configuration directories, remote credential transports and revocation.

* fix(backup): save settings before replacing gateway credentials

Leave the key store untouched when settings validation or writes fail. Restore the previous settings association when the key store refuses its write, and report a rollback failure separately. Cover invalid settings and read-only settings/store failures.

* fix(gui): reset missing caller filters and focus key controls without scrolling

* test(provider): synchronize Zhipu quota fixture observations
2026-10-01 21:28:54 +08:00
Zhu Wang 1adb2b571e feat(routing): group requests by session with names and estimated costs (#396)
* feat(routing): group requests by session with names and estimated costs

* fix(routing): address session view review feedback

Preserve Luna Reserve and other header fallbacks for ordinary Codex turn metadata; reuse metadata and the kind across trace and native accounting. Default to By request and persist the grouping choice. Store only token accounting fields in route history while retaining legacy JSON compatibility.

Add regressions for helper precedence, native kind accounting, history compatibility and grouping preferences.
2026-10-01 20:50:15 +08:00
c0f3fa012a usage: combine gateway and local session requests with model charts and account labels (#239)
* usage: Requests lists the agents' own session-file calls, put to the vendor and account their files prove, charts them by provider, agent or model, shows what was said, and the tray panel gets a Usage tab

The Usage page's Requests tab now covers requests an agent sends with its own sign-in (Claude Desktop's Code tab and Cowork, Claude Code, Codex), which never pass the gateway, and shows more of every request: why it failed, what was said in it, who it was of.

Requests tab: a strip of four totals (tokens, requests, cost, cache hit rate); the trend of one metric — tokens, cost or requests — as columns by the hour, day or week, told apart by provider, agent or model (both remembered), beside a ranking of the same that is its legend and a way in (a click lists only that provider's or agent's requests; a provider picker sits beside the agent's). The ranking is as tall as the chart and scrolls in itself, with no scrollbar: a slim rail in the card's margin shows how much there is and where it is (its thumb dragged, the rail clicked), and the edge with more beyond it fades; under the chart (860px and narrower) it shows whole. A row opens to its details: the vendor's error type and words, request id, endpoint, and what was said in it (input and output, read from the agent's session file when opened, the call that ended nearest the request — nothing stored). A refresh period (off / 5 s / 10 s / 30 s / 1 min) and refresh-now. The Overview says it counts gateway calls only; Requests and the panel say they count both. The tray panel gets a Usage tab (today / 7 / 30 days, provider picker, four totals, a small chart, top five, "Open Usage"), and is as tall as its tallest tab (#124).

Provider and account of a session-file call: never magpie's "claude" or "codex" subscription. Anthropic's when Claude Desktop's session metadata (not Claude-3p) names the account and organization, with that account's email; OpenAI's when Codex's rollout names a provider that is OpenAI's in config.toml — the built-in "openai", or a named one at OpenAI's address or signed in with the ChatGPT account and no base_url (requires_openai_auth) — with the creator account's email when the file records it. Anything else is "Local session", its recorded provider id and session account kept (CSV: session_provider, session_account). The config read is today's: a file names the provider id only.

Gateway: a failed call keeps the vendor's error text and type, request id and endpoint; a Claude subscription's failure uses the status Claude Code reports (api_error_status). A request turned away before any vendor was asked is logged, marked rejected, listed in Requests and left out of every total. The client's own session header is kept as native_session beside X-Magpie-Session, and a session-file call is matched to a gateway record by request id, then by that session, equal tokens and an end within 2 s, only when unique both ways.

Reading: session-file calls live in the session index main keeps on disk, parsed incrementally and outside its lock (Claude Code, Cowork in both Claude folders, Codex); the usage log is read on from where it was; a page builds the ledger once. Days are counted on calendar dates (DST). CSV gains request_id, endpoint, error_message, error_type, source, session_provider, session_account. i18n: one "Endpoint" (终结点), which a second key had turned for Azure's field.

Tested: go vet ./..., go build (also GOOS=linux/windows, -tags nogui) and go test -tags nogui ./... pass. The whole GUI suite in Chromium and WebKit, en and zh (Playwright 1.62.1): all 393 tests pass. On this computer's files (7 days): 3,330 Claude Desktop calls resolve to Anthropic with the account, 1,991 Codex calls on a named ChatGPT-signed-in provider to OpenAI (620 with the account; older rollouts don't record it), 9 Claude Code CLI calls stay Local session.

Co-authored-by: yetone <yetoneful@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* usage: a session-file call of one of magpie's own Codex or Claude accounts is told as that provider, beside the gateway's calls on it — "OpenAI" and "Codex" (or "Anthropic" and "Claude Code") read as two vendors when they were one subscription

Codex: an official route (above) whose creator account and user are one of magpie's Codex sign-ins (the CLI's own or a saved one) is magpie's "codex", with that account's email as the gateway's rows have it. Claude Desktop (not Claude-3p): an account and organization that are one of magpie's Claude sign-ins is magpie's "claude". Only while magpie has that provider signed in; another account, a rollout with no account recorded, or an ambiguous workspace stays OpenAI or Anthropic, and a relay stays Local session.

Tested: TestSessionCallsOfMagpiesAccountsAreItsProviders (the account matched, another account, none recorded, magpie's account through a relay, Desktop in magpie's and another organization, Claude-3p, magpie signed out) and TestLedgerTellsCodexSessionCallsAsMagpiesCodex (a gateway Codex call and a session call of its account one provider of two calls, another account's OpenAI). On this computer (7 days): 3,439 Claude Desktop calls are magpie's Claude, 626 Codex calls its Codex, 1,322 older Codex rollouts with no account stay OpenAI. go test -tags nogui ./... and the usage GUI tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: 修复请求统计缓存膨胀与会话账号误归属 #239

调用明细改为按会话文件分片保存并限制内存缓存,汇总索引仅保留统计数据;网关日志按需读取。

严格匹配订阅账号与用户 ID,排除自定义渠道、非官方端点覆盖、外部 CODEX_HOME 及关闭的供应商。补齐零 token 失败去重、Codex 原始会话 ID、拒绝标记导出和面板显示修复。

通过全量 Go 测试、静态检查、受影响模块的并发检查、52 项浏览器测试及 macOS/Linux/Windows 构建;使用 35 万条合成调用验证索引体积与内存改善。

* fix: 降低请求统计刷新与写入开销并明确本地账号展示 #239

请求分片改为追加帧和字符串去重,分页复用未变化的紧凑快照,采用 LRU 并移出全局锁内的磁盘读取。缓存 Cowork 目录,补齐同前缀改写检测及崩溃临时文件清理。

本地记录仅显示匹配账号或本地会话,明确官方登录标记与请求路由的区别。默认今日与模型维度,Auto Review 复用共享目录定价并单独标注估价参考。

完整 Go 测试、vet、相关 race 和 52 项浏览器回归通过;提供 2000 文件及四个大会话连续追加的可复现性能对照。

* fix: 优化用量徽标与耗时分级并保护分页边界 #239

官方登录、API、订阅和本地会话采用独立颜色与柔和底色,兼容明暗主题。耗时按 10 秒及 30 秒阈值着色,保留估算标识与原始毫秒提示;无有效耗时显示短横线。

限制分页选取范围,避免超大偏移整数溢出及越界空页的无效堆构造。补充阈值、徽标对比度和极端分页参数回归。

* fix: 将账号与来源徽标分为两行展示 #239

* fix: 提高用量表格模型文字对比度 #239

发送模型和已知实际模型使用主题正文次级色,表头与时间同步加深。保留缺失值弱化、转发提示和本地日志来源说明。Chromium 与 WebKit 的中英文、浅深色主题 10 项界面检查通过。

---------

Co-authored-by: yetone <yetoneful@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:42:10 +08:00
Aliceandraper 02d501894b usage: link request accounting to routing history (#361)
Co-authored-by: raper <raper@raperdeMacBook-Air.local>
2026-10-01 16:18:02 +08:00
Ns2Kracy c900734020 feat(usage): account for upstream provider keys (#364)
* feat(usage): attribute gateway usage to individual API keys

* refactor(usage): keep provider accounting focused on CLI and CSV

* fix(usage): attribute System One calls and name upstream key fields explicitly

* fix(usage): leave keyless System One requests unattributed
2026-10-01 14:33:18 +08:00
yetone 5fe0906907 gateway: a remote magpie's routing group answering with the member it routed to isn't a swap (莫 on Discord: 发送模型 group/auto-deepseek-v4-1-flash, 实际模型 deepseek/deepseek-v4.1-flash shown amber) — a request for one of another magpie's groups ("group/<id>", the name only a magpie answers to) comes back with the model its member's vendor named, and the Usage page's Requests and the Routing page marked that as the vendor serving another model. Now it is routed, not swapped (usage.GroupRouted): the try, the route and the ledger row say so, the served member shows plain, muted on the Usage page, with a title (and a line in the request's story) saying the remote magpie's group routed the request to it, in English and Chinese. Rows recorded before are judged again as the ledger lists them, and routes kept in the Routing page's history as they are read. A real swap through a remote magpie, and a swap within a group of this magpie's own, are still marked; a swap behind the remote's group shows on that magpie's own Routing page. Checked end to end with two sandbox magpies over a fake vendor: the local ledger and routing history mark group/… false (swapped before the change) and fake/sol served as luna true. TestRemoteMagpieGroupNotSwapped, TestRemoteMagpieGroupLedgerRow, TestRemoteMagpieGroupHistory and remote-group-served.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (load flakes pass alone); routing-served, usage-ledger, panel-routing, remote-magpie, click-scroll, old-webkit pass. 2026-10-01 13:46:14 +08:00
Comic Changandyetone 024f494bc4 Allow provider-specific upstream model names (#280)
A provider may serve a model under an id of its own: namespaced with a
prefix, dated, a `-latest` that is not what models.dev calls it, or simply
a different spelling. magpie sends the name it knows the model by, and the
provider answers that it has no such model.

  magpie model wire relay-b/model-2 vendor-c/model-2-preview
  magpie model wire 'relay-b/*' 'vendor-c/*'
  magpie model wire relay-b/model-2 --reset

A `*` in the **value** stands for the model, so `relay-b/*` set to
`vendor-c/*` sends `vendor-c/model-3` for model-3. A value with no `*` sends
every model of that provider under one name, which is the case where a relay
really does serve them alike. The CLI shows the pattern the way it was
given, with a concrete line for one of the models, so what is in force is
not something you have to imagine. An exact key wins over the provider's.

The canonical id stays canonical. `Entry.Model` is not rewritten: it is the
key automatic routing groups are built from, what `Resolve` returns as the
model's identity, what `GET /models` lists, what an agent asks for, and what
a call is recorded and priced as. The alias goes into the request's model
field and nowhere else — which is the part that was wrong at first.
Handing it to the serializer gave the provider's name to the code that
decides what a request should look like: `buildChat` checks the model for
Gemini compatibility, `buildAnthropic` matches it for adaptive-only and
effort handling, `buildCodeAssist` maps by model. A provider serving a
model under a namespaced id got a request shaped as if it were that other
model. So the request is shaped from the model magpie knows and the field is
rewritten once at the end, with `rewriteModel` — which already existed to
put one provider's own model name in a body and keeps every other byte as
it was.

It has to reach every path. A request the provider understands as it came
is relayed by `passthrough` without being rebuilt, and the Anthropic token
count is asked of the provider directly; both rewrite the field too, so on
a provider speaking the client's own protocol — the common case for a relay
fronting one vendor — the alias still applies.

"Swapped" compared the canonical id with what the vendor's reply named, so
an alias that is not a prefix or a dated variant marked every call swapped.
It compares the name that was actually sent, in the gateway and in the
ledger alike; the ledger reads the map once for the lot, since its rows are
what a reader sets beside a bill. The Codex backend path is left alone: it
sends magpie's own pick and never an alias.

`provider.Test` asked with the canonical id, so a provider that only accepts
the alias reported a failure while its real requests worked. It asks with
the alias now and still reports the canonical id, since that is the one the
user picked.

A blank name was a hole: the setter trimmed what it was given, so a value of
only whitespace was removed on the way in, while the read path would have
used it had anything else written it — a hand-edited file, or a build from
before the trim. Both ends treat a blank as no name now. The key is
checked where it is written rather than in Save, so one hand-edited entry
cannot block every save, and #288's walk moves it on a provider rename
without a line here.

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-01 13:03:57 +08:00
yetone 49c98f40f5 gateway: a Remote magpie records the agent that asked on the other computer (Jorben on Discord) — requests forwarded to a remote magpie went as magpie itself, so the remote's usage and routing said "magpie" for all of them. The forwarding magpie now names the agent and itself (X-Magpie-Agent, X-Magpie-Via), read only from a magpie User-Agent and stripped before any vendor; the remote records that agent with Via, and Requests shows "Claude Code · via office-mac". It is only a label: models, routing rules and the drift check still go by the real sender. TestRemoteMagpieAgent, TestLastSeenNotVia and usage-ledger.test.cjs fail without the change. 2026-10-01 11:38:37 +08:00
yetone c641c1da2d usage: a built-in moved onto its plugin keeps one history per account (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — the built-in's calls were kept as going to "dee@example.com" (or "api.factory.ai as dee@example.com"), the plugin's as "kiro as dee@example.com", so Usage split an account's model rows at the move, the older half as if the id had gone elsewhere. A subscription's calls are now told apart by the account they went as, the host only for the rest; records already kept group together with no rewrite. TestSummarizeKeepsMovedAccountTogether fails without the change. go vet, go test ./internal/usage pass. 2026-10-01 11:15:22 +08:00
Comic Chang 901ee08280 Count usage at the configured provider/model price (#278)
Magpie reports models.dev's list price for every call. That is the right
number for a vendor's own key and the wrong one for a provider that
resells or exposes a model at a different tariff, and it leaves a model the
catalogue does not list with no cost at all — a provider's own /models
gives ids, not economics.

  magpie model price relay-a/gpt-5.5 0.12,1.20,0.01,0.15
  magpie model price relay-a/* 0.12,1.20,0.01,0.15
  magpie model price relay-a/gpt-5.5 --reset
  magpie model prices

Absolute USD per million tokens. All four parts are required: a price
missing one would understate the rest of every call, and a hand-edited file
that left a part out would bill it at zero, so each part is a pointer and
the setter refuses a price that is negative, NaN or infinite, naming the
part at fault. Zero stays distinct throughout — a model served at no cost
has a price, not the absence of one. A `<provider>/*` entry covers every
model of that provider and an exact entry wins over it; --reset removes
only the exact entry and says so when a provider-wide one is still in
force.

The order a price is looked for in is the price for this model, then
`<provider>/*`, then the provider's own catalogue, then models.dev for the
maker. A price is a provider's tariff, not the model's: the same model
through two providers has two prices, and each keeps its own. A provider is
resolved by its id, or by an id it was renamed from, and never by its
display name — Find answers those too, and a record naming one was counted
at another provider's price.

provider.EffectivePrice is the one lookup every cost caller shares, so the
usage ledger and a session's totals both go through it, and a session
naming a model bare is still priced at its maker's list price, which is a
different question from what one provider charges. catalog.PriceOf and
PricedBy keep returning the models.dev price for anything wanting an
unbiased number. The background-task pickers (draw, search, vision) still
rank by catalogue price: they choose which model answers, not what a call
is reported to cost.

Unlike every other per-model preference this does not call
catalog.Touched. What a call costs is not what an agent picks a model by,
and the model lists magpie keeps in the agents' own files are not its to
rewrite over a number in a cost report. A rename moves the prices with it
through the walk #288 adds, so nothing here has to say so, and an id
recorded before a rename still resolves.

Validation happens where the entry is written, not in Save: one hand-edited
bad price in the file must not make saving a theme or a window size fail. A
price for a model the provider does not serve is refused the way
`magpie model name` refuses the same id, since it is a price that never
applies and nothing reports it as dead; a removal is exempt, so an entry
left for a model that has since gone can still be taken away.
2026-09-30 20:31:11 +08:00
yetone ffa55fc277 copilot: Auto — Copilot's own pick, the only choice a Student plan has — is listed for every Copilot account and works through magpie, as Copilot's clients do it (#256; Discord, CakeJskSUMM: Copilot Student can only use Auto) — Copilot's /models doesn't list Auto; its clients add it themselves (Copilot CLI 1.0.79: {id:"auto",name:"Auto"}, AUTO_MODEL_ID) and ask POST {api}/models/session with {"auto_mode":{"model_hints":["auto"]}} for a session_token, the selected_model and expires_at, each request then naming that model with the token in Copilot-Session-Token. magpie listed no Auto, failed an account whose list left nothing to pick by hand ("Copilot lists no chat model for this account"), and sent "auto" as a model. Now copilot_auto.go keeps the session per GitHub account, asked again two minutes before it expires; the gateway resolves Auto before choosing the API, so a picked model served only on /responses goes there; a body still naming "auto" (the model test) is rewritten; the endpoints of every model Copilot lists, hidden ones too, are kept for the picked one. A reply to "auto" from another model isn't counted as swapped (Cursor's auto too); the Copilot CLI's picker, which offers auto itself, leaves out the listed one. Tested with fakes in a sandbox HOME: provider/copilot_auto_test.go (a Student-like account whose models are all disabled lists [auto], the session body sent as above, requests carry the picked gpt-5-mini and the token, one session reused, renewed near expiry, an object-shaped selected_model, a refusal reported) and gateway/copilot_auto_test.go (a chat request for copilot/auto goes to /responses as gpt-5-mini with the token, two turns one session; fails without the gateway change); served_test gains the auto cases. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Student account: what its /models returns, whether the plain session request suffices (Auto v2's /models/session/intent is not done), and the body is from VS Code's Copilot Chat, not captured traffic. 2026-09-30 10:17:15 +08:00
yetone 4f01baa65f catalog: fetched again each day magpie stays open, and sooner when a call's model has no price, so a model models.dev lists after magpie started is priced (#224 Koohoko: gpt-6.1-sol shows no cost) — the models.dev catalog, where list prices come from, was fetched only at the app's start (when older than a week), so a magpie left open, or magpie serve, never learned a new model's price. KeepFresh (internal/catalog/fresh.go), run by the app's backend and by magpie serve, looks every hour and fetches when the cache is older than a day; a call whose model has no price asks for a fetch (catalog.Missing, from usage's pricer) when the cache is older than six hours and none was tried in that time, only in a process keeping it fresh, so CLI commands and tests never fetch on their own. Loading and Reset now share a mutex instead of a sync.Once reassigned under a running Do, which a background Sync could hit while a call was being priced. Tested with fresh_test.go against a fake models.dev in a temp XDG_CACHE_HOME: stale after 25h and not before, Missing fetching nothing when not keeping, fetching once when keeping and gpt-6.1-sol then priced, and not again; passes under -race (failed under -race with the Once). go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. 2026-09-30 04:28:01 +08:00
yetone cf6b82f70d usage: a Codex, Copilot or catalog-less relay call is priced at its maker's list price, as a Claude account's is (#224 Koohoko: on the Usage page only Claude models show a cost; gpt-6-astra and gpt-6-luna through Codex's ChatGPT account and gemini-3.8-flash and gpt-6-luna through Copilot show tokens and no cost, and the Codex row reads ≈$153+) — the Usage page and the Requests list price a call by the models.dev ids of the provider it went to (Provider.Catalog), and only the Claude account had one (anthropic): Codex's and Copilot's accounts, like Grok's, WorkBuddy's and a relay added without a catalog, have none, so every call they served was counted unpriced although models.dev lists all these models (openai: gpt-6-astra $10/$50, gpt-6-luna $0.1/$0.5; google: gemini-3.8-flash $0.75/$3.75, per million, in the cached catalog). A call is now priced by its provider's catalogs first, as before, else by the first maker among the vendor presets that lists the model (the same makerCatalogs the reasoning levels borrow from), and a call whose provider has gone since is priced by its maker too. The model is matched by its own key first, then without a path or case (a relay's openai/gpt-6-astra is gpt-6-astra), a Bedrock geography, a (variant) or :tag, as the reasoning levels already are (new catalog.PricedBy, Provider.ListPrice, provider.MakerPrice); a model no maker lists (a Cursor or Kiro alias, a relay's own name) stays unpriced and keeps the + as before, no price is made up. Both the Usage summary and the Requests ledger go through the one pricer, so they agree. Tested with a new TestSubscriptionListPrice: calls to codex (gpt-6-astra, gpt-6-luna), copilot (gemini-3.8-flash, gpt-6-luna), claude (claude-opus-5-5) and a relay with no catalog (openai/gpt-6-astra, mystery-1) each get their maker's price in the ledger and the summary, mystery-1 alone unpriced (fails on main: all seven unpriced); TestLedger and TestSummarize unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestAddSecondOfPreset failed once in the full run with a fourth provider and passes on its own and in the package run; it doesn't touch pricing). Not tried against the reporter's usage log or in the GUI; the Sessions page's own pricing (internal/sessions) was not looked at. 2026-09-29 23:52:32 +08:00
yetone 1e9f3a46cb usage: a Requests tab lists every request of the period, one row each, and exports them as CSV, from the window and from magpie usage --csv (Discord 范不着: a per-request ledger as sub2api keeps, to set beside the upstream's own bill) — the Usage page summed calls by agent and model, and the log did not keep the model the agent asked for or the one the reply said answered, so a single request could not be checked against a vendor's bill. usage.Record now keeps Requested (req, the model id the client sent, a magpie alias or group as it came) and Served (served, the model the vendor's reply named, only when it named one); every usage.Append in the gateway fills them (the relayed and translated paths, Codex's backend, image draws, decide), and records written before load as they did, without them. The swap test the Routing page uses (a dated or pinned name is the same model) moved to usage.Swapped so the ledger marks the same rows. usage.Ledger gives the period's records newest first with each one's list-price cost, the agent it is counted under and whether the model was swapped, filtered by agent, failures alone and a search over the models, provider, account, session and effort; usage.WriteCSV writes them (time, agent, requested_model, provider, host, model, served_model, swapped, effort, input, output, cache write, cache read and reasoning tokens, cost_usd, duration_ms, ttft_ms, status, error, session, kind). The Usage page gets a third tab, Requests (请求), between Overview and Sessions, under the same period picker: an agent filter, All/Failed, a search field and Export CSV over a table of time, agent, requested model, provider · account, model sent, served model (amber with the Routing page's sentence when another model answered, plain on a dated name, a dash when the reply named none), effort, in, out, cache write, cache read, cost, duration and status, a failure's row marked by a red dot and bold code; the totals over it and 100 rows a page with Newer and Older. GET /api/usage/requests serves a page, POST /api/usage/requests/export saves all the filtered rows to Downloads (never over an earlier file) and opens the folder, GET /api/usage/requests.csv hands a browser the file in magpie web. The table scrolls sideways in its own box; at the window's narrowest the words after the head's cost give way so the three tabs fit without the page scrolling sideways. No border stripe; clicks move nothing (the pager held where it was). en and zh strings together. Tested with TestLedger (order, cost, swap, a dated name, an old record without the fields, each filter, the CSV exactly), TestServedModelRecorded and TestServedModelUnnamed (a request through a fake upstream answering with another model records Requested fake/sol, Model sol and the served name; one naming none leaves Served empty), TestUsageCSV (magpie usage --csv, a period, a stray argument refused), TestUsageLedgerRoutes (paging, totals, filters, export to Downloads twice) and a new internal/gui/tests/usage-ledger.test.cjs in Chromium and WebKit, en and zh, light and dark: the tabs and columns, the swap tag on the swapped row only, the 429 row, Older and Newer over 130 requests with the pager left where it was, the agent, Failed, search and period filters asking the server from the first page, Export CSV posting the filters with no page and saying where the file went, at 560 wide no tab scrolling the page sideways and the cost uncut, no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (122 tests) pass. Not tried in the real window through Wails or against a live gateway's log. 2026-09-29 21:26:01 +08:00
yetone 6aa99ef1c0 usage: costs show in RMB at a live rate, when Settings picks it (#212 L1cardo: 添加费用用人名币结算的选项,最好是能根据实时汇率来计算) — the Usage page, the tray panel, the TUI and the CLI all price a call in USD, magpie's list prices being dollars; Settings now has a Currency row (after Usage shows) offering $ USD, kept by default, and ¥ CNY, which converts every one of those at display time only — usage.Totals.Cost and the JSONL records it comes from stay in USD always, and a vendor's own balance (already in its own currency, a relay's ¥) is never touched.
The rate is internal/fx: a free, no-key endpoint (open.er-api.com/v6/latest/USD), cached in memory and in a small file beside magpie's other caches (XDG_CACHE_HOME/magpie/fxrate.json) for 12h, refetched only once that's stale, and falling back to the last rate it ever had — or a fixed 7.2 — while offline; the currency row's tooltip names the rate and when it was learned. usage.FormatCost carries the $/¥ formatting (2 decimals above $1, 3 below, whole numbers above 100) that usage_cli.go and internal/tui/pages.go used to each have their own copy of; both now read Settings' currency once per command/redraw rather than per row. internal/gui/api.go exposes the rate on /api/settings always and on /api/state only when cny is actually chosen, so usd users' hot path never touches the network for a rate nothing shows with.

Tested with new internal/fx/fx_test.go (a fake server: fetches and caches, answers from memory then from the cache file with no further calls, refetches once stale, falls back to a stale cache or Fallback when offline, ignores a reply with no CNY rate), TestFXInState in internal/gui/fx_test.go, TestFmtCostShowsChosenCurrency in internal/tui/usage_test.go, and a new internal/gui/tests/currency.test.cjs in Chromium and WebKit, English and Chinese (the Usage page's $ by default and ¥12.34×7.2 once cny is picked, the currency row's labels/state/tooltip, a real wheel scroll deep into Settings left alone by picking a currency, the choice surviving a reload). go build -tags nogui, GOOS=linux and GOOS=windows builds, and go test -tags nogui -count=1 ./... all pass clean.

A fetch that failed is not tried again for 10 minutes (the stale or fixed rate shown at once meanwhile), so offline each state() does not wait up to 4s on the network: TestGetDoesNotRetryAFailedFetchAtOnce.
2026-09-29 19:41:29 +08:00
yetone f9c99cf370 gateway: a call Codex makes for itself says so on the Routing page, and goes to a pool account with what Codex says about it (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna) — magpie changes no model: Codex sends its own calls on Luna (the guardian review of an approval, x-openai-subagent: guardian; a thread's title; memories) and, on Luna Reserve once the plan's allowance is used up, every turn with x-openai-codex-luna-reserve. Relayed to Codex's own sign-in these headers went on as they came, but a turn the pool of Codex's accounts served was signed afresh and went without them. Now x-openai-*, x-codex-* (turn metadata, installation, window, parent thread), session_id, conversation_id, x-client-request-id and version go on with it too; never a sign-in (Authorization, chatgpt-account-id, anything with authorization in its name) nor Codex's device attestation, the pool account's own going in its place, nor what holds for Codex's own account alone (x-openai-codex-luna-reserve, as another account's allowance may be left, and x-codex-turn-state, the backend's routing of the turn for that account). The trace, the Recent calls and the usage log keep the call's kind (kind, from x-openai-subagent; memgen; luna_reserve), and the Requests list, a replay and the request's story show it as a grey tag by the model — Approval check / 审批判定, Title / 标题, Memory / 记忆, Subagent / 子代理, Review / 审查, Compaction / 压缩, Luna Reserve / Luna 储备, an unknown one as Codex names it — the story saying Codex made the call itself and picks its model. The Requests list's model column fits its content (up to 36%) so model and tag keep their room, the tag giving way first. Tested with a fake ChatGPT backend on the pool path (acct-1 used up, acct-2 answering: both get the headers and neither the client's sign-in; fails without the change), callKind/codexHeader cases, and a new internal/gui/tests/routing-kind.test.cjs in Chromium and WebKit, en and zh (tags, the model shown whole, the story, a click leaving the row where it is); the other GUI tests, go build -tags nogui, GOOS=windows go build and go test -tags nogui pass 2026-09-29 17:56:43 +08:00
yetone d924802de5 usage: a streamed reply's time to its first token is kept, and its first text's (#196: usage 记录首字延迟(TTFT),用于计算模型的真实生成速度) — usage.jsonl had only a call's whole ms and its out tokens, so how fast a model writes couldn't be told from how long it waited or thought. The reply is now read as it goes to the agent, in the agent's protocol whatever the vendor spoke (Anthropic content_block_delta text/thinking/input_json and a tool_use's start, Chat content/reasoning_content/reasoning/tool_calls, Responses output_text/reasoning/function_call_arguments deltas and a function call's start, Gemini parts), and each record gets ttft_ms to the first content and first_text_ms to the first text, counted from the request as ms is, so ms − ttft_ms is the writing time; a reply that isn't streamed gets neither, and omitempty leaves old records as they were. Only the stream's events up to its first text are read (a JSON reply, or past 8 MB, not at all). Each try in the routing trace keeps its own from when it was sent, so a group that fails over shows which member was slow, and the route, the request log and the Codex backend's relay keep them too. Shown on the Routing page (TTFT in the list, first token / first text / tok/s in a try's story), in the Gateway's request log, and on the Usage page and in magpie usage per model as the mean TTFT and tok/s = out ÷ (ms − ttft) over the answered streamed calls. Tested with fake vendors streaming a gap apart for Anthropic, Chat and Responses, as they came and translated (Chat from Anthropic, Responses from Chat), the ChatGPT backend without a Content-Type, a whole reply, a group whose first member fails (its try untimed, the second's timed from its own start, the request's including the failed try), each protocol's events and split writes, and the summary's sums; the GUI's browser tests pass 2026-09-29 16:40:03 +08:00
yetone a1ca00d31b routing: a request shows the reasoning its model was actually sent at (显示意图路由实际的推理强度 #144) — the trace keeps the effort the agent asked for and, on each try, the one sent upstream after the turn's pick and fitting to the model's levels (picked marks the turn's pick), so the Routing list reads "Fake · m-big · high", its tooltip says what the agent asked for, and the request's story adds "The turn's pick replaced the medium Codex asked for."; usage.jsonl records the sent effort too, and the Usage session list shows where magpie routed a session's calls and at what reasoning ("→ m-big · high +1", with a Routed line per model and effort in its detail) 2026-09-28 18:11:58 +08:00
yetone 8dae6aa25b usage: each call is kept with the session it was part of — the X-Magpie-Session header when the caller sends one, else the session its agent names itself (Claude Code's x-claude-code-session-id, Codex's session_id, OpenCode's) — as "session" in usage.jsonl, and magpie usage lists the sessions that used the most, so several sessions on one model can be told apart 2026-09-27 23:02:36 +08:00
yetone 4f0b889ec1 usage: TestSummarizeTellsPlacesApart holds at any hour — its calls of hours before were yesterday's when it ran after midnight, and Today left them out; it now runs at noon 2026-09-27 01:25:17 +08:00
yetone c4268e524f providers: a provider's id can change — its editor has an ID field (magpie provider set <id> id=<new> in the CLI), and renaming it moves what named it by the old one: the agents on its models (each spelled its own way, recorded as magpie's), the routing groups' members, rules and classifier, other providers' fallbacks, which agents are shown it, and the vendor's fetched list. The old id stays with it, so an agent still running on the old config reaches it, a profile saved before applies to the new id, and its usage so far is counted under the new one. Asked on Discord 2026-09-26 20:22:30 +08:00
yetone 4790c06b02 usage: each call keeps where it went — the provider's host, or the account it was — so a provider id later pointed somewhere else no longer puts the earlier place's tokens and cost under the new one: a model's usage is told apart by host (provider/model @ host) when its provider went to more than one place in the period, or to somewhere other than where it goes now. Asked on Discord 2026-09-26 19:03:14 +08:00
yetone 04302b1e63 agent(codex): a Codex taken off magpie behind its back says so, with Apply again — something else rewriting config.toml (another switcher, an installer, a profile of its own) left the row showing a magpie model while Codex asked OpenAI for it, and the user blamed magpie; magpie now remembers what it set on each agent (applied.json) and tells three kinds of drift apart: the wiring gone (the provider table, catalog or base URL, or a profile overriding them), a magpie model replaced by one of Codex's own, and a Codex used since with none of its requests reaching the gateway (history.jsonl against when the gateway last saw it — an old process on an old config). A drifted row carries a coral Apply again pill on its name's line (a badge on the logo in the panel), the reason in its tooltip, Keep current settings in the row menu; the window asks /api/drift every 15s, so it shows without a reload 2026-09-26 01:05:19 +08:00
yetone a9e1a6769a agent + usage: each agent says what its User-Agent begins with, beside its name and icon, and the gateway tells requests apart by that — usage kept a list of its own, which had to be kept in step with the agents by hand and wasn't (DeepSeek Harness, omp and Command Code were missing from it, so they were drawn with no logo); a new agent is now known by its requests as soon as it is described 2026-09-25 05:51:32 +08:00
yetone 658d264e14 usage: DeepSeek Harness, omp and Command Code are known by their User-Agent — deepseek-harness/… was kept as it came, not as the agent dsh, so the Routing view and Usage drew it as an unknown agent with no logo; records kept that way are read as the agent's too 2026-09-25 05:47:19 +08:00
yetone 829d52f75a provider: a custom provider can name several catalogs (catalog=openai, deepseek) — a gateway reselling many vendors gets names and reasoning levels for all of them, prefixed ids like deepseek/deepseek-chat included (#17) 2026-09-24 14:10:53 +08:00
yetone 402a96c39d Rename dial to magpie, with a magpie for a logo
The product, module path (github.com/yetone/magpie), binaries, env vars
(MAGPIE_ADDR, MAGPIE_DEV_UI, MAGPIE_THEME, MAGPIE_DEBUG), config and cache
folders (~/.config/magpie, ~/.cache/magpie), bundle id (com.yetone.magpie),
gateway provider id and token, and every UI string now say magpie.

On first run magpie copies ~/.config/dial and ~/.cache/dial to the new
folders when they don't exist yet (copy, not move, so a still-running dial
keeps working; the old folders can be deleted by hand later).

The logo is a small round magpie in profile: one path for the header and
the tray, and the app tile adds the white belly and the blue-green tail.
The header mark is drawn in ink rather than the accent colour, which
fought the text in light mode.
2026-09-23 23:55:11 +08:00
yetone 30c489a000 usage: a token statistics page
The gateway now records every call's token counts (read from the provider's
own usage block, in both the translated and the pass-through paths) with the
calling agent taken from the client's User-Agent, into
~/.config/dial/usage.jsonl. A Usage tab sums them by period, agent and
model, with a per-hour/day/week timeline and cost at models.dev list
prices; dial usage does the same in the terminal.

Also nudges the window header 1pt up so the brand sits level with the
traffic lights, and lets ?view= open the window on a tab.
2026-09-23 03:16:33 +08:00