main
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7f62dbf3ed |
gateway, agents, gui: a model's reply limit is published within the window beside it, on /v1/models, Cursor's and Muse's lists, the Gateway page and magpie model output (#1438, yhong91)
#1438 (yhong91): GET /v1/models listed grok/grok-4.7 with context_window 256000 (the Grok backend's own) and max_output_tokens 500000 (models.dev's x-ai entry, which catalog.Decorate fills in when the backend gives no output). A client that reads both sizes its replies past what the window holds. The agents' files already kept output within the window (maxTokens, #338/#347), but the gateway's lists, the GUI and the CLI did not. What changed: - provider.OutputWithin(window, output) and Entry.PublishedOutput(): the output is kept within the window; an unknown window or output (0) is left as it is. - Every place that publishes the pair uses it: modelObject (/v1/models), cursorLocalModel (Cursor Private Inference), Muse Code's list, the Gateway page's models and groups (providerInfo, providersState), the agents' maxTokens, and `magpie model output` (which also says "500k, kept within its 200k window" when it lowers one). - Entry.Output stays the model's own limit. The request-path cap (outputLimit/withMaxOutput) and the user's Max output setting are unchanged, so no request is lowered further than before. This is why the clamp is not in replyLimit/entryFor as the issue suggests, and why the commenter's patch (modelObject only) was not taken as is: it was right but missed Cursor, Muse, the GUI and the CLI. - docs/subsystems/gateway-routing.md: the rule and its routes. Tests (each fails without the change): - gateway TestModelListsKeepOutputWithinTheWindow: models.dev 500000 over a backend 256000 window, plus output within the window, unknown window and unknown output, on /v1/models, Cursor's capabilities and Muse's limit; a group request with max_tokens 300000 still reaches upstream as 300000. - provider TestOutputWithin, gui TestProviderModelOutputWithinTheWindow, TestModelOutputCmdWithinTheWindow. Breaking it by hand: OutputWithin returning output unclamped turns all of them red, plus agent TestMaxTokensWithinContextWindow; gateway sites back on e.Output turn the gateway test red on all three lists; moving the clamp into Entry.Output turns the fixture and the group-request guard red. Real data: an offline dump of the owner's catalog (copies of ~/.config/magpie and its caches, 203 entries) has 3 models with output over their window, grok/grok-4.5, 4.6 and 4.7 (256000 vs 500000), now published as 256000; 0 are not Grok. The live gateway on 3425 was not running, so /v1/models itself was not read, and no real Grok request was made. Run: gofmt -l; go vet -tags nogui ./... on darwin, linux and windows; go test -tags nogui ./... -count=1: all green except TestLoopEndsWorkBuddysReply/plugin/anthropic ("let go only after 3207 lines", bound 2000) at load average ~35 with other suites running; it passes 5 of 5 alone and does not touch the output limit or request path. |
||
|
|
58ba737811 |
remote magpie: a remote magpie's model prices are this magpie's list prices for its models, so both Usage pages count a call at one cost (Sorghum on Discord: 希望本地 magpie 可以自动集成 remote magpie 手动设置的模型价格)
What changed
- The local magpie asks a remote magpie's /v1/models with a new header,
X-Magpie-Prices (provider.PricesHeader, beside the Drawers and
Videomakers headers in listHeaders).
- The remote answers each model's "magpie_price": what its own Usage page
counts the model at (provider.EntryPriceIn: the price its user set by
hand, else its provider's list price at its price rate; a routing group
only when every member costs the same). EntryPriceIn is agent/route.go's
entryPrice moved into provider unchanged, so the agents' cost and the
remote's price are one rule.
- It tells prices only to a request from its own computer or one let in
with a gateway key (local || sharedWith), and only when asked: a machine
let in without a key (MAGPIE_ADDR open, not shared) and an older magpie
or an agent that sends no header get nothing new.
- The local keeps the price with the fetched list (catalog.Model.Price; read
raw, so an odd value or a negative part drops the price and keeps the
model and the list) and uses it as that provider's list price
(ListPrice -> remotePrice). A price the local user set (provider/model,
provider/*, */model) still comes first, and the local provider's own
price rate still applies on top.
- Old remote <-> new local: no field, models priced as before.
Old local <-> new remote: no header, nothing new sent (and an unknown
field would be ignored by encoding/json anyway).
- The model editor's price tooltip and `magpie model price` say the greyed
price is what the other magpie counts the model at (remoteList; new
string in zh, zh-TW, ja, de).
- Side effect: smallModel/cheapest picks over a remote's models (image
description, web search) now see these prices, where every remote model
was unpriced before.
- docs/subsystems/providers-accounts.md: a row for it.
What was run
- New TestRemoteMagpiePrices (real gateway handler behind lanGuard as the
remote, a local remote-magpie provider fetching it): the remote's hand-set
price is the local's price; a real chat call through it costs $9 on both
the local's and the remote's usage records; the local's own price wins
and survives a refetch; an old remote's list leaves the model unpriced.
New TestRemoteMagpiePricesTold: priced for loopback asking, not without
the header, not for a keyless LAN caller with MAGPIE_ADDR open, priced
with a gateway key. New TestLiveMagpiePrice in catalog.
- Broken by hand, each red: no remotePrice in ListPrice (FAIL: priced
false); remote price over the user's (FAIL: remote's price won); the
local/sharedWith gate removed (FAIL: keyless machine priced); the header
gate removed (FAIL: told to a list that didn't ask); sane() always true
(FAIL: a/negative priced). A string-typed magpie_price first failed the
whole list ("not a model list"), hence the raw read.
- gofmt -l clean; go vet ./..., GOOS=linux go vet -tags nogui ./...,
GOOS=windows go vet ./...: clean; go build, GOOS=linux -tags nogui and
GOOS=windows builds OK; go test -tags nogui ./... green under a temp HOME
with pinned caches.
- GUI: model-price-edit (new remote-title check), model-price-tiers,
provider-price-rate, gui-ja, gui-de, gui-zh-tw, i18n-keys: 18/18 in
Chromium and WebKit; the title check goes red with the remoteList branch
removed.
- Not tried between two real magpie binaries on two computers, nor in the
real window.
|
||
|
|
3a30ac87c2 |
catalog, gui: a 1-hour cache write is 2× input for a Claude model only, any other's unset is its 5-minute price and isn't shown (PAMI on Discord)
v0.1.1032 counted a 1-hour cache write left out at 2× input for every model, so gpt-6-astra priced 10,50,1,12.5 showed "$20 (1 hour) at 2× input", a write OpenAI has no price for. OneHourFor gives a Claude model's price, listed or given by the user, its 2× input; any other model's 1-hour writes count at the 5-minute price, magpie model price shows only its cache write, and the app shows the 1-hour box only for a Claude model or one priced with one. |
||
|
|
71d2da1f42 |
catalog, usage, gateway, gui: a price can have a long-context tier and a 1-hour cache write, and the price boxes take decimals (PAMI on Discord)
OpenAI bills gpt-6-astra, gpt-6.1-sol and gpt-6-luna over 272K input at 2× input and cache and 1.5× output, for the whole request. A price now has tiers: `magpie model price <m> 10,50,1,12.5 --tier 272k 20,75,2,25` (--tier can repeat). A tier applies when input + cache reads + cache writes is over its size, so 272,000 is not over and 272,001 is. The built-in prices take tiers only where models.dev lists one (cost.tiers of type "context"), and it does list one for those three. The ledger, each request and OTLP are costed at the tier the call reached. Session and day totals sum many calls, so they stay at the base price.
Anthropic bills a cache write kept for an hour at 2× input and one kept for 5 minutes at 1.25×. Its usage reports which is which in cache_creation.ephemeral_5m/1h_input_tokens. The gateway (API and Claude subscription) and Claude Code's transcripts now keep that split. A price's fifth part is the 1-hour write. When it isn't given, it is 2× input: that is Anthropic's rule, and only Anthropic reports 1-hour writes. Claude list prices carry the 2× too. cache_write_tokens is still the total, and the usage CSV adds cache_write_5m_tokens and cache_write_1h_tokens at the end. The app shows the split on a request's Cache and on the usage tooltips. Old prices and old records, which have no 1-hour part, read as before. The sessions cache (18) and the call cache (calls-v8) are re-read once.
In the GUI, the price boxes were number inputs and dropped what they didn't parse ("0,25"). They are now decimal text boxes that take both "0.25" and "0,25". A model's price row has a 1-hour box (empty means 2× input) and a quiet Long-context price row. That row shows the list's tier greyed in, or sits behind a link when the list has none. A bad size is refused by name. en/zh/ja/de.
Tests:
- New: TestPriceTiers, TestCostSplit, TestPriceJSON, TestModelPriceTiers, TestModelPriceTierCmd, TestAnthropicCacheWriteSplit and TestClaudeCacheWriteSplit. The ledger CSV test expects the new columns.
- model-price-tiers.test.cjs is new; model-price-edit and provider-price-rate are updated. All of them fail against the old assets and pass in WebKit. Chromium couldn't be launched here.
- go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
|
||
|
|
1aaac93b52 |
provider, gui: Compact at — where Codex and Claude Code compact a long conversation, set per provider or per model in its editor and to any size in Settings → Long conversations (#876, hisiling)
Until now the only choice was 272K for every model or Full window for every model. The 272K default stays as it is (
|
||
|
|
531dc0aa33 | claude: a Claude model runs to its own window in Claude Code, not 272K, and magpie model compact on|off is the Long conversations switch from the terminal (Max on Discord: a [1m] model's context stopped at 272K, magpie wrote CLAUDE_CODE_AUTO_COMPACT_WINDOW back when it was taken out, and there seemed to be no way to change it) — on magpie, Claude Code was told CLAUDE_CODE_AUTO_COMPACT_WINDOW=272000 whatever its model, so a [1m] Claude model compacted at 272K, though Anthropic runs it to 1M (the cap was meant for other vendors' long windows: Chen's DeepSeek turns of ~550K). Now a main model that is one of Anthropic's Claude models, from whichever provider, has the env left out (magpie's own value taken away), and Claude Code compacts at the model's own window; another vendor's model of a longer window keeps the 272K cap unless Settings → Long conversations is on Full window. A value the user writes there is still kept. The switch was only in the app's Settings; magpie model compact [on|off] (alias full-context) shows and sets it, and the Settings row's description says how Claude models are treated (en, zh, ja, de). TestClaudeCompactWindow (a Claude [1m] model gets no 272000, set and synced; another vendor's after it is capped again) and TestModelCompactCmd fail on the old code. The Long conversations row read right in WebKit, en and zh. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestClaudeFailureKeepsClaudeCodesStatus flaked once under load, passes alone). | ||
|
|
4547dafe95 |
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
|
||
|
|
e001bd883a | usage: a model spelled with a dotted version is priced at its maker's, and a model can be priced from any provider (tony on Discord: old sessions' copilot/claude models had no known price) — Copilot and its CLI's sessions name Anthropic's models claude-opus-4.6 and claude-haiku-4.5, models.dev's anthropic entry claude-opus-4-6 and claude-haiku-4-5, and a session's bare model was looked up only by its exact key at its maker, so neither was priced; a gateway call whose provider is gone or lists no price fell to the same miss. A price is now looked for with a dot and a dash taken for the same, after every exact match. gemini-3-pro-preview stays unpriced: Google's entry on models.dev no longer lists it and no price is made up. For it, and any model like it, magpie model price <model> <in>,<out>,<cache read>,<cache write> keeps a price under */<model> that counts from any provider not priced by the user, after <provider>/<model> and <provider>/* and before any list price, and for a session's bare model; --reset takes it away, magpie model prices lists it. TestDottedIDPricedAtItsMakers, TestAnyProviderPrice and TestPriceOfDottedAndAnyProvider fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. | ||
|
|
024f494bc4 |
Allow provider-specific upstream model names (#280)
A provider may serve a model under an id of its own: namespaced with a prefix, dated, a `-latest` that is not what models.dev calls it, or simply a different spelling. magpie sends the name it knows the model by, and the provider answers that it has no such model. magpie model wire relay-b/model-2 vendor-c/model-2-preview magpie model wire 'relay-b/*' 'vendor-c/*' magpie model wire relay-b/model-2 --reset A `*` in the **value** stands for the model, so `relay-b/*` set to `vendor-c/*` sends `vendor-c/model-3` for model-3. A value with no `*` sends every model of that provider under one name, which is the case where a relay really does serve them alike. The CLI shows the pattern the way it was given, with a concrete line for one of the models, so what is in force is not something you have to imagine. An exact key wins over the provider's. The canonical id stays canonical. `Entry.Model` is not rewritten: it is the key automatic routing groups are built from, what `Resolve` returns as the model's identity, what `GET /models` lists, what an agent asks for, and what a call is recorded and priced as. The alias goes into the request's model field and nowhere else — which is the part that was wrong at first. Handing it to the serializer gave the provider's name to the code that decides what a request should look like: `buildChat` checks the model for Gemini compatibility, `buildAnthropic` matches it for adaptive-only and effort handling, `buildCodeAssist` maps by model. A provider serving a model under a namespaced id got a request shaped as if it were that other model. So the request is shaped from the model magpie knows and the field is rewritten once at the end, with `rewriteModel` — which already existed to put one provider's own model name in a body and keeps every other byte as it was. It has to reach every path. A request the provider understands as it came is relayed by `passthrough` without being rebuilt, and the Anthropic token count is asked of the provider directly; both rewrite the field too, so on a provider speaking the client's own protocol — the common case for a relay fronting one vendor — the alias still applies. "Swapped" compared the canonical id with what the vendor's reply named, so an alias that is not a prefix or a dated variant marked every call swapped. It compares the name that was actually sent, in the gateway and in the ledger alike; the ledger reads the map once for the lot, since its rows are what a reader sets beside a bill. The Codex backend path is left alone: it sends magpie's own pick and never an alias. `provider.Test` asked with the canonical id, so a provider that only accepts the alias reported a failure while its real requests worked. It asks with the alias now and still reports the canonical id, since that is the one the user picked. A blank name was a hole: the setter trimmed what it was given, so a value of only whitespace was removed on the way in, while the read path would have used it had anything else written it — a hand-edited file, or a build from before the trim. Both ends treat a blank as no name now. The key is checked where it is written rather than in Save, so one hand-edited entry cannot block every save, and #288's walk moves it on a provider rename without a line here. Co-authored-by: yetone <yetoneful@gmail.com> |
||
|
|
49f40b463f |
Configure per-model context windows and output limits (#279)
A provider that serves a model the catalogue does not list, or lists at the wrong size, has a window and a reply limit magpie cannot know: both fall back to whatever the catalogue says about the model's maker, which for such a provider is nothing at all. magpie model context relay-a/gpt-5.5 262144 magpie model output relay-a/gpt-5.5 131072 magpie model context 'relay-a/*' 200000 magpie model output relay-a/gpt-5.5 --reset Both are looked for in the same order as each other: this model, then `<provider id>/*`, then the provider's own list, then the catalogue. A model's own value wins over its provider's. The reply limit is new; the window is not — `Provider.Contexts` has been there, with a `"*"` of its own, settable through `magpie provider set <id> context.<model>=`, and this reuses it rather than shadowing it. That reuse is the point rather than a detail. A context is a routing input and not only a number agents are shown: at 95% of a held member's window a request moves to a member that takes more, so two sources of truth would mean the gateway routing on one number while agents are told another. `magpie model context` is a second way to set the same field, not a second field. Both reach the agents. Pi keeps a model's window and reply limit in its own models.json, OpenCode keeps its limit, and Crush, Droid, Cline, Qoder and Zcode keep their own; an agent reads those files when it starts, so a limit set here that never reaches them is a limit that does not take effect. `magpie provider set` has always told the agents, and this behaves the same way rather than introducing a second, quieter path. The test asserts each setter reaches them exactly once — the two paths differ (one saves the provider file, the other settings and then says so) and that difference is worth pinning. An end-to-end pass confirms it: with a Pi models.json in place, `magpie model context relay-a/sol 999999` writes `contextWindow: 999999` into `providers.magpie.models`. A `0` is refused rather than read as a reset, naming `--reset`; a mistyped model id is refused the way `magpie model name` refuses the same id; a reset says what is actually in force instead of claiming to be back to the vendor's list while a provider-wide value still applies; and `magpie model context <provider>/*` reports that value rather than saying the provider serves no such model. Validation happens where the entry is written, so one hand-edited bad key cannot stop a theme or a window size from saving, and #288's walk moves these on a provider rename without a line here. |
||
|
|
2c432fe3fa | routing, models: a rerouted request's tooltip names who answered it (#337), and model names can go without their provider (#335) — the Routing page's and the tray panel's rows named the member that answered a rerouted request, but their tooltip still said the path first resolved; it now says the provider · account · model that answered, and on a second line what it was rerouted from. A setting, Settings → Preferences "Provider in model names" or magpie model suffix off (on by default, as before), writes the models listed to Codex and the other agents (OpenCode, Pi, omp…) by their name alone, except two in an agent's list that would read the same, which keep theirs. routing-reroute-title.test.cjs and plain-names.test.cjs (Chromium, WebKit, en/zh), TestPlainNames, TestPlainNamesReachAgents and TestModelSuffixCmd fail without the change; click-scroll, old-webkit, currency, text-size, panel-routing, routing-served, routing-steady pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. | ||
|
|
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. |
||
|
|
c1d38cc50f | effort: Claude Code can start at max — settings.json's effortLevel keeps up to xhigh (/effort max lasts a session), so magpie keeps max as CLAUDE_CODE_EFFORT_LEVEL in its env, which every session asks for (tested: Claude Code then sends output_config.effort max), and another level takes it away. A model whose reasoning levels aren't known (one models.dev doesn't list, a custom provider's) can be given some of none…max on the Providers page or with magpie model efforts, which the lists agents get and the gateway then take it to have: a request's effort is fitted to them, max sent as max | ||
|
|
47e12df418 | models: a provider's model can go by a name of the user's, and offer only some of its reasoning levels — Claude Opus 5.5 · Claude Code can be Opus 5.5, and a model with six levels can offer just low, medium and high. Both are kept in settings.json by provider/model (modelNames, modelEfforts), apart from the vendor's list, so a Refresh, a restart or an upgrade leaves them, and the same model from another provider keeps its own; a provider renamed takes them along. They are applied where the catalog is made, so the gateway's /v1/models, the lists magpie writes into the agents (Codex's model catalog, ZCode, Pi, OpenCode, OMP …) and magpie's own pages all show them, and a change rewrites the agents' lists as any other change of the catalog does. Only what is shown changes: the id, where requests go, and the levels the vendor is asked at stay as they were; a found routing group keeps the model's own name. The provider's editor has Names & levels under its models — a name typed in place, a tick per level, Restore default — and the terminal has magpie model name <provider/model> <name>|--reset and magpie model efforts <provider/model> low,medium,high|--reset. Issue #92 |