Commit Graph
23 Commits
Author SHA1 Message Date
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
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
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 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 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 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