Repository navigation
Conversation
yetone
left a comment
There was a problem hiding this comment.
Thanks for this. The package is careful in a lot of places: every call goes to Mistral's own hosts (api.mistral.ai, console.mistral.ai), there is no client mimicry (no User-Agent or CLI headers are sent), the keys stay in magpie's store, an unknown key gives a readable 401 instead of a request, and the README's disclaimer is clear. I reviewed it at fa3cb09 and can't merge it yet, for two reasons.
1. License: the request and reply fixes are a translation of PolyForm Noncommercial code, shipped as MIT.
The README says the fixes are "Ported from mistral-gateway". That repo's LICENSE is PolyForm Noncommercial 1.0.0, with Required Notice: Copyright (c) 2026 Classic298. The port follows gateway.py function by function:
snapEffort(src/provider.mjs:89) issnap_reasoning_effort(gateway.py:385): the same "is not enabled" / "is not supported" branches, the same'level'scan and the same tie rule (low and below lean down, medium and above lean up).normalize.mjsmirrorsnormalize_content_chunks,normalize_stream_eventandnormalize_completion(gateway.py:273-380).- The 429 loop has the same 10 s interval, and the
max_tokensfill has the same 131072 / 32768 constants (gateway.py:694).
This package's LICENSE is MIT, and it carries neither PolyForm's terms nor the Required Notice. Only Classic298 can license that code under MIT, and PolyForm Noncommercial doesn't allow a commercial use that MIT would permit. So the plugins repo can't publish it as it stands. Either of these would fix it:
- Classic298 agrees, on the record (an issue or comment on their repo is enough), to license the ported parts under MIT. Link it here.
- Write those parts again from the measured behaviour (the 422 fields, GLM's 4096 cap, the typed content parts), without following
gateway.py, and say so in the README in place of "Ported from".
2. A 429 is retried inside the fetch for up to 60 s, so magpie never sees it.
magpie rests an account and falls back to the next one in the group when a request gets a 429. This loop (src/provider.mjs:186-194) answers the 429 itself, so for up to a minute the request just hangs and nothing falls back. With Retry-After: 0 it retries every 100 ms. I ran this on mote against a stand-in that always answers 429 with Retry-After: 0: 20 upstream requests in the first 2 s, which is about 600 in the window. If Mistral answers a spent budget with 429, every request on that account waits a minute before failing. Please return the 429 as it came. The Retry-After header already tells magpie when to try again. The repo's other plugins leave 429s to magpie the same way, apart from narrow cases with a test.
Smaller points, none of which blocks on its own:
- The two wallets share one account, so when magpie rests the account after a refusal on one budget, the other budget's
--apiids are rested too. Saying this in the README is enough. - Lines 214 and 231 rebuild a Response from decoded text but keep upstream's
content-encodingandcontent-length. magpie's host removes both (internal/plugin/host.go), so magpie is fine. In OpenCode, a reply that was gzipped would carry a wrong length. Copying the headers and deleting both, as line 227 already does, covers it. mistral-vibe.test.mjshas notest()calls, sobun testprintsRan 0 testseven when it passes. A failing assert still exits 1, but wrapping the blocks intest()would make the count mean something. Its header comment also saysnode test.mjs.
What I ran (mote, macOS, Bun 1.4.2, Node 22, HOME and TMPDIR in the run's own folder)
bun testin packages/mistral-vibe: exits 0, "ok: 8 checks passed" (0 counted tests).node mistral-vibe.test.mjs: same.bun scripts/check.mjs mistral-vibe: registry.json and mistral-vibe pass.opencode2.mjsis byte-identical toscripts/opencode2.mjs.- The 429 probe above.
- npm:
@magpie-community/opencode-mistral-vibe-authdoesn't exist yet (404), so the name is free. - Read
gateway.pyandLICENSEat Classic298/mistral-gateway's default branch, pushed 2026-11-07.
|
Updated in fd85cb3 — both blockers addressed. 1. License. The request and reply fixes are now written from the measured behaviour, not translated from mistral-gateway. 2. 429. Returned as it came, Smaller points:
Checks: |
|
Thanks — both blocking points are addressed in fd85cb3. 1. License. I took the second route: the fixes are now written from measured behaviour, not translated from
2. 429. The retry loop is gone. A 429 is returned as it came, with its Smaller points:
Checks on this commit: |
yetone
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. I re-reviewed at fd85cb3. The 429 fix is right, and so are the smaller points. The license blocker isn't fixed yet, so I can't merge this.
2. 429: fixed. sendChat (src/provider.mjs:190) sends once, and a 429 comes back at line 208 as the upstream's own Response, with Retry-After and the body unchanged. A 429 on the effort retry (line 201) keeps its headers through cleanHeaders. I broke it two ways on mote and both went red on "a 429 is returned as it came, once": a second send after a 429, and a 429 rebuilt without its headers (Retry-After lost). Each run gave 8 pass / 1 fail.
1. License: still a translation of gateway.py.
git diff fa3cb098 fd85cb34 -- packages/mistral-vibe/src changes names and comments in the port I flagged. The structure and logic are the same:
prepareBody(src/provider.mjs:16-73) has the same statements in the same order as the handler in gateway.py:684-739: alias,stream_optionson a stream,max_completion_tokens→max_tokensonly when not null,seed→random_seed, dropuser, the GLMmax_tokensfill, stripreasoning_contentandindexfrom every message, thenreasoning_effort = "high"under the same five conditions (no effort, "glm" in the model, tools present, last messagerole: "tool"). The comments are paraphrases: "A reply sent back as it came carries these; Mistral 422s them" (provider.mjs:54) is gateway.py:720's "Replies carry these fields, and Mistral 422s when a client sends a reply back as it came". Between the two commits the only changes areparsed→body, the reworded comments, and dropping the 32768 default.snapEffort(provider.mjs:88-122) issnap_reasoning_effort(gateway.py:396-425) step by step: the samereasoning-object/effortvsreasoning_effortowner pick, the "is not enabled" branch gated on a non-null request, a split at the first "is not supported", and the same three-way early return (requested not a level, already listed, or nothing listed). Only the tie loop is written differently.splitPartsandrewriteTurn(src/normalize.mjs:20-47) arenormalize_content_chunksand the per-choice body ofnormalize_stream_eventandnormalize_completion(gateway.py:284-394), down to thesaw_tool_callflag that turns a later "stop" into "tool_calls".MODEL_ALIASES(src/constants.mjs:18-31) is gateway.py:56-69 entry for entry, and constants.mjs:16-17 still says "Taken from mistral-gateway". index.mjs:3-5 still says "the gateway's request/reply fixes live in the provider's fetch".
Renaming a translation doesn't make it an independent work. The new README line "They are not a translation of another implementation" (README.md:117-119, and the zh-CN README) is contradicted by this branch's own history, and by constants.mjs and index.mjs in the same package. That's worse than the old "Ported from", because it tells users the license question is settled when it isn't.
Either route from the first review still works:
- Ask Classic298. A comment or issue on their repo agreeing to MIT for the parts you used is enough. Link it here, and put back the "Ported from" credit with that permission.
- A real rewrite. Start from the measured facts written as a spec (which fields get a 422, GLM's 4096 cap, the typed-part shape, the effort error's text), not from gateway.py or the earlier port. Doing that in a fresh file without the old one open is the honest way to get there. The alias table is plain facts, so it can keep a credit line. Then the README can say what it says now.
Smaller points (none blocks on its own)
- The tie rule changed.
preferLower(provider.mjs:111) is true for "medium", so medium with only low and high allowed now snaps to low, where gateway.py, and this PR before, picked high. If that's deliberate, say so in the comment at line 86 ("below medium" reads as excluding medium). If not, use<. No test covers a tie. - The usage test is named "both budgets, the key's own first", but readQuota (src/quota.mjs:107-108) always puts the Vibe window first and marks the other one
aside. The test checksaside, so the code is fine; only the name is wrong.
Everything else I read holds: credentials (keys stay in magpie's store, Authorization is reset per request, short placeholders such as "None" aren't taken as keys, and nothing is logged), request shaping, the --api id split, model listing and dedupe, the usage windows, and the 401 → signIn: "expired" path.
What I ran at fd85cb3 (mote, macOS, Bun 1.4.2, HOME=$(mktemp -d))
bun testin packages/mistral-vibe: 9 pass, 0 fail.bun scripts/check.mjs mistral-vibe: ✓ registry.json, ✓ mistral-vibe.opencode2.mjsis byte-identical toscripts/opencode2.mjs.- Repo-root
bun test: exit 0, 6921 pass, 20 skip, 0 fail across 502 files. - The two hand-made 429 mutations above: each 8 pass / 1 fail, red on the 429 test.
- Diffed fa3cb09..fd85cb3, and compared it with Classic298/mistral-gateway's gateway.py at 356278b5 (pushed 2026-11-08). The LICENSE there is still PolyForm Noncommercial 1.0.0.
|
Rewritten from the measured facts in ab90d59, in new files, with the old port deleted.
Also:
Checks at ab90d59: |
|
Opened Classic298/mistral-gateway#2 asking whether the adapted parts may be licensed under MIT: Classic298/mistral-gateway#2 If they say yes, I will credit them and link the issue from the package README and LICENSE. Until then, ab90d59 is the rewrite from measured behaviour, and the alias table keeps its plain-facts credit. |
Unofficial plugin for the Mistral Vibe plan (Vibe Code), for magpie and OpenCode.
mistral-vibe; one sign-in holds both keys:vibe=…;api=…(the Vibe key alone works)./v1/model-catalog?surface=vibe-code, 4 models measured 2026-11-08) plus DeepSeek. Each model is offered on both wallets: the bare id spends the plan's Vibe budget,<id>--apispends the API budget. The suffix is stripped before the upstream request; the fetch picks the key from it./v1/billing/budget(vibe_budgetandapi_budget, reset),/v1/billing/subscriptionandconsole.mistral.ai/api/vibe/whoami; both windows are shown, the key's own first. A 401 marks the account expired; a 403 or a plan without a budget says so instead of an empty card.max_tokensfill, GLM typed content parts tocontent/reasoning_content, tool-call streams endingtool_calls, 429 retry with Retry-After,reasoning_effortsnapping.README.md(English) andREADME.zh-CN.md(Chinese); registry summary in en and zh.bun scripts/check.mjs mistral-vibeandbun testin the package pass.Disclaimer: unofficial, not affiliated with Mistral AI. It uses the credentials and endpoints the official Mistral Vibe CLI uses; the plan's limits and Mistral's terms still apply.