Commit Graph
1454 Commits
Author SHA1 Message Date
yetone de290249bb pi: a model that always thinks has off hidden on the Messages API, and its Chinese refusal of thinking off is asked again (#699, liuweifeng: 走 Anthropic Messages API 的 always-thinks 模型被删掉 thinkingLevelMap 的 off,Pi 仍提供 off 导致上游 400) — piThinkingLevels left off out of thinkingLevelMap for every model on anthropic-messages, which is right for Claude (Pi offers a level left out, and Claude takes thinking disabled whatever its levels) but had Pi offer off for ZCode's GLM-5.3-Flash too, whose levels are low/high/max and which answers thinking disabled with 400 "该模型始终支持思考,不可关闭". Now off is left out only for a model that thinks only when asked: gateway.ThinksOnlyWhenAsked, the gateway's own anthropicModel (the test that decides it leaves a Claude's request as sent and turns another vendor's thinking off unless asked), plus a relay's opus-5.5 spelling. Another vendor's model there gets "off": null unless none is among its levels (GLM-5.2's none still maps off to it), as on Chat and Responses. The gateway's alwaysThinks, which asks a refused thinking-off request again with thinking left to the model, didn't know the Chinese wording, so the 400 reached Pi (and any agent whose request the gateway turned off); it now matches 始终…思考 / 思考…不可关闭. OpenCode's variants, dsh's levels and omp's efforts name only the model's own levels, off only for none, so they don't offer a level the model refuses; omp can't hide its off, and the gateway's retry now covers it. TestPiOffOnMessagesAPI (GLM-5.3-Flash off null and not offered; GLM-5.2 off=none; Claude and opus-5.5 off left out) and TestAnthropicPassthroughAlwaysThinksInChinese (asked again once without thinking) fail on the old code and pass. Pi's models.json generated in a temp HOME with PI_CODING_AGENT_DIR in it: zc/GLM-5.3-Flash has "off": null, low/high/max; claude-sonnet-5 has no off key. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestDevinSignedInOnlyInMagpie, a timing test that passes alone. Not tried with a real ZCode account. 2026-10-03 23:36:50 +08:00
yetone 89d305229b providers: a removed plugin subscription is named as its row was, and can be signed out for good (#694, mintonight: 点了移除,国际版账号没有消失,放在 plugin 里无法彻底删除;再添加回来还是旧账号) — Add a provider's "Removed from magpie — still signed in" listed a removed plugin account by its agent, which for every plugin's sign-in is "plugin", with the generic logo; and its only way out was Add it back, which brought the old account back, since Remove only hides an account and nothing signed it out. Now a removed account is named and drawn as its row was (accountLabel, shared with the provider row: a plugin's by the provider it signs in to, a moved built-in's by the built-in's logo; Exclusion.Name carries it to the CLI's list too). Its row in the add sheet opens the app's menu (openProtoMenu, the page left where it was): Add it back as before, or Sign out…, which asks first and posts provider/forget; the Providers page's reminder line has Sign out beside Add it back and Don't remind me. provider.ForgetAccount signs a plugin's accounts out of plugin-auth.json (the host's signOut, else under plugin-auth.json.lock), takes them out of logins.json and, for a moved built-in, out of the move's backup so moving back doesn't restore them; an agent's own sign-in that magpie only reads is hidden, as an account's Remove hides it; a built-in's saved accounts go through ForgetLogin, the one in use last. Signing in again then starts afresh. zh strings added. TestForgetRemovedPluginAccount (bun fake plugin: removed is named FakeCo, forgotten leaves plugin-auth.json, logins.json and the removed list empty, Add back brings nothing) and removed-forget.test.cjs (webkit, en/zh: the row named Qoder, the menu with no request and no scroll, Sign out asks then posts forget with the provider id, Add it back still posts show, the reminder's Sign out) fail on the old code and pass; add-sheet, click-scroll, plugin-two-subs and saved-signedout-remove still pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Qoder account; the plugins repo needs no change (the Qoder plugin keeps its sign-ins only in plugin-auth.json). 2026-10-03 23:09:13 +08:00
yetone c3cd790446 provider: a Refresh drops the picks the vendor's list no longer has (akic404 on Discord: a remote magpie offering far fewer models, one of its providers renamed from ws-ba5my… to bailian, still showed every old model here after a Refresh, until the provider was deleted) — a Refresh already replaced the fetched list (catalog.SaveLive writes it whole), but the models picked from the old list stayed in the provider's Models, and Exposed serves a pick the list lacks as one typed in by hand (#597), so every "ws-ba5myt9wad852ujp-…/qwen3.6-max" the remote no longer had stayed among the models agents see, drawn in the editor as "Added by hand". Now Fetch (through the new Refetch) drops from the saved picks those the list fetched before had and the one fetched now doesn't; a pick neither list had was typed in by hand and stays, and a fetch that fails or answers no model changes nothing. It applies to providers of a key (a remote magpie, any /v1/models provider), not to signed-in accounts, whose lists have their own rules. The editor's Refresh is told the dropped ids and takes them out of its draft too, or its Save wrote them back. TestRefreshDropsPicksTheListNoLongerHas (a remote-magpie preset against a server whose list shrinks and is renamed, then goes down) fails on the old code (all four picks kept) and passes. End to end with two sandbox magpies under temp HOMEs: A (nogui serve on 3461, a provider ws-ba5my over a fake upstream), B with A as a remote magpie and four picks (three of A's, one typed); after A's provider set ws-ba5my id=bailian and two models dropped, B's provider models left the typed pick alone, and with A stopped a Refresh failed and changed nothing. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./internal/... under a temp HOME pass. 2026-10-03 23:01:47 +08:00
yetone 67092380fb plugin: pi packages run as pi runs them — a program a login starts can't take the host's stdin, a select is answered with its option's id, and the extensions get pi's runner, pi's auth.json and session_start (owner: 都补上,用真实账号测) — tried with real accounts, five pi packages had problems that the fake one didn't show. pi-devin-plus runs devin auth login with stdio "inherit" when it finds no Devin sign-in, and the program read magpie's next requests from the host's stdin as its answer ("Login canceled") while writing over its answers; host.js now gives every program started through Bun.spawn and Bun.spawnSync (all of node:child_process goes through them) nothing to read and stderr to write. pi-zcode asks its region with onSelect and options { id, label }, pi's own shape, and checks the id ("bigmodel"); pi.js answered with the label, so the login said "Login cancelled". Packages read their sign-in back from pi's auth.json (readStoredCredential: pi-zcode for its plans, pi-cursor for its token, pi-provider-qoder through the registry, pi-antigravity syncs its accounts through it) and register what they find on session_start (pi-zcode its plans' models, pi-devin-plus and pi-xai-oauth refresh their catalogs), and a provider registered after loading went into a queue nothing read. Now each package is bound with pi's ExtensionRunner as pi's session binds it (bindCore: no UI, tools or session of magpie's), so a later registerProvider reaches the runtime; each pi provider's first account is kept in $PI_CODING_AGENT_DIR/auth.json as pi keeps it, written when it changes; and a provider newly signed in, and each one signed in when the host starts, is sent session_start, after which magpie lists its models again if they changed. A runner that can't be made leaves the packages registered as before. TestPiPackage now has a select answered with an id, a login that starts a program on the terminal, and a session_start that reads the sign-in from pi's auth.json and adds a model; it fails on the old code (the label answered; the model never listed, auth.json empty) and passes. With real accounts in a sandbox HOME: pi-zcode signed in through Z.ai and lists 11 models (none before); its requests reach ZCode but the account's Start Plan answers 200 {"code":1005,"msg":"exceed quota limit"}, which pi-zcode reads as an empty answer (its bug). pi-provider-qoder signed in through Google, 16 models, tool round trips on Kimi-K3 and Efficient. pi-devin-plus signed in, its catalog refreshed on session_start (53 models), tool round trips on swe-2 and claude-opus-5.5; pi-xai-oauth (grok-4.7) and pi-antigravity (gemini-3.8-flash) tool round trips; @rahularya01/pi-cursor answers text (its agent doesn't call a client's tools, upstream). pi-provider-kiro wasn't signed in: it needs pi's terminal, a Builder ID or an IdC login. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) pass. 2026-10-03 22:51:24 +08:00
Fanbuz b7c41ea2fe feat: 单独索引 Codex 归档会话并去除物理副本 (#698) 2026-10-03 22:45:46 +08:00
Fanbuz c9d0d15b94 fix: 记录网关实际返回的响应 ID (#693) 2026-10-03 22:45:42 +08:00
yetone a27bd9ec96 proc: the desktop app takes the agents' folder variables from the login shell, as it does PATH (atie on Discord: PI_CODING_AGENT_DIR set in the shell, magpie still wrote Pi's config to ~/.pi/agent/models.json) — magpie reads PI_CODING_AGENT_DIR, CODEX_HOME, CLAUDE_CONFIG_DIR and the rest with os.Getenv, but an app opened from the Finder, the Dock or the login item gets launchd's environment, with nothing a shell profile exports, so every agent was wired into its default folder in the home. proc.UserPath already asked the login shell for its PATH (sh -ilc printf with markers); the same single call now prints PATH and every variable in agentenv.Vars between the markers, parted by NULs, and each one magpie's environment doesn't have and the shell gives a non-empty value is set with os.Setenv; one magpie was started with is kept. These are read as soon as the app starts (the library is synced into the agents at once, the backend wires them), so UserPath now waits for the shell's answer up to 2 s instead of not at all; a slower profile still doesn't hold the window up longer, and what it says later is still taken. LoginPath uses the same call. Windows is unchanged (an app gets the user's environment from the registry there). Only the GUI's runGUI/runPanel call UserPath; no test but proc's own does. TestUserPathTakesAgentVars (a fake $SHELL whose profile exports PI_CODING_AGENT_DIR with a space, CODEX_HOME and an empty GROK_HOME) fails on the old code (nothing taken) and passes: Pi's folder is set by the time UserPath returns, magpie's own CODEX_HOME is kept, the empty and absent ones stay unset. Tried with a built binary calling proc.UserPath under env -i (launchd's PATH, a temp HOME): with /bin/zsh, .zshrc's PI_CODING_AGENT_DIR="$HOME/my pi" and CLAUDE_CONFIG_DIR came through, CODEX_HOME=/kept stayed, .zprofile's PATH folder was added, in 19 ms; with /bin/bash, .bash_profile's value came through. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestDevinSignedInOnlyInMagpie, timing tests on fake CLIs (5 s waits) that pass when the package is rerun; none of them reaches UserPath. 2026-10-03 22:34:36 +08:00
yetone 30c072713d gui: the Gateway's models say what they take — reasoning levels, images, context at each row's end, in full in a tooltip — and the search finds a group by its models (ARNO on Discord: 希望网关里的模型列表支持搜索查找,支持显示模型或者模型组更多的信息,比如该支持的推理强度,是否支持图像等) — each model row on the Gateway page now ends, before its copy button, with small muted chips: the reasoning levels agents are offered ("low–xhigh", or "low / medium" for two; the levels kept, not all it has, and none for a model whose levels aren't known), a picture glyph when it accepts images, and its context ("400K", "1M"; the 1M badge after the name moved there). Their tooltip says it in full: "Reasoning: low, medium, high, xhigh", "Accepts images" or "Text only", "Context: 400,000 tokens". A routing group's are what agents are told of it, as of a model: /api/providers' gateway.groups now carry the catalog entry's efforts, images, context and output, and members (provider/model, in its order); its tooltip says images are every model's, the context the least of its models', its output, and names its models. The search (already there, shown from 8 models) shows from 5, has a tooltip saying what it matches, and also matches a group's models, so "kimi" finds the group sending to it. Strings in zh. TestGatewayGroupsSayWhatTheyTake checks a group's listed members and that its efforts, images, context and output are the catalog's; gateway-model-info.test.cjs (webkit) checks the chips and tooltips of models and a group, the search by id, name, provider and a group's models, Escape, and that a click on the chips picks the model without moving the row. Related GUI tests (gateway-*, click-scroll, list-sort, public-url, request-archive, sse-body, web-page, api-key-theme) pass; go vet, go build -tags nogui ./... and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. 2026-10-03 22:34:36 +08:00
yetone 4c7788424c gateway: a rate limit that comes back as soon as its rest is over rests longer each time, until the account answers (01huadalang on Discord: WorkBuddy 429s a free model while the account has credits, and magpie asks it again every minute) — a 429 the vendor gave no time for rested a minute, every time: an account refused again the moment it was back was asked once a minute forever, a request lost each time before the group's next model took it. The failover is magpie's (restAfter), so this is the same for the built-in and for the plugin, which passes WorkBuddy's 429 and its headers on as they are. Now rateRest counts the rate limits that come as soon as a rest ends (within half an hour of it) and rests 1, 2, 4, 8, 16 then 30 minutes; an answer starts it over (clearRest drops the note it counts by), one long after the last rest a minute again, and one while still resting — the last one left, tried anyway, or one request's own retries — keeps the count where it is. What the vendor says still counts: Retry-After or a reset header, or a reset time in the error as keepRetry notes it, when longer than the backoff. A rest is by the account (restKey) as before, by the model for OpenRouter's shared-pool free models. A usage that says the account has credits never put a resting account first (restLast orders after weighing); the test holds that. The Routing page says "rate limited again as soon as it was back (n times in a row)" for such a rest. TestRateLimitBacksOffWhenItComesBack fails on the old code (no count, a minute every time). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, TestSavedButSignedOutClaudeServed and one internal/gui test, which pass rerun. Not tried against WorkBuddy itself: its 429's body and headers weren't seen. 2026-10-03 22:34:36 +08:00
yetone 88fd366efb gateway: Gemini's thought signatures come back on its tool calls, through Pi and every other client (#687) — Pi on the built-in Google Gemini (an API key, gemini-3.8-flash at high) failed the turn after every tool call with 400 "Function call is missing a thought_signature in functionCall parts … default_api:bash, position 3". Gemini 3 signs the first call of each step it thinks through and wants the signature back; its OpenAI-compatible API gives it in tool_calls[].extra_content.google.thought_signature, which magpie relayed to Pi as it came, and Pi's openai-completions (like the AI SDK) keeps a call's id but drops extra_content, so the call went back unsigned. magpie's own translation (Claude Code, Codex, Gemini CLI on the same provider) never read the field at all. Now a call Gemini signed reaches the client with an id carrying the signature (<id>__ts__<signature as base64url>, letters, digits, _ and - only), which every client sends back as given: a Chat request relayed to Gemini has it taken off the call's and the result's ids and put in extra_content, and parse does the same for a translated request (the signature on the call's Part, given back by buildChat). A turn with no signature at all — another model's calls, or an id a client shortened — goes to Gemini with skip_thought_signature_validator on its calls, the value Google documents and the Code Assist path already sends; a later call of a signed step goes without, as Gemini signs the first. Any other upstream gets the calls' own ids, never the long ones. TestGeminiThoughtSignatureThroughPi (streamed and whole) and TestGeminiThoughtSignatureThroughAnthropic, against an upstream that checks signatures as Google does, fail on the old code with the issue's 400 and pass; TestGeminiCallsWithoutSignature and TestSignedID cover the fallback, other upstreams and the id's round trip. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/gateway/... under a temp HOME pass. Not tried against generativelanguage.googleapis.com: there is no Gemini API key on this machine. 2026-10-03 22:34:36 +08:00
Kagari Mimiandhazukisendai 763eb60478 gateway: let a searching relay be named (#686)
* gateway: let a searching relay be named

A provider marked as searching by itself is still not picked automatically, so a relay quota is not spent for other models, but Settings can now name it as the searcher. The row and tests say both halves of that rule.

* gateway: keep named search relays out of fallback

Search ranking is automatic and fallback ranking again, while Settings additionally accepts a relay said to search. Manual-only choices come last and modelSearch skips them, so a failed automatic search never spends a relay quota; a provider on a searching host keeps its automatic rank even when its box is ticked.

---------

Co-authored-by: hazukisendai <251835060+hazukisendai@users.noreply.github.com>
2026-10-03 22:34:05 +08:00
Leo 39ec205faa fix: refine usage day selection and CSV filenames (#689)
* fix: refine usage day selection and CSV export

* fix: suppress default outline on usage day bars
2026-10-03 22:33:59 +08:00
mofelee ee68b96788 fix: avoid duplicate Pi interaction and gateway traces (#692) 2026-10-03 22:33:54 +08:00
Snowyy d62fecd468 plugin: reuse config parsing and avoid request body string copies (#696) 2026-10-03 22:33:49 +08:00
SadWood 6c46e5561c fix(gateway): 修复 Anthropic 图片来源字段混用 (#677)
图片来源按 base64 或 URL 输出各自字段,避免空的互斥字段导致 Factory 拒绝请求。
补充图片编码及已迁移 Factory 插件链路的回归测试,覆盖历史截图和后续文字请求。
2026-10-03 22:33:43 +08:00
yetone e8e317d419 gateway: a Chat Completions keepalive is a comment line alone, with no blank line after it — Crush (openai-go v2.7.1) failed every reply through magpie that had a keepalive in it with "Agent processing failed: failed to start agent processing stream: unexpected end of JSON input": its SSE reader takes every blank line as an event, data or not, and parses the empty data as JSON. The comment and blank line were ": keepalive\n\n"; now ": keepalive\n", which every reader skips (a blank line with no data is no event in the SSE spec, and openai-go's own reader skips a comment line). TestTranslatedStreamKeepsClientAlive now also wants no event without data (old code: 25). Found while testing every agent CLI through a sandbox magpie: crush run on magpie/codex/gpt-6-astra failed and passes now; claude, codex, gemini, opencode, pi, goose, droid, grok, cline, omp, agy and commandcode answered too. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that fails on main without this change too. 2026-10-03 22:26:47 +08:00
yetone 57a87f0ab9 gui: code text on Windows is Cascadia Mono / Consolas with Chinese in YaHei UI, not SimSun (#690) — on a Chinese Windows 11 the Library's MCP tags (远程 · 需登录), the routing page's model chips (gpt-6.1-sol) and other small code text drew thin, unhinted and not round: the tags take --code, whose stack (ui-monospace, SF Mono, Menlo, Consolas, monospace) has no Chinese face, so their Chinese fell to the generic monospace, SimSun on a Chinese Windows; and routing.css's model chips and step lines, the sign-in sheet's link and code, the import textarea, the headers editor and What's new named stacks of their own (ui-monospace, SFMono-Regular, Menlo, monospace, or var(--mono, …) with no --mono anywhere) with no Windows face at all, so all their text was SimSun. Now every monospace text takes var(--code), and on Windows (html.win, as --font already is) --code is "Cascadia Mono", Consolas, "Microsoft YaHei UI", "Microsoft YaHei", monospace; the Mac's and Linux's stack is unchanged (the Mac's ui-monospace is SF Mono either way). win-fonts.test.cjs (no sheet names a monospace stack of its own; the badge, the id, the model chip and the sign-in link take the Mac's stack off Windows and Cascadia Mono / Consolas / YaHei UI with html.win) fails on the old CSS and passes in webkit and chromium; the routing, library, omarchy and signin-link GUI tests pass as before but routing-compact, routing-manual and routing-time, which fail the same on main. Not seen on a real Windows machine; the light grey of the ids (--faint) is unchanged. 2026-10-03 22:23:17 +08:00
yetone 9f7b6bd86c codex: a cached entry keeps the cache's own max when magpie's list has none, and Codex's models_cache.json is read under CODEX_HOME everywhere (follow-up to #674, from reviewing #679) — restoring a cached native entry (backend away) set max_context_window to max(MaxContext, Context) from magpie's live list, so a list saved without the max (before magpie kept it) wrote the window over the cache's own (872000 became 272000); now a list with no max leaves the cache's, and codexcat.Window still raises it to a window past it. codexcat.CacheEntries and catalog.Codex read ~/.codex/models_cache.json even with CODEX_HOME set, while codexVersion and codexInstructions followed it; all four now take catalog.CodexModelsCache() (CODEX_HOME, else ~/.codex), and provider's codexCLIHome and the copy in codexInstructions are gone. TestCodexReadsCacheUnderCodexHome, TestCodexCachedListUnderCodexHomeKeepsItsMax and TestCodexCatalogEntryFromCodexHome fail on the old code (the ~/.codex cache read, none listed from CODEX_HOME); TestCodexNativeModelsTakeSetWindows now wants a cached gpt-5.5 with no live max to keep the cache's 400000 (old code: 272000). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. 2026-10-03 21:39:13 +08:00
yetone 7b5c3cb797 gateway: a relay that refuses Claude Code's newest asks is asked again without them, and from then on sent none (Keenc on Discord: CHCP answered "thinking.adaptive.display: Input should be 'summarized', 'omitted'" and "messages.1.output_config: Extra inputs are not permitted", Vertex Claude "thinking.enabled.display: Input should be ...") — both shapes come from Claude Code 2.1.288 itself, not magpie: its stream-json/IDE sessions send thinking.display "updates" (its connector_text mode; "highlights" exists too), and with per_turn_effort it puts a turn's effort as output_config on a system message in messages. Strict (pydantic, extra=forbid) relays refuse both; Claude Code drops them on its own 400 but only once per conversation, so every new conversation logged the errors. magpie now reads such a 400 from an Anthropic provider (refuseShapes, alongside refuseBetas, up to three rounds), marks that provider unfit for the shape, asks again at once and, from then on, sends it a display it takes ("highlights" as "summarized", "updates" left out, or none when display itself is refused) and messages without output_config, dropping the empty system message that only carried the effort; the request's own top-level output_config and the environment system message stay. A provider that takes the shapes gets them as Claude Code wrote them. magpie writes no budget_tokens below 1024 anywhere (v0.1.758 sends enabled/2048 as adaptive + effort low), so the "≥1024" error isn't from magpie's rewrite. TestRelayRefusingClaudeCodesNewShapes and TestClaudeCodesNewShapesKeptWhereTaken fail on the old code. With the real claude 2.1.288 through a sandbox gateway to a strict mock relay: v0.1.758 passed the 400 to Claude Code each conversation; this build answers the first one by asking again itself, the resumed turn and later conversations reach the relay first time ({"type":"adaptive"}, messages with role and content only). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against CHCP or Keenc's Vertex relay themselves. 2026-10-03 21:34:52 +08:00
mofelee d5591b8f8d usage: export effective model costs to Langfuse (#684) 2026-10-03 21:31:04 +08:00
Trojanking 7e2f5d9db7 devin: send the images a tool returned (#682)
A tool_result that holds only an image (Claude Code's Read on a PNG) reached
Devin as "(no output)": buildDevin read the result's text and never its
Images. Carry the ones that hold bytes on the tool message; Devin takes no
URL.
2026-10-03 21:30:50 +08:00
yetone b8fdba5d64 usage: OpenCode's own calls are counted, as Claude Code's and Codex's are (#680, Sun1090: cc-switch 会统计 OpenCode 的用量,我这样不会被统计吗?) — OpenCode used on its own providers (OpenCode Zen/Go, its own keys), not through magpie's gateway, never reached the Usage page: the local calls were Claude Code's and Codex's session files only. Now each OpenCode assistant message is a call (agent opencode): its model, provider (as the session provider), input, output with the reasoning in it (reasoning kept apart too, as Codex's), cache read and write, its end time and how long it took, the session it ran under (a subagent's in its parent's), and a reply that failed with no tokens as a failed call (a stopped one is none). They are read where the Sessions page reads OpenCode: opencode.db — OpenCode 2's session_v2/session_message (a compaction on the reply before it) or the older session/message, the old ones not yet copied while OpenCode 2 migrates — else the JSON files under storage/. A reply through magpie's gateway (provider "magpie", or "dial") is left out: the gateway logged it already. Each session is a call source of its own, read again whole only when it changed (its size is the messages' count plus the sum of their time_updated, so an older message written again tells too) and kept in the call cache on disk as a file's calls are; the database listing is kept while opencode.db and its WAL are unchanged, and a read-only handle is shared between sources and closed after 10 s unused. The Sessions listing is unchanged (allFiles still takes callFiles; Calls and CallSources take callSources). ContentOf says no place for an OpenCode call. OpenCode's own cost isn't used: calls are priced at magpie's effective prices, as the others are. TestOpenCodeCallsJSON, TestOpenCodeCallsDB (cache kept unchanged and across a restart, an updated message read again, a failed and a stopped reply) and TestOpenCodeCallsV2 (compactions follow the reply before them, magpie's skipped) fail on the old code (no OpenCode calls). With a read-only copy of this computer's ~/.local/share/opencode (JSON storage, 3 OpenCode Zen kimi-k2.5-free replies): 3 calls, input 12281, output 235 (123 + 112 reasoning), cache read 24573 — the same as jq over the message files and as SQL SUM over an opencode.db built from them. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 21:27:17 +08:00
Leo 48d524d65a feat: select a day in usage charts (#683) 2026-10-03 21:27:06 +08:00
yetone f426c08395 gui: a long model list keeps every picked model in sight, and its foot is a button that draws the rest (#685: Sun1090 — zen实际使用了10个模型,现在只能看到前7个,剩余的我都不知道模型名称,怎么搜索?)
The provider editor drew the first 80 of a vendor's list and then "… {n} more, filter to find them", and the add form the first 120: a model picked past them wasn't drawn at all, and the note named none of what it left out, so there was nothing to filter by (OpenCode Zen's 84 models, 10 picked, 3 of them past 80). Now past the cap only the unpicked are left out, a picked model is drawn wherever it is in the list, and the note is a button, "Show the other {n}" (显示其余 {n} 个), that draws the rest; they stay drawn while the editor is open. The button is marked data-unrolls, so the click holds the list where it is.

provider-models-rest.test.cjs (84 models, 10 picked, 3 past 80: all 10 drawn, the button opens the last one without moving the list or picking anything, and a chip click keeps them open; en and zh) fails on the old code. The provider, model and click-scroll GUI tests under WebKit pass but provider-levels-scroll, which fails on main as well. Chromium doesn't launch on this machine, so WebKit only, and not tried in the app with a real provider.
2026-10-03 21:20:58 +08:00
yetone 730a163f47 cursor: a step that calls tools records what it used, the cache included (#676) — Cursor tells a turn's usage (TurnEndedUpdate) only once the client has run the tools and sent their results in the same Run, and the caller runs them after the Run is closed, so every tool step was recorded as an estimated input with no cache (2395 / 0 where Cursor counted 4265, 4224 of them cached), while text turns showed ~99% cache. After the calls Cursor sends only the checkpoint (no token details) and heartbeats; a cancelled Run ends with an error and no usage. Every Run is counted in the dashboard's usage events (DashboardService/GetFilteredUsageEvents) by its conversation_id, input apart from the cache read and written; one shows about 2.5 s after the Run closes. So a tool step with no TurnEnded now closes the Run and looks for its event (after 1.5 s, every 0.5 s, up to 6 s): its conversation's, no older than the Run, not already counted; else the estimate as before. A Run with no conversation id gets one, to be found by. The estimate is also no longer added to a turn read wholly from the cache (input 0, cache read > 0). The mover's min for cursor is 0.1.9, which does the same in the plugin (magpie-community/plugins: cursor 0.1.9). TestCursorToolStepReadsItsUsageEvent fails on the old code (5008 estimated, 0 cached) and passes. Tried with the real Cursor account: the built-in (grok-4.7-medium-fast) reports 6153 in, 6144 cached on a tool call, streamed or not; and through a sandbox magpie's plugin host with plugin 0.1.9, 6186 in, 6144 cached, 121 out, where 0.1.8 recorded 5057 estimated, 0 cached. A tool step now takes about 2.5 s longer to finish. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 21:14:13 +08:00
yetone bd45fa452a gui: Linux tray quotas reach the bar's tooltip (atie on Discord: on Omarchy, hovering magpie's tray icon showed no quota with 设置 › 用量 › 菜单栏显示额度 on, after opening the panel and restarting) — Wails v3.0.0-beta.24's Linux SetTooltip is a "TBD" no-op: the StatusNotifierItem's ToolTip stayed the {title: "magpie"} it was made with and no NewToolTip was ever sent, so every bar showed "magpie" (Omarchy's bar shows tooltipTitle before Title, waybar ToolTip before Title too, and waybar reads ToolTip again only on NewToolTip). magpie now sets the ToolTip itself after Wails' SetTooltip: through the prop.Properties Wails exported (reached by reflection, so the (sa(iiay)ss) type it was made with is kept and PropertiesChanged goes out), then sends org.kde.StatusNotifierItem.NewToolTip from the same session-bus connection. The quotas go in the title, the part both Omarchy's bar and waybar show; waybar reads it as Pango markup, so &, < and > become look-alikes (sniTip) rather than losing the whole tooltip. Until the tray is up (or should a Wails update move the field) the loop tries again next time. TestSNITipIsSafeMarkup covers the markup. In the omarchy VM (Omarchy 4, Quickshell bar), built there with -tags "production gtk3", in a sandbox HOME with the VM's DeepSeek and Kimi balance keys: the released magpie's item kept ToolTip "magpie" (Title "¥79.15 | $8.83"); this build's ToolTip is (sa(iiay)ss) "DeepSeek · ¥79.15\n\nKimi · $8.83", dbus-monitor shows PropertiesChanged then NewToolTip from the item's connection, and hovering its icon in Omarchy's bar shows both balances. Waybar itself wasn't run (the VM's Omarchy 4 has none); what it does is from its source (sni/item.cpp). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut, timing tests that pass alone. 2026-10-03 20:57:39 +08:00
yetone 58edc00bba gui: a provider with no model picked opens with none ticked, so Select none and Save stays none (#578, #614: Sun1090 — 我点了全不选,并且点击了保存,但是还是显示24个。重新打开供应商,里面也是勾选了24个free模型; 我没点保存,点了只选free 那个按钮,关闭的时候已经生效了)
The editor took its picks from the models marked on, and with no pick saved magpie marks on the ones agents are served: the vendor's first 24 (Exposed). So a provider with none picked opened with those 24 ticked as if picked, never dashed as v0.1.689 draws the served ones, and the next Save wrote them as picks. Select none and Save did write none, but opening the editor again showed 24 ticked, and a Save from there made them picks. That is also why Free only looked applied without a Save: the buttons only change the editor's draft, and Cancel, Escape or a click outside drop it, but what opened afterwards was the 24 served, ticked, not what was saved. Now the editor starts from the saved picks (the provider's chosen, those of them still served), so none saved opens with none ticked and the served 24 dashed, with the hint saying agents see the vendor's list. Since a fresh provider now opens dashed rather than ticked, a click on a served chip leaves it out as a click on a ticked one does, the rest of what is served picked in its place; a chip not served is picked as before.

provider-draft-picks.test.cjs (30 models, 3 free, none saved: opens with none ticked and 24 served; Free only then Cancel or Escape writes nothing and opens as before; Free only and Save writes the 3; Select none and Save writes [] and opens with none ticked; saved again untouched it stays []) and provider-none-picked.test.cjs (now: opened with none picked, both drawn as served) fail on the old code; model-rate.test.cjs's chip click now leaves the served chip out. go build -tags nogui ./... passes; the provider and model GUI tests under WebKit pass but provider-levels-scroll, which fails on main as well. Chromium doesn't launch on this machine, so WebKit only, and not tried in the app with a real provider.
2026-10-03 20:57:39 +08:00
yetone a55c17f397 gui: Cmd+, opens Settings, Ctrl+, on Windows and Linux (#670: App进入设置没有快捷键吗?mac一般是command+逗号)
magpie had no shortcut for Settings: only the gear in the header opened it. Now Cmd+, on the Mac and Ctrl+, elsewhere opens the Settings page from any page of the window, from a text box too, as a Mac app's does; in the tray panel it opens the window on Settings, as the gear does there. Another modifier with the comma (Ctrl on the Mac, Alt, Shift) doesn't, and a browser tab (magpie web) leaves the keys to the browser. The gear's tooltip says the shortcut, "Settings (⌘,)" or "Settings (Ctrl+,)" (data-keys, kept through a language change). magpie has no app menu of its own (Wails' default Mac menu has no Settings item), so the shortcut is the page's, beside the text size's Cmd/Ctrl + − 0.

settings-shortcut.test.cjs (window, a focused text box, the panel asking for window/main?view=settings, other modifiers, the browser tab, the tooltip; en and zh) fails on the old code. go vet ./internal/..., go build -tags nogui ./... and GOOS=windows go build ./... pass; the GUI tests under WebKit pass but those that fail on main as well (33 there: cli-update, group-from-model-add, menu-scroll, old-webkit, provider-levels-scroll, redact-rule-row, routing-compact/manual/time, seg-contrast, session-identity, signin-callback, zcode-site). Chromium doesn't launch on this machine (icudtl.dat missing), so it was tried in WebKit only, not in the app's own webview.
2026-10-03 20:57:39 +08:00
yetone 23eed59f7a gateway: a group's auto effort keeps a conversation's reasoning while its cache is warm, going up but not down (#617: Kenoia — with group/core effort "auto", 40 of 42 effort changes between a Pi session's turns on gpt-6.1-sol read 0 cached tokens, against 4 of 834 turns without one)
A group with effort "auto" asks its decision model (Jev or a model asked in words) how hard each user turn is as it begins, and the turn's requests ask for that level. Within a turn it holds, but from one turn to the next it moved freely (xhigh → high → xhigh …), and the reasoning effort is part of what the vendor caches a conversation under: a turn asking for another level reads none of the cache back. Kenoia's production data shows it: 237,632 uncached input tokens on a 26 s xhigh → high switch that had just read 230,912 from cache. The agent kept its own effort fixed; magpie changed it.

Now a new turn keeps the conversation's level when the decision model picks a lower one while the cache is warm (keepsEffort): a higher pick is still taken at once, so a hard turn gets the reasoning it needs and a conversation pays for at most one miss per level. A lower pick is taken on a turn whose cache is lost anyway: one after cacheCold (5 min) with neither a request nor an answer (ruleAnswered now keeps when the vendor last answered), or one after the agent compacted the conversation (under half as long as its last request). Tool-result rounds keep their turn's level as before. The trace's rule shows the level kept (pick) and the one the decision model wanted (wanted, beside classified.effort). Where the change would go as a configuration_update that keeps the cache (the MAGPIE_EFFORT_UPDATES preview: Responses, a GPT-6 model, a ChatGPT account or an OpenAI key), the wanted level is sent and the update carries it, so with the preview on auto still follows every pick.

TestAutoEffortKeepsTheCache (high, xhigh up at once, high held at xhigh on the turn and its tool rounds, medium taken after the cache went cold) fails on the old code (the held turn sent high); TestKeepsEffort covers warm, cold, answered since, compacted, up, same and no pick; TestAutoEffortGoesAsUpdate (a group/core of codex/gpt-6-luna, MAGPIE_EFFORT_UPDATES=on, Responses) sends xhigh at the top and u:high before the next user message. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real account.
2026-10-03 20:54:46 +08:00
yetone f0b0153e92 gateway: a routing group's own reasoning levels are the most its members are asked for, whoever asks (#671: 我在设置里限制了最高到high,但是他突破了这个限制)
A group whose 推理档位 is set to its own levels (Group.Levels, #295) only listed them to agents: nothing bounded what was sent. The reporter's group offers none…high with 推理强度 "每轮判断" (effort auto, GPT-6-Luna deciding), and gpt-6.1-sol went out at xhigh: the decision model picks among low, medium, high and xhigh, and that pick was fitted to the member's own levels (fitLevel) only, never to the group's. The same held for an agent asking for a level the group doesn't offer (Pi or Codex keeping xhigh from another model, Claude Code's output_config max or a 60000-token thinking budget) and for a model asked for at an effort by its suffix (group/x:max).

Now every try of a group's member is bounded by the levels the group offers for it (groupLevels: the group's own, else the outermost group in it that names some): the turn's pick, the suffix's effort and the agent's own level (agentEffort, in Chat's, Responses' or Anthropic's words, thinking budgets included) each become the nearest level the group offers (withinLevels, fitEffort's rule) before the member's own fitting; ultra counts as offered where max is. A member the user fixed at an effort ("provider/model:xhigh") is the user's explicit choice and keeps it. A group without its own levels is unchanged.

TestGroupLevelsCapEffort (a none…high group: auto picking xhigh, on the turn's first request and its tool rounds; the agent asking xhigh, max or ultra in Chat and Responses; Anthropic adaptive effort max and a 60000 budget; group/r:max; a member fixed at xhigh) fails on the old code (auto xhigh sent as xhigh) and passes. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real account.
2026-10-03 20:54:46 +08:00
yetone 72fd0cb42e gateway: a Claude that thinks only adaptively is never sent a thinking budget, whoever wrote it (Keenc on Discord: claude opus 5.5 已经不支持旧的 thinking.type.enabled 了,必须用 thinking.type.adaptive + output_config.effort,magpie 还在按老格式发参数,会导致 API Error: 400) — magpie turned thinking adaptive only where it built an Anthropic request itself, from the model's name in magpie. An Anthropic client's own thinking {type: enabled, budget_tokens} was relayed as it came, so an agent that doesn't know the model (asked by a relay's or magpie's name for it) got the vendor's 400 "claude-opus-5-5 requires adaptive thinking"; and a model named otherwise in magpie than at the vendor (an upstream name, e.g. "opus" for claude-opus-5-5[1m]) was built with a budget, the check reading magpie's name before the vendor's went in. Now forwardOnce, which every request to an Anthropic endpoint goes through (relayed, built, a group's effort fitted, count_tokens), reads the model the body goes with and, for one from 4.6 on, sends thinking {type: adaptive} (its display kept) with the budget as the effort it is nearest in output_config.effort (<=4096 low, <=12000 medium, <=24000 high, above max, as buildAnthropic asks), unless the request names an effort already; "disabled" and adaptive go as sent, and older Claudes (4.5 and before) keep enabled with their budget. claudeVersion also reads opus-5.5 without "claude-" and the old order claude-N-N-family, and caps the major at two digits so a dated id is never taken for a version. TestAdaptiveOnlyModelsNeverGetABudget (a fake Anthropic upstream answering 400 to enabled for 5.5: a relayed budget, Bedrock's spelling, an effort beside a budget, a name mapped after, Chat's reasoning_effort onto a mapped name, disabled kept, Sonnet 4.5 kept on its budget) fails on the old code and passes. Against the owner's real relay in a sandbox gateway under a temp HOME: yylx/claude-opus-5-5 with thinking enabled, budget 10000 answered the 400 with the old build and 391 with this one; budget 31999 (effort max) and Chat's reasoning_effort high answered too. AiHubMix and OpenRouter took enabled on the old build already (they convert it themselves). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestCodexVersion, a timing test that passes alone. 2026-10-03 20:51:25 +08:00
yetone 64665bb249 codex: a window set on a ChatGPT account's model reaches Codex's own model list (#674)
With Codex signed in to ChatGPT through magpie (openai_base_url at
/backend-api/codex), Codex reads the backend's model entries from magpie's
/backend-api/codex/models, which passed them on as the backend gave them:
a window the user set on the codex provider (magpie model context
codex/gpt-6.1-sol 600k, or "*" for all) was in /v1/models but Codex still
got context_window 272000. The model_catalog_json path did the same: an
own model's entry, taken from Codex's models_cache.json, kept the cache's
window instead of the one magpie resolved for it.

Now each native entry takes the window the user set (Provider.ContextOf,
as /v1/models says it) — context_window, and max_context_window raised to
it where the window is past it, so the entry never says less than its
window — and nothing else in it changes; a model with none set keeps the
backend's. The ETag carries the windows set, so setting or taking one away
has Codex ask again. A list made from Codex's cache (the backend not
answering), which keeps what magpie last handed Codex, takes the account's
own list's window back for a model with none set, so a window taken away
doesn't stay. The model_catalog_json entry takes the window magpie
resolved. base_instructions and the rest of each entry are untouched.

TestCodexNativeModelsTakeSetWindows (set windows on, other models as the
backend says, max raised, ETag changing on reset, the cache restored) and
TestCodexCatalogOwnEntryTakesResolvedWindow fail on the old code and pass.
Against the owner's real ChatGPT account in a sandbox gateway under a temp
HOME (codex provider only, gpt-6-sol 600000 and gpt-5.5 400000 set):
/backend-api/codex/models gave gpt-6-sol 600000/872000, gpt-5.5
400000/400000 (max raised from 272000), the other five 272000/872000 as
the backend lists them, all 52 fields and prompts kept; /v1/models agrees
for the two set; no token rotated. go vet, builds (darwin, GOOS=linux,
GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME
pass. A Codex session's usable window wasn't checked.
2026-10-03 20:45:58 +08:00
TryWorld 07644556a7 fix(davsync): scope settings credentials during partial sync (#673) 2026-10-03 20:43:13 +08:00
yetone 64ad965e71 gateway: a request passed on to a remote magpie keeps the session its agent named (#672)
Kenoia: Pi's X-Magpie-Session and session_id were lost on the hop to a remote magpie, so the remote recorded no session and keyed its affinity by the first user message, which two sessions can share. As reported: forwardOnce builds a fresh request and copies the client's own headers only for Codex (fromCodex) and Claude Code, and passOnCaller, the remote-magpie step, set only X-Magpie-Agent and X-Magpie-Via; sessionOf, nativeSessionOf and conversationID on the remote then found nothing. A Responses body's prompt_cache_key was never touched.

withCaller now also notes the caller's X-Magpie-Session and the first of the agent's own session headers nativeSessionOf reads (session_id, x-session-id, x-opencode-session, ...), by its name, trimmed and at most 128 bytes, and passOnCaller sets them on the request to a remote magpie, so Chat, Responses, Messages, drawings and videos passed on all carry them. passOnCaller is called only for a remote magpie provider, so no vendor is sent them.

TestRemoteMagpieKeepsTheSession (two gateways in-process: a Pi request with X-Magpie-Session and session_id to a Remote magpie provider, on /v1/chat/completions and /v1/responses with a prompt_cache_key) fails on the old code (the remote was sent neither and recorded no session) and passes: the remote is sent both, records session and native session, and the vendor behind it sees neither header. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried between two real computers.
2026-10-03 20:42:16 +08:00
yetone 0d78acd62a gateway: WorkBuddy's plans draw with the image models their config lists (freecss on Discord: let WorkBuddy's subscription image models be magpie's image models) — Settings › Models › image model, the Magpie Image MCP and /v1/images/* now offer a WorkBuddy plan's image models, for the China build (workbuddy) and WorkBuddy AI (workbuddy-ai), built-in or moved onto @magpie-community/opencode-workbuddy-auth. Which models are taken from GET <site>/v3/config, signed as a chat is, by their tags (text-to-image, image-to-image), with no ids written in: today the China desktop sign-in lists hunyuan-image-v3.0-art, WorkBuddy AI gpt-image-2.5-sunburst (which edits too). The list is kept an hour per account (5 min after a failed ask); Drawers answers at once and asks in the background. A drawing goes as WorkBuddy's CLI sends it, to <site>/v2/images/generations or /v2/images/edits in JSON: GPT Image with quality, background, output_format and b64_json; Hunyuan with model, prompt, n and size only; an edit's images as data URLs, to the model itself when it takes images or to its -edit beside it, else a 400 that says the model draws from a prompt only. WorkBuddy's {code, msg, data} is unwrapped (a non-zero code is a 502 with its msg); Hunyuan's COS URL is passed on as the other URL drawers' are. These models are never AutoDrawer's pick, since each image costs the plan's credits. For a moved plan the plugin's fetch signs and sends it: pluginFetch now also takes an absolute URL, only to the plugin base's own host, and Provider.Origin gives a provider's site. TestWorkBuddyDrawsWithItsConfigsModels, TestWorkBuddyAIDrawsGPTImage and TestWorkBuddyPluginDraws (fake upstream: config, generation, edit, envelope error, plugin path, foreign host refused) fail on the old code. Against the owner's real accounts (moved onto the plugin) in a sandbox gateway under a temp HOME: workbuddy/hunyuan-image-v3.0-art drew a 1024x1024 PNG (URL), workbuddy-ai/gpt-image-2.5-sunburst one in b64, an edit of it with gpt-image-2.5-sunburst turned the bird red, and an edit asked of hunyuan-image-v3.0-art got the 400 without a call; no token rotated. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The GUI's model picker wasn't looked at (nogui sandbox). 2026-10-03 19:55:07 +08:00
yetone 2d5f9c748e gui: the Usage page shows every account again, each as compact as before the trend lines, and remembers which ones are open (ARNO on Discord: 以前每个账号所占的空间很小,基本能一页看到所有账号的情况,现在要一个个手动展开账号,展开后占的空间又特别大,余额走势的线条也分散在各个地方很乱。而且关了magpie后又得一个个展开)
With 17 subscriptions and 29 accounts, a card with several accounts showed one of them and hid the rest behind "Show N more accounts". Opening one showed every account at full size, with resets, credit days and its own trend line, so a page that once fit most accounts filled several screens. The open state was kept per subscription, and ARNO had to open the cards again after quitting.

Now every account of a card is on the page. The one in sight (the last served, else the first) is shown in full. The others are shown in brief: the account row and its windows' names, figures and bars, with the reset times only in the bar's tooltip, and no trend line, credit days, "read" line or resets row (the balance stays). A chevron at the end of each account row opens or folds that account alone. The card's button, now in its head beside the name, opens the rest in full or puts them back in brief. Each account's choice is kept in localStorage magpie.usageAccounts (provider + account), so it survives a restart and syncs with the tray panel. A subscription opened under the old magpie.usageOpen starts with all its accounts in full. A brief account's separator sits where a full one's does, so folding doesn't move the account clicked, and the page doesn't scroll (backToReader). The tray panel shows the accounts open in full (or the one in sight) with the same button. A jump to an account from the tray opens that account.

Tested: usage-fold.test.cjs checks the full/brief split, the per-account chevron (no scroll, the account stays put), the head button, that the choice survives a reload, Hide accounts, and the panel following the window. A new case with 17 subscriptions and 29 accounts at 1280x800 with Trends off has all 29 accounts' bars on the page and at least 15 fully on the first screen (16 now; 15 before the curves at 9ccabdfb^; 12 on v0.1.752, with 12 more only behind buttons). It fails on the old code. tray-cell-click now expects the opened account in magpie.usageAccounts. quota-curve, click-scroll, panel-usage, account-mask, mobile-web, tray-cell-click, quota-families and quota-pools pass in WebKit. Screenshots at 1280x800 in zh: Trends off, 19 of 29 accounts' bars fully on screen (scroll height 1360 vs 2005 with Cycle). go vet ./internal/... and go test -tags nogui -count=1 ./internal/gui/ under a temp HOME pass. Not tried in the built app with real accounts.
2026-10-03 19:45:33 +08:00
yetone 6e915d794d search: a searcher is asked on the API it searches on, and one that finds nothing gives way to the next searcher and the search APIs (#669: with Automatic resolving to deepseek/deepseek-flash and Tavily set up, every client's web_search got web_search_tool_result.content [], the routing log saying 200) — magpie asks its searcher on Anthropic's API, and DeepSeek's Anthropic API wasn't counted among those that search, so the request was translated onto DeepSeek's first API, Chat; translate chose the API a provider searches on (DeepSeek's Responses) only for a client's request, never for magpie's own search, so DeepSeek got no search tool at all, and deepseek-flash said it couldn't search or wrote its own DSML tool-call markup as text. That text went to the model as what was found, with no page, and was taken as a success, so Tavily was never asked. Now translate sends a searcher to the API it searches on as it does a client; api.deepseek.com searches on Anthropic too (tried: web_search_20250305 there answers web_search_tool_result blocks of real pages with DeepSeek's encrypted_content, and takes magpie's own blocks back in a later turn); a searcher's results are read one by one, title, address and page_age (now carried to the client's block), never its encrypted_content, which stays "" in what magpie sends; a reply with no page and no address in its text is "found nothing", and the search goes to the next searcher magpie would pick (up to 3, within the 2 min), then to the search APIs. TestSearcherAskedWhereItSearches (DeepSeek searching on Responses only: old code asked Chat and gave the DSML text and []), TestDeepSeekNativeSearchResults (DeepSeek's native reply: pages, their ages, no encrypted_content passed on) and TestSearcherFindingNothingGivesWay (a 200 with nothing found goes to Tavily, or to the next searcher first) fail on the old code; TestDeepSeekSearchesOnResponses now expects the Anthropic API to search. With the real DeepSeek key in a sandbox gateway (temp HOME, copy of providers.json, searcher deepseek/deepseek-flash) the issue's curl on aihubmix/glm-5.3-flash got 10 web_search_result pages (weather.com.cn, moji, …) and an answer from them; deepseek/deepseek-flash asked directly with that reply in its history searched natively and answered. No Tavily key here, so the fallback to it was tried only against the fake. go vet ./internal/..., builds (darwin -tags nogui, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, a timing test that passes alone 3/3. 2026-10-03 19:38:13 +08:00
yetone dc3deaf09a sessions: Alma's chats are on the Sessions page, read only (mamba on Discord: magpie现在是不是还不能管理alma的会话呢) — Alma keeps every chat in one SQLite database in WAL mode, chat_threads.db in its data folder (~/Library/Application Support/alma on a Mac, $XDG_CONFIG_HOME/alma on Linux, %APPDATA%\alma on Windows): chat_threads (title, "New Chat" until named; model; workspace → its folder; incognito), chat_messages (AI SDK UIMessages in a tree, subagents' under parent_tool_call_id) and usage_records / aux_usage_records (a reply's tokens, cache reads and writes included in input). magpie now lists each chat that has something in it, incognito ones left out, and so are the chats Alma's scheduled tasks run in (thousands of them, which would bury every other agent's sessions), told the way Alma itself tells them: metadata.isCron, or a title starting "⏰ Cron:" (the title is set before the flag; 39 here have only the title): its title (else its first prompt), its folder, prompts and replies, tools and skills (tool-<Name>, dynamic-tool, Skill's skill), and models and tokens from the usage records (the model without Alma's provider id or plugin:<plugin>:<provider>:), priced like the others. It reads through a read-only connection, one read transaction a chat, and reads a chat again only when its own rows changed (updated_at, its messages' count, time and bytes, its usage rows), since the database's files change with every write to any chat. Alma's chats can't be deleted, moved or resumed from magpie: a chat is rows in Alma's live database, with triggers, a full-text index and other tables pointing at it, that only Alma can change safely, and Alma has no command to pick a chat up again; its data folder is in Dirs for discovery. Against a copy of the real database (6126 threads, 25424 messages, 11038 usage rows): 928 chats listed (3763 scheduled ones and the empty ones not), 18300 messages, all titled, every token total equal to the database's sums for those chats; the first listing takes 2.4 s, later ones 0.1 s. TestAlmaSessions (with a chat flagged isCron and one titled "⏰ Cron:" only, each failing without its rule), TestAlmaChatReadAgainWhenItChanges (a title and a usage row changed with no newer time) and TestAlmaModel fail without the change; tests set APPDATA so Windows never reads the real Alma. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the GUI against the live app. 2026-10-03 19:18:46 +08:00
yetone 2eb9266395 goose: runs on magpie as a custom provider that hands Goose each model's window and says which models Goose can send a thinking level (wani on Discord: goose gui不支持设置推理等级,而且上下文也不对)
magpie never wired Goose to its gateway: its model field offered only Goose's own providers, so a user had to add magpie by hand as a custom provider. Goose then treats every declared model as one that doesn't think (ModelInfo.reasoning defaults to false). With no context_limit, it guesses each window from its own registry by the model's name, or falls back to 128K. Goose's model field now offers magpie's models. Picking one writes custom_providers/magpie.json next to Goose's config.yaml (~/.config/goose, or %APPDATA%\Block\goose\config on Windows), then sets GOOSE_PROVIDER=magpie and GOOSE_MODEL. The file holds only keys Goose reads, from DeclarativeProviderConfig (crates/goose-providers/src/declarative.rs) and ModelInfo (crates/goose-provider-types/src/base.rs):
- engine openai, base_url set to the gateway's /v1, Authorization: Bearer magpie as a header, api_key_env "" and requires_auth false;
- dynamic_models false, so Goose uses this list rather than /v1/models, which has no windows;
- skip_canonical_filtering, so models Goose's registry doesn't know stay in the list;
- for each model, its context_limit (ContextLimitResolver takes it before anything but GOOSE_CONTEXT_LIMIT) and reasoning.

reasoning is true only for the models Goose actually sends a level to on an OpenAI-compatible provider. Those are OpenAI's reasoning models, as is_openai_responses_model in crates/goose-provider-types/src/formats/openai.rs tells them (gpt-5/6, o-series). For every other model (Claude, GLM, DeepSeek, Gemini…) Goose keeps thinking_effort to itself and never sends it, so a Thinking Level shown for them would do nothing. Sync rewrites the file as the catalog changes. Check reports a base_url or header someone else rewrote. Picking no model removes the file. The Notice asks for a restart, because Goose loads custom_providers at start-up. GOOSE_CONTEXT_LIMIT is left alone: it is one global override, and it would beat every per-model window.

What Goose itself can't do:
- On an OpenAI-compatible provider, Goose sends no effort for a model that isn't OpenAI's.
- The in-session thinking menu follows Goose's name heuristics and its registry, not the provider's reasoning.
- Before a session starts, the desktop app's context ring prefers Goose's registry to the declared window.

TestGooseRunsOnMagpie covers the options, the file's keys, the windows, reasoning for gpt-5.5 but not glm-4.6, Check, Sync and removal; the old code fails it. go vet, the builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass, except TestProvidersAnswerWhileListsComeIn and TestSavedButSignedOutClaudeServed: these timing tests pass when run alone. Not tried against a real Goose.
2026-10-03 19:18:46 +08:00
yetone b43544b27a gui: the quota curves can be turned off, picked once in the allowances' head, and the cards drawing them are calmer (ARNO on Discord: 新版本加上了额度走势后,额度面板变得非常混乱和难以观看了) — since 9ccabdfb every account on the Usage page had its own "2 days | Cycle" segmented control over its curve, so a card with two accounts carried two, there was no way to have none, and the tray's cards drew a thin line each. The range is now one pick in the allowances' head, beside Hide accounts, in the app's own menu (Trends: Off / 2 days / Cycle; 走势:关闭 / 2 天 / 本周期), remembered in magpie.quotaRange as the range was. Off draws no curve on any card, the tray panel's thin lines included; the panel follows a pick made in the window at once (storage event) and keeps it after a restart. The cards themselves: an account after a curve's legend wasn't set apart by the rule every other section gets (the next email sat right under the legend), now it is; the plot is 52px rather than 64; a window far shorter than the span drawn (a 5-hour one over a week's cycle, an all but upright comb) is drawn faint and without its upright even burn; a legend key never breaks inside (a long name ends in …); the curve's head names its range in small print instead of holding the control. quota-curve.test.cjs fails on the old code (no pick in the head, a control on every card, no Off, the 5-hour window's even burn over the week, no rule before the second account) and passes: Off removes every curve without moving the page or the pick, survives a reload, and the tray's line goes when Off is set from another window and stays gone after its reload. In WebKit: quota-curve, click-scroll, panel-usage, usage-fold, account-mask and mobile-web pass; before/after screenshots with seven subscriptions (two Claude, two Codex, Antigravity's five windows, GLM, Kimi) in en and zh, window, tray and a 390px web view. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut, timing tests that pass alone. 2026-10-03 19:18:46 +08:00
yetone 1baad0d2d4 plugin: pi packages are plugins as OpenCode's are — a pi coding agent package (its extensions) adds providers to magpie: their models, their sign-in, renewals and requests (owner: 我希望全面兼容 pi 插件!!!!!!!!!!) — a package is pi's when its package.json has a "pi" manifest, the "pi-package" keyword, or pi-coding-agent among its dependencies (IsPi, mirrored in host.js). Adding or updating one installs @earendil-works/pi-coding-agent beside it, and the plugin host loads every pi package through pi's own loader (pi.js: discoverAndLoadExtensions and ModelRuntime, with the credentials kept in plugin-auth.json), so an extension's registerCommand, registerTool and pi.on are taken and left unused, and its registerProvider calls become magpie providers. A provider's models keep their context, output, images and the reasoning levels their thinkingLevelMap allows; its OAuth login runs as pi runs it, each onPrompt asked in magpie's sign-in in turn, onAuth's page opened, a later prompt being the code pasted back; a provider with apiKey takes a key; magpie renews through refreshToken and saves the result. Requests arrive as Anthropic Messages and go to pi's stream: system and tools, images, tool results, thinking budgets and effort as the nearest level the model has (and the least it allows when it can't turn thinking off: Antigravity's gpt-oss refused requests without one), max_tokens held to the model's; the answer comes back as Messages SSE or JSON with thinking and its signature, redacted thinking, tool calls (their thought signatures kept for the next turn), usage, and a failure before anything is said as the vendor's status with Retry-After. The Plugins page's search also finds pi packages that bring a provider. Tried with the owner's real Antigravity account through pi-antigravity, next to the Grok OpenCode plugin in the same host: 14 models listed, Gemini streaming with usage, Claude Sonnet thinking with signatures, gpt-oss, tool round trips on gemini-3.8-flash, gemini-3.1-pro and claude-sonnet-4-6, a 400 and Antigravity's 429 passed on with their status. A Claude Code turn didn't get through: Antigravity answers 429 to any system prompt with Claude Code's billing header or its "Claude agent" line, through magpie's built-in Antigravity just the same. TestPiPackage (a fake pi package: models, an OAuth login asking twice with a wrong code then a right one, a key, streaming and JSON answers, thinking and tool signatures back, a 429, a renewal) and TestSearchFindsPiPackages pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, TestProvidersAnswerWhileListsComeIn and TestSavedButSignedOutClaudeServed, timing tests that pass alone. 2026-10-03 19:12:19 +08:00
yetone a742ee10b3 antigravity: a Gemini reply that says nothing is an error, asked again, not a turn ended as it should (#667: antigravity/gemini-3.8-flash handed pi content [] and zero usage as a normal stop, the agent's run ending mid-task; #636 Bug 3) — Antigravity's Code Assist stream (and any Gemini one) now and then ends with a STOP and no parts, with only reasoning, or with no candidate at all. translate passed the decoder's KStop on and finished the client's stream as it does any reply: Chat's finish_reason "stop" and [DONE], Anthropic's end_turn and message_stop, Responses' response.completed, and a non-streamed reply of 200 with no content — nothing an agent can tell from an answer, and nothing the holdWriter took for a failure, so nobody asked again. Now a reply from a Gemini upstream (CodeAssist, Gemini) holds its end (KStop, KUsage) until something is said in it — text, a call, a search, an image — and one that ended with nothing said, not cut at its length nor by a safety filter (still a refusal, #248), ends with an error event, "<provider>: an empty reply (the model answered nothing; try again)", or a 502 when not streamed. As a 502 before any content it is held, asked again of the same account (lastRetries) or of the next one, and an agent told it retries rather than ends its turn, as Cursor's and Devin's empty replies already are. Other upstreams aren't changed: Claude ending a turn with nothing after a tool's result is its answer. TestGeminiEmptyReplyIsAnError (Factory's Gemini route, which shares the decoder and translate: STOP with no parts, only reasoning, no candidate; Chat, Anthropic and Responses streamed and Chat not; asked again; text and reasoning cut at MAX_TOKENS still go as they came) fails on the old code with the issue's wire shape and passes. Against the owner's real Antigravity account in a sandbox gateway (temp HOME, port 3488): a prompt asking gemini-3.8-flash-low for no output gave, on the old build, finish_reason stop with no content 3/3, and on this one the error event after the retries 3/3; ordinary replies on Chat (streamed and not), Anthropic and Responses, and a bash tool call, are unchanged. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (a first run had TestClaudeAccountNamedAsItsLogin and an internal/gui fetching test fail, timing tests that passed on the rerun). 2026-10-03 19:07:36 +08:00
yetone e61d1da9ce gateway: a model that won't be made to call a tool is asked again with the call left to it (#668: OmO's first turn forces its todo tool, and Claude Opus 5.5 answers 400) — the issue reads the 400 as Anthropic wanting additionalProperties:false on every object of a named tool's schema; it doesn't. Against Anthropic's API (the owner's Claude subscription) the same two tools, schemas without additionalProperties, with tool_choice {type:"tool"} are taken by claude-sonnet-4-5, claude-opus-4-8, claude-opus-5 and claude-sonnet-5, and refused by claude-opus-5-5, claude-sonnet-5-5 and claude-fable-5-1 with tool_choice: type "tool" and "any" are not supported for this model., with or without additionalProperties:false on every object; OpenRouter (Anthropic, Bedrock, Azure, Claude Platform on AWS) answers the same. Anthropic says "tools.N.custom: For 'object' type, 'additionalProperties' must be explicitly set to false" only of a tool sent with strict:true, auto or forced alike, and buildAnthropic sends no strict; the reporter's own capture shows none either, so it is the vendor behind their Claude Pro endpoint holding the forced tool to strict mode. Either way the 400 came from forcing the call on a model that can't be forced, and it was relayed as is, with no fallback. forwardTranslated now asks again when a forced tool_choice (a named tool, or required) is turned away with either error: the call is left to the model (auto), only the named tool offered, as the Claude subscription's bridge does, and the provider isn't sent a forced choice for that model again (unfit, as reasoning-off refusals are). Schemas are unchanged. TestForcedToolChoiceRefused (Chat to an Anthropic-only provider, with each error) fails on the old code. Through a sandbox gateway on an Anthropic-only copy of the owner's OpenRouter provider, a Chat request forcing todo to anthropic/claude-opus-5.5 answered 400 with the old build and a todo call with this one, twice. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. A Chat client on a Chat endpoint (passthrough, e.g. OpenRouter's chat) is not changed. 2026-10-03 19:04:19 +08:00
lizhuo ebcd5281c2 gui: open matching allowance cards from macOS menu-bar quotas (#662)
* gui: navigate menu-bar quota cells to allowance cards

* gui: fix quota navigation in lightweight and folded views

Route quota hits through the existing status-item monitor rather than relying on local monitor ordering. Recreate released panels through panelWin, and expand hidden accounts before focusing them. Keep panel headers non-draggable and cancel pending navigation after five seconds or purposeful scrolling.

Add a native AppKit subprocess regression that fails with the former nil-panel dereference and verifies runtime-queued focus, closing, and recreation. Extend browser checks for folded accounts, expiry, cancellation, and panel drag state.
2026-10-03 19:01:13 +08:00
yetone 8c652a83f7 antigravity: its 429 to Claude Code's system prompt says it isn't a quota (#666: Claude Desktop 调用 antigravity 恒定报 429) — Antigravity answers "Resource has been exhausted (e.g. check quota)" to any request whose systemInstruction holds Claude Code's billing line (x-anthropic-billing-header: …) or the Agent SDK's identity line (You are a Claude agent, built on Anthropic's Claude Agent SDK.), on every model (gemini-3.8/3.7 flash, 3.5 flash lite, 2.5 flash, claude-sonnet-4-6, gpt-oss), whatever the quota left, while the same words in a message go through. Claude Desktop's chats run the Agent SDK and carry both, so they always failed, while its title requests (no system prompt) and Codex on the same account answered. Not the session id (magpie already sends request.sessionId as -<uint63>), thinking signatures (never sent back) or the User-Agent. magpie doesn't hide the lines from Google; the gateway now adds to such a 429 that it is no quota and that Antigravity turns away Claude Code's and the Agent SDK's system prompt, so no one chases a quota. Reproduced on the owner's real Antigravity account with Claude Code 2.1.288 through a sandbox gateway (11/11 429; system field removed 200; either line alone 429). TestAntigravityTurnsAwayClaudeCodesSystemPrompt. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 18:39:29 +08:00
yetone a85b2434ab models: reasoning levels are borrowed again for models Azure also hosts, and a routing group keeps its members' levels when one thinks with none to pick from (ARNO on Discord: routing group在接入harness的时候,没有设置reasoning_efforts。我发现是之前很多能自动推导reasoning_efforts列表的模型现在都推导失败了) — the Azure OpenAI preset (c35471cf, Catalog "azure, openai") was taken for a maker, so models.dev's azure list was the maker's word on every model it hosts: Kimi K2.5, DeepSeek V3.2 and R1, Grok 4.20/4.1 Fast Reasoning, listed there with no levels, took none on a relay or custom provider (and the gateway capped them at high as levelless), where the providers listing any give low…high, none/high and the rest. Azure is now marked Hosts, as Groq and Ollama Cloud are; its own provider still takes its own list's word. And groupEntries narrowed a group to the levels every member has, so one member that thinks with no levels (a thinking switch, a Claude 4.5 with a budget alone, OpenRouter's qwen3.8-omni-flash) left the group none and dsh's route wrote it without reasoningEfforts; such a member is now skipped as an unknown one is, since the gateway sends it the effort asked, no more than high (fitFor). The same Entry.Efforts feeds dsh, Codex's catalog, Pi and OpenCode. On a copy of the owner's config and models.dev cache: no plain model loses levels; group/auto-claude-haiku-4-5 and -sonnet-4-5 get low/medium/high/max again (they had them on 09-30), group/auto-qwen3-8-omni-flash none/low/medium/xhigh; across models.dev, the 34 ids Azure emptied (kimi-k2.5, deepseek-v3.2, deepseek-r1, grok-4-20-reasoning …) borrow again. TestHostListIsNoMakersWord (provider) and TestDshGroupHasItsMembersLevels (agent: the group's dsh entry has reasoningEfforts and the thinking field offers low/high/max) fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 18:17:11 +08:00
yetone dd03e431c1 mcpauth: a sign-in asks for offline_access when the authorization server offers it, so Vercel's MCP stays signed in (#615: "vercel老是掉"; Neon, Notion and Supabase stay) — mcp.vercel.com's resource metadata names only the scope "openid" (its 401 names none), and magpie asked for exactly that; its authorization server, https://vercel.com, is an OpenID provider whose scopes_supported lists offline_access, and Vercel's docs say that scope is what issues a refresh token (access tokens last an hour; refresh tokens 30 days, single use, rotated). So magpie's Vercel sign-in had no refresh token, and an hour later Token found it due with nothing to renew it by and marked it dead: "sign in again". Neon (no scopes, refresh by default), Notion ("default") and Supabase (its own scopes) give a refresh token without it. Now discovery adds offline_access to the scope asked for (authorize and registration) when the authorization server's metadata lists it and the scope named any; a sign-in that names no scope keeps the server's default. A rotated refresh token was already saved. The test fake gains OpenID, a Vercel-like server (resource names only "openid", no scope in the 401, refresh token only with offline_access); TestSignInAsksForOfflineAccess fails on the old code (scope "openid", no refresh token, ErrExpired at the first renewal) and passes. A Vercel sign-in made before this has no refresh token and has to be made again once. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn and TestUserPath, timing tests that pass alone. Not tried with a real Vercel account. 2026-10-03 18:09:21 +08:00
yetone 9416a6040a site: the Omarchy intent hit shot reads as its caption — the classifier in 964 ms, glm-5.3 in 5.6 s (owner: 改一改吧) — the shot said 961/972 ms and 5.5 s; re-shot in tokyo-night and catppuccin-latte, retried until the routing card said 964 ms and 5.6 s, en and zh from the same request. Same sizes, so SHOTS is unchanged. 2026-10-03 18:07:42 +08:00
yetone a345af91e5 gui: a plugin card's Update chip updates the plugin (莫 on Discord: 显示更新但是好像没有更新按钮) — on Plugins › Discover a card whose plugin has a newer version showed an "Update" chip that looked like a button and did nothing (a click opened the plugin's page); the Update button was only on the Installed tab. The chip is now that button: it names the version ("Update to v0.0.9" / "更新到 v0.0.9"), updates the plugin without opening its page, shows Updating… meanwhile and goes once it is current, and is disabled while another plugin action runs or the plugin is off. plugin-card-update.test.cjs (en, zh) fails on the old code (no button) and passes; plugin-updates, plugin-market and plugin-check-updates still pass (WebKit). 2026-10-03 18:04:41 +08:00
yetone 191700c4fa gateway: a reply that only thought and then failed is the failure, not an empty 200 — a client asking for the whole reply (not a stream) got 200 with null content and zero usage when the provider thought, then failed with nothing said: the collector's error counted only when no parts came at all, and thinking is a part. Cursor's plugin ends such a turn with "an empty reply" (composer-2.5 with no system prompt, 1 in ~5 on a 45k-token prompt): streamed, the client saw that error; whole, an empty answer it didn't ask again. Now an error with nothing but thinking before it is answered as the error (502, or its own status), in translated replies and Claude Code's. TestThinkingThenErrorIsAnError fails on the old code. Against the real Cursor account in a sandbox: 8 whole requests (chat and Messages) to cursor/composer-2.5, the one that only thought answers 502 "Cursor: an empty reply", the rest as before. Also found while testing Cursor's overflow: a 303,700-token prompt to composer-2.5 / gpt-5.2 (200k) comes back 400 context_length_exceeded after one try (Cursor answers 502 "No system prompt found"); 34k–52k prompts answer with usage. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 18:01:00 +08:00