7 Commits
Author SHA1 Message Date
yetone 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.
2026-10-10 01:30:29 +08:00
yetone 0361189f9e test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass. 2026-10-01 23:15:07 +08:00
Comic Changandyetone 024f494bc4 Allow provider-specific upstream model names (#280)
A provider may serve a model under an id of its own: namespaced with a
prefix, dated, a `-latest` that is not what models.dev calls it, or simply
a different spelling. magpie sends the name it knows the model by, and the
provider answers that it has no such model.

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

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

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

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

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

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

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

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-01 13:03:57 +08:00
Comic Chang 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.
2026-10-01 02:14:22 +08:00
Comic Chang 901ee08280 Count usage at the configured provider/model price (#278)
Magpie reports models.dev's list price for every call. That is the right
number for a vendor's own key and the wrong one for a provider that
resells or exposes a model at a different tariff, and it leaves a model the
catalogue does not list with no cost at all — a provider's own /models
gives ids, not economics.

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

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

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

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

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

Validation happens where the entry is written, not in Save: one hand-edited
bad price in the file must not make saving a theme or a window size fail. A
price for a model the provider does not serve is refused the way
`magpie model name` refuses the same id, since it is a price that never
applies and nothing reports it as dead; a removal is exempt, so an entry
left for a model that has since gone can still be taken away.
2026-09-30 20:31:11 +08:00
yetone c3052fe43c test: magpie model efforts gives a model whose reasoning levels aren't known the ones typed — TestModelNameCmd still expected it refused, as before c1d38cc 2026-09-27 11:51:36 +08:00
yetone 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 2026-09-26 23:23:45 +08:00