Changelog
Every release.
What shipped, what was fixed, what was verified. The last 10 entries — pulled live from the kRouter repo CHANGELOG.md.
Latestv0.5.1642026-10-07
Releases on GitHubOctober 2026
Zenith stops preferring your last account, Anthropic spend caps stop costing a request each time, and Cline works through kRouter again
**Everyone with more than one account on a provider: upgrade.** Zenith, the default routing engine, was meant to pick the healthiest account. Two bugs turned it around. An account whose provider reports no quota (almost every API-key account) was scored as if it had none left, cutting its health score to a tenth. And the list-position bonus ran backwards: priority 1 is the top of your list, but the bonus was 10 points per position, so the bottom account got the most. With health cut to a tenth, that bonus decided the pick, and Zenith tended to send traffic to the last account in your list. **Cline users: click Apply again.** The dashboard's Cline setup removed `/v1` from the base URL, so every Cline request went to a path kRouter does not serve and returned 404. The Cline CLI also ignored Apply entirely once it had been set up, because current Cline reads a different file. ## Fixed - **Zenith picks the healthiest account again.** An account with no quota data is no longer penalised, and the list-position bonus now favours the top of your list: 40, 30, 20 and 10 points for the first four accounts, nothing below. On a score of about a thousand that only settles near-ties. The dashboard's Zenith view uses the same numbers the router does. - **Anthropic's monthly spend cap holds the account until it resets.** When an organization reaches its spend cap, Anthropic answers `429` with `enforced_spend_limit_reached` and no `retry-after` until the date it names. kRouter treated it as an ordinary rate limit and retried the account every few minutes for the rest of the month, spending a round-trip on it before every fallback. It now locks the whole account until that date (or 00:00 UTC on the 1st of next month) and falls back at once. A spend limit you set yourself (a `400` saying "You have reached your specified API usage limits") is held the same way. The connection shows why, and a successful Test connection clears it early, for example after you raise the limit. - **An expired per-model cooldown could hide an account-wide lock**, so a locked account kept being picked for the model it last failed on. The longer of the two locks now applies. - **OpenRouter's free-model daily cap holds that model until midnight UTC.** The `free-models-per-day` 429 was treated as a per-minute limit, so kRouter retried the model about every 90 seconds until the day reset and said the daily quota was healthy. It now parks that model on that account until the reset OpenRouter names (or 00:00 UTC) and falls back at once; paid models on the same key keep working. The per-minute free limit is unchanged. - **OpenCode Go:** Qwen models now go to OpenCode's Messages endpoint, where OpenCode serves them; on Chat Completions they fail with the same `ModelProtocolUnsupported` error as #23 (the Protocol control can still send them back). Space Bunny uses its current id (`space-bunny`), and the price note says $10 a month, or $40 for Go Plus. - **Cline:** - Apply keeps `/v1` on the base URL. - Apply also writes the providers file the Cline CLI reads, keeping your other Cline providers. - The VS Code extension gets a model for Act mode too; it only had one for Plan mode. - Reset removes only the entry kRouter wrote. It no longer deletes a `cline auth` entry for your own local endpoint, such as Ollama. - Apply refuses a base URL that is not `http(s)`, and the stored API key is readable only by you. - The card says which Cline it configures and that VS Code must be closed while you click Apply. - **Continue's setup guide uses `config.yaml`.** It showed the deprecated `config.json` block, and says how to add the model to the `models: []` file Continue creates on first run. - **Claude Code's Reset removes the Max context override.** Reset left `CLAUDE_CODE_MAX_CONTEXT_TOKENS` in `~/.claude/settings.json`, so Claude Code kept the old context window. A value you set yourself is kept. - **OpenAI text-to-speech with a model id.** `openai/tts-1` was read as a voice named "tts-1" and failed; so did every OpenAI speech model in the catalog. A bare model id is now the model with the default voice, and the request's `voice` field reaches the provider. A base URL stored on an OpenAI connection (set through the API) is now used for speech too; it was ignored. - **Claude models.** The `anthropic` provider listed only three models Anthropic has retired. Both Claude providers now list Opus 5.5, Sonnet 5.5, Fable 5.1 and Haiku 4.5 first, followed by the older models Anthropic still serves. Their context window (1M), output limit (128K) and prices are correct, and Opus 5.5, Sonnet 5.5 and Fable are marked as unable to turn thinking off, which they reject with a 400. - **DeepSeek.** V4 Pro is no longer marked as able to read images; DeepSeek says it cannot. Prices follow DeepSeek's August 2026 repricing, and V4 Pro Max and V4 Pro No Thinking, which had no price of their own and were costed at an older, much lower DeepSeek rate, now cost the same as V4 Pro. - **Settings describes routing as it works.** The default strategy is labelled Zenith, not Fill First; it has run Zenith since 0.5.70. The help text says round robin does not keep a conversation on one account, and that Antigravity uses round robin by default. - **Skills:** the docs point to Endpoint → API Keys (there is no Keys page), say a key is needed under Docker too, and the "View on GitHub" link uses the right branch.OpenCode Go's Grok, GPT Luna and Muse Spark models work, and you can pick each model's protocol
**OpenCode Go users: seven models stopped failing.** grok-4.5, grok-4.6, grok-4.7, gpt-5.6-luna, gpt-6-luna and Muse Spark 1.2/1.3 are served by OpenCode only on its Responses API. kRouter sent them to `/chat/completions`, so every request answered `400 ModelProtocolUnsupported` (#23). Worse, OpenCode reports that error as HTTP 401, which kRouter treated as a bad account — so one request to one of these models cooled your whole OpenCode Go account down for about a minute, breaking every other model on it too. Both are fixed. Thanks @AincradBot for the precise report. ## Fixed - **The seven Responses-only models** now go to OpenCode's `/responses` endpoint. kRouter converts to and from the Responses API itself, so every client works: OpenAI, Claude Code and Responses clients (Codex), streaming or not, with tools, reasoning and JSON mode. - **OpenCode's "model not supported" error is reported as 400**, not 401, so it no longer cools down the account. - **OpenCode Go's model list is complete**: all 43 models OpenCode offers today (it listed 11), including Kimi K3, GLM 5.2/5.3, DeepSeek V4, Qwen 3.7/3.8, MiMo V2.5/V2.6 and Hy3/Hy4. **Also fixed, for every provider reached through a Responses API** (GitHub Copilot, Codex, grok-cli): - **A reply cut off by the token limit** ended with no finish reason and no usage, and Claude Code's stream never properly closed. It now ends with `max_tokens` / `length` and its usage. - **A failed reply came back as a successful one** whose text was the error. Non-streaming clients now get a real error (429 for rate limits). - **Only the first system message reached the model.** Later system messages and developer messages (Codex sends these) were dropped. All of them now arrive, in order. - **Non-streaming `/v1/responses` requests got a chat reply** they could not read, from every OpenAI-format provider. They now get a proper Responses object. Always-streamed providers returned an empty reply there, and Claude clients got a chat reply too; both are fixed. ## New - **Pick the protocol for each OpenCode Go model.** Every model on the OpenCode Go provider page has a Protocol control: Auto (kRouter's choice, shown), Chat Completions, Messages or Responses. If OpenCode moves a model, you can follow it the same day instead of waiting for a release. The choice applies to all your OpenCode Go connections, including ones added later.Docker users can log in, and example secrets from the docs no longer open the dashboard
**Upgrade if you run kRouter where anyone else can reach it.** kRouter's docs — and upstream 9router's, which many users migrated from — showed example values for `JWT_SECRET` and `INITIAL_PASSWORD`. kRouter used them verbatim. Anyone who pasted the website's `-e JWT_SECRET="..."` had a signing key the internet knows, so a stranger could forge a dashboard session; anyone who kept `INITIAL_PASSWORD=change-me` or `your-password` had a password the internet knows. Both were reproduced against the 0.5.161 image (HTTP 200) and are refused now. **Docker users: the README's `docker run` could not log you in.** With no stored password and no `INITIAL_PASSWORD`, kRouter refuses the default `123456` for anything but the machine it runs on — and under Docker your browser is never that machine. The documented command now generates a password, prints it, and passes it in. This dates back to 0.5.136. **npm users:** 0.5.161 never reached npm, so this is also your first release with everything in 0.5.161 below. ## Security - **Example `JWT_SECRET` values are ignored.** A `JWT_SECRET` under 32 characters, or one of the placeholders the docs showed (`...`, `generate-a-long-random-string`, `9router-default-secret-change-me` and others, matched without regard to case or spacing), is ignored in favour of the random secret kRouter generates in its data directory, with one warning in the log. Session forged with `...`: 200 on 0.5.161, 401 now. - **Example `INITIAL_PASSWORD` values get the default's rule.** `your-password`, `change-me`, `your-first-login-password` and others now allow logins only from the machine kRouter runs on, like `123456`. Remote login with `change-me`: 200 on 0.5.161, 403 now. - **The database export needs your password again.** Exporting or importing the database (which holds your provider credentials) asks for the password even inside a session; only the kRouter CLI may skip that. Any value in the CLI-token header used to count as the CLI: 200 on 0.5.161, 401 now. It still needed a logged-in session. - **The settings API no longer accepts a stored password from the client.** The password changes only through the change-password flow, which checks the current one. ## Fixed - **Docker login**, as above. README, `DOCKER.md` and the website now generate and print the password; pasting only the `docker run` line fails with a clear message instead of starting a container you cannot log in to. - **Forgot your password in Docker?** The image now includes a reset: `docker exec krouter node scripts/reset-password.js`. The kRouter CLI is not in the image, so there was no way back in before. - **The first password change rejected `INITIAL_PASSWORD`** as the current password, while still accepting the public `123456`. It now follows the same rule as login. - **Every command in `DOCKER.md` failed** with "invalid reference format": 22 invisible characters inside "krouter" since 0.5.61. - **Compose instructions** were missing the clone they need and set a random password nobody ever saw; the compose file's own comment put a command in `.env`, where it is never run. - **The login page and docs said "Profile"**; the dashboard calls it **Settings → Security**. The login page shows the `123456` hint only when no password is set, and mentions `INITIAL_PASSWORD`. - `start-redaction.sh` health-checked kRouter at an address that does not exist, so it always reported failure. ## Upgrade notes - If your `JWT_SECRET` was short or an example, you log in once more. Several instances sharing sessions need the same long random value on each: `openssl rand -hex 32`. - If `INITIAL_PASSWORD` is an example value and you never set a password, remote logins now get a 403. Log in on the machine itself, or restart kRouter with a real `INITIAL_PASSWORD`. ## Releases - npm can publish with an `NPM_TOKEN` secret when trusted publishing is refused, and the workflow now prints npm's actual reason for a refusal instead of a bare `E404`. - GitHub Release notes are no longer broken into ragged half-width lines. - The GHCR package page no longer suggests pulling a build-cache tag that is not an image.tray mode can recover from a crash again, and Docker images are back
**Upgrade if you run `krouter -t`.** Tray mode — the way the README tells you to run kRouter — never attached crash handling. A server crash went unnoticed, nothing restarted it, a port conflict never recovered, and the tray icon kept saying the router was up. It also meant the MITM crash-loop recovery fixed across 0.5.158–0.5.160 could not run in tray mode at all. **Docker users: this is the first working image since 0.5.157.** The image builds for 0.5.158, 0.5.159 and 0.5.160 all failed on a lockfile that had drifted out of sync, so Docker Hub and GHCR have been serving 0.5.157. That image does include the remote-code-execution fixes, which landed in 0.5.157 itself — but it misses everything since, including the `DATA_DIR` fix and the MITM crash-loop recovery. ## Fixed - **Tray mode had no crash recovery.** Crash handling was attached at the very end of start-up, and tray mode returns before reaching it. It is now attached as soon as the server spawns. - **The SQLite engine was being uninstalled on every launch.** `better-sqlite3` and the tray library share one runtime directory, and both were installed with `--no-save`, so each install pruned the other. Every start reinstalled `better-sqlite3` and then deleted it again, silently, leaving kRouter on its slower SQLite fallback. Both now persist. The first launch after upgrading tidies up once and then settles. - **Node 18 installs succeeded and then failed to start.** The bundled Next.js 16 needs Node **20.9**, but the package said 18. It now says 20.9, and refuses to start below it with a message naming both versions. CI proves the 20.9 floor on a clean machine: install, start, build. - **`krouter --help` printed `Usage: @sifxprime/krouter`**, which is not a command you can run. It now says `krouter`. - **"Hide to Tray" quietly added a login item.** The menu said nothing about starting on login and printed nothing. It is now labelled *Hide to Tray + Start on Login*, confirms what it did, and says how to undo it — including the exact file on Windows, which has no tray toggle for it. - **`--no-browser` was listed in `--help` but has never done anything** — nothing opens a browser automatically. It is no longer listed, and still accepted so existing scripts keep working. - **OpenCode Go model list** came from the general Zen catalogue (86 models, including free-tier variants) instead of the Go roster (43). Every other OpenCode Go path already used the Go endpoint. — thanks @AincradBot (#22) - **Gemini with an API key** now sends the key in the `x-goog-api-key` header instead of a `?key=` URL parameter, where it ended up in logs, and stops sending fields Gemini rejects. Streaming is unaffected. — thanks @manindersarao (#4) ## Security - **Next.js 16.3.8** closes `GHSA-vcvr-r3jv-pc5j`, a critical RCE in `next/og`'s `ImageResponse`. kRouter does not use `next/og`, so it was not reachable — closed anyway. - `undici` (DoS, and a TLS certificate check bypassed in `BalancedPool`) and `ip-address` (an SSRF classifier gap) are updated. - **Left open on purpose:** `node-forge` `GHSA-86w9-cpqp-85rv` has no fixed version published. It is about signature *verification*; kRouter only uses node-forge to *generate* its local MITM certificate and never verifies a signature with it. - Removed `http-proxy-middleware`, an unused dependency carried over from upstream. It was the only thing putting `braces` (`GHSA-vfj7-8cjw-p6xm`, a stack-exhaustion DoS with no fixed version yet) among production dependencies. `braces` remains only in lint tooling, which never sees untrusted input, and was never in the npm package's bundled runtime or the Docker image. - Private vulnerability reporting is enabled, with a [SECURITY.md](SECURITY.md). ## Docs and project - The README is 619 → 428 lines. A 200-line section on optional PII redaction sat above the features and providers lists; it is now a short summary, with the detail moved — not deleted — into [docs/REDACTION_SETUP.md](docs/REDACTION_SETUP.md). - Removed five translated READMEs (6,446 lines) that described the upstream project and never mentioned kRouter. - The default dashboard password (`123456`) is now documented. It only works from the machine kRouter runs on; change it under **Profile**. - GitHub now detects the licence as **MIT**. Attribution moved from `LICENSE` to `NOTICE`; extra prose in `LICENSE` was stopping GitHub from recognising it. - Issues are open, with bug and feature templates, and there is a [CONTRIBUTING.md](CONTRIBUTING.md). ## Releases A version tag now publishes everything from CI: npm (with provenance), Docker Hub and GHCR, and a GitHub Release built from this changelog. A failed image build opens an issue instead of failing silently. CI runs the test suite on every push and pull request. Versions published by hand carried the maintainer's local build path (`/Users/<name>/...`) in about 380 compiled files — not a secret, but not something a package should ship. CI builds don't, and the publish job now refuses a tarball that does.
September 2026
the crash-loop recovery is now proven, not just argued
No behaviour change. v0.5.159 fixed the MITM crash-loop recovery but shipped verified only by contract tests and by reading the write path, and said so. This closes that gap. That distinction mattered more here than it usually would. The two previous versions of this path both passed every check that looked at *shape* and still did nothing at runtime — one wrote to the wrong directory, the next wrote to `db.json`, a file the app had migrated into SQLite and no longer reads. Shape was never what was broken, so a test that only inspects shape could never have caught either one. `consumeMitmRecoveryMarker` moved into `src/shared/services/mitmRecovery.js` so it can be driven directly. Reaching it through `initializeApp` would have pulled in the tunnel stack, cloudflared and the MITM bootstrap side effects — none of which it needs. It now returns `"no-marker" | "disabled" | "write-failed"` so callers and tests can tell those apart instead of inferring from side effects. Eight tests run it against a real temporary `DATA_DIR` and the real SQLite layer: - no marker is a no-op, and a second run after a successful one is too - a marker disables MITM and is removed - it works when **no settings row exists at all**, which is the fresh-install case — a plain `UPDATE` would match zero rows here and silently do nothing - the value is read back through a **second SQLite connection**, so it cannot pass on a cached in-memory object. This is the assertion neither broken version could have survived - unrelated settings survive the merge - a truncated marker still disables MITM - a failing write keeps the marker so the next boot retries, and logs rather than swallowing Suite: 1999 passing, 0 failing.the crash-loop safety valve now actually closes
v0.5.158 fixed the directory the MITM crash-loop recovery looked in. It did not fix the larger problem, which only surfaced when the router was finally run end to end against a real, isolated data directory: **the recovery was writing to a file the app abandoned.** When the server crashes `MAX_RESTARTS` times in a row, the CLI supervisor assumes MITM is the cause, disables it, and restarts. It did that by patching `settings.mitmEnabled` in `DATA_DIR/db.json`. But `src/lib/db/migrate.js` imports that JSON into SQLite once, writes a `.migrated-from-json` marker, and keeps the JSON only as a rollback artifact: - on a **fresh install**, `db.json` never exists — `existsSync` is false and the write is skipped - on a **migrated install**, it exists but nothing reads it — the write lands in a dead file Either way the safety valve did nothing and the server kept crash-looping. The `catch { /* best effort */ }` wrapped around it is why this stayed invisible through many releases. ## The supervisor no longer knows how settings are stored That assumption is what rotted here, twice — first the directory, then the file itself. So it is gone. The supervisor writes a marker, `DATA_DIR/.mitm-recovery`, and the app disables MITM itself through `updateSettings()` — the same path every other caller uses. Storage can change again; this cannot silently rot with it. Three details matter, and each is pinned by a test: - The marker is consumed at the top of `autoStartMitm()`, **before** settings are read, so the change takes effect on the boot that handles it rather than one boot later. - The marker is removed **only after** the write succeeds. A failed write leaves it in place so the next start retries, instead of dropping the request silently. - A failed write is **logged, not swallowed**. `updateSettings()` was verified rather than assumed: it is an upsert (`ON CONFLICT(id) DO UPDATE`) performing an atomic `{...current, ...updates}` merge inside a transaction. Both properties were load-bearing — a fresh install has no settings row at all, so a plain `UPDATE` would have matched zero rows and reproduced the same class of bug a third time. ## Honest limit This is verified by contract tests and by reading the write path, **not** by observing a live recovery. `initializeApp()` only runs when a page renders through the root layout, and in a fresh unauthenticated install every route either redirects or is prerendered, so the boot path could not be driven in a sandbox. The corollary is worth knowing: this recovery — in its old design and its new one — only fires once a page has rendered. An API-only deployment would never reach it.DATA_DIR is honoured everywhere, including where it was silently failing
**Upgrade if you set `DATA_DIR`.** `cli.js` ignored it and resolved `~/.krouter` unconditionally, so a single process disagreed with itself: auth and machine-id came from `DATA_DIR`, while `db.json`, the tunnel directory and the mitm pidfile came from the home directory. `DATA_DIR` is documented — the README lists it as *"Data directory (SQLite, certs, cache)"* and `docs/ARCHITECTURE.md` states `db.json` lives at `${DATA_DIR}/db.json` — so this was a broken promise, not an undocumented edge. The consequence worth the release was silent. When the server crash-loops past `MAX_RESTARTS`, the CLI clears `settings.mitmEnabled` in `db.json` to break the loop. Looking in the wrong directory made `existsSync` return false, the surrounding `catch { /* best effort */ }` swallowed it, and the server kept crash-looping with the safety valve doing nothing at all. Worse, a stale `~/.krouter/db.json` left from before `DATA_DIR` was set would be written to instead of the real one. Mostly this hit Docker and multi-instance users — the people most likely to set `DATA_DIR`, and the least likely to be watching a terminal when the loop began. ## The cause was duplication Three hand-synced copies of the same resolver, each carrying a *"kept in sync with…"* comment, and one had drifted. They are now one module, `cli/src/lib/dataDir.js`, shipped inside the package. It cannot import the app's own `src/lib/dataDir.js` — that is ESM and lives outside the CLI package — so it mirrors it, which is the relationship the old comments described, minus the drift. Two smaller bugs came out with it: - `cli.js` fell back to `process.env.APPDATA || ""` on Windows, so an unset `APPDATA` produced a **relative** path and wrote into the current working directory. It now falls back to the home directory, as the app already did. - `api/client.js` honoured `DATA_DIR` but without the Windows-path and writability guards, so one setting could resolve three different ways inside one process. The `mkdir` fires only for an explicitly configured `DATA_DIR`; the default path stays a pure lookup, with a test pinning that so it cannot become a surprise side effect of importing a module. Seven new tests cover both behaviour and shape — two of them assert the deleted copies stay deleted, since a drifted duplicate is what caused this in the first place. ## Also - Removed two dead `gitbook` rules from `.gitignore`, finishing the removal started in v0.5.157.an unauthenticated RCE on Windows hosts, and a docs app that never deployed
**Upgrade if you run kRouter on Windows.** Next.js published two critical advisories against the 16.3.1 this release was built on, and one of them reaches the dashboard: - **[GHSA-p293-qw3h-jr36](https://github.com/advisories/GHSA-p293-qw3h-jr36)** — unauthenticated remote code execution on Windows-hosted servers, fixed in 16.3.3. `server.js` binds `0.0.0.0` by default, so on Windows the dashboard is reachable from the local network by anyone who can route to the machine. This one is real and it is why the release exists. - **[GHSA-2xp9-vwfh-vxw4](https://github.com/advisories/GHSA-2xp9-vwfh-vxw4)** — unauthenticated RCE in the Image Optimization API when AVIF files are used. **Not reachable here.** `next.config.mjs` has set `images.unoptimized` since long before the advisory, so there is no `/_next/image` endpoint to attack. Recorded rather than omitted, because "we ship a vulnerable version" deserves a reason and not silence. Both are now closed by Next.js 16.3.4, which also brings `sharp` to 0.35.4 and clears [GHSA-rgj7-g3m4-5g8c](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c). `npm audit --omit=dev` goes from 2 findings, one critical, to zero. Neither advisory was reported by Dependabot. It reads manifests, and the vulnerable version was a lockfile resolution of a `^16.2.9` range — so nine alerts sat open against a different directory while the two that mattered went unmentioned. ## The nine alerts were noise, and the directory is gone Every one of those nine was the same package, `next`, pinned at 16.1.1 in `gitbook/` — a second Next.js app inherited from the fork. All nine require a running Next server. `gitbook` had none: it was `output: "export"` with no middleware, no route handlers and no server actions. It was also dead. Its workflow ran exactly once, on 2026-06-14, and failed. It deployed to `9router.github.io`, a domain this project does not own. Nothing had touched it since the initial fork commit. It has been removed along with `gitbook-pages.yml` and the two `next.config.mjs` entries that existed only to keep it out of the bundle — 121 files, and the entire source of the alert noise that was hiding the real problem. ## The support cards line up now The two contact cards sized themselves to their own text, so `krouter@kodelyth.com` made the email card 28.79px wider than the WhatsApp one, with left edges 28.79px apart. Only their right edges agreed, because `align-items: flex-end` was the only thing aligning them. The container now carries one definite width and both cards fill it, so neither can size to its label: measured live, width, height, left and right all agree to 0.0000px. The marks inside them are matched too. The official WhatsApp path is drawn edge-to-edge in its 24-unit box while the envelope stops at 94%, so at an identical box size WhatsApp rendered 0.86px wider and 4px taller and read as the heavier of the pair. Its viewBox is padded to inset it to the envelope's exact fraction; the two ink widths now land 0.009px apart, with the official path data untouched. ## Also - A rejected `krouter-web` dispatch fails the job again. The website's own cron is declared every 10 minutes but measures at a 212-minute median across 85 runs, with a 751-minute worst case, so a silent dispatch failure means the site lags a release by hours rather than minutes.the two support marks now read at equal weight
The envelope in the support widget looked bigger than the WhatsApp mark. Measured on the live widget, it is not: the envelope carries 177px² of ink against WhatsApp's 255px², and the two boxes are within a pixel of each other in width. What differs is contrast. The envelope rendered in near-black `rgb(11, 13, 18)` on a pale badge while WhatsApp sat at mid-green `rgb(37, 211, 102)`, and the darker mark dominates regardless of size — which is why the earlier attempt at this, resizing the glyph, did not fix it. Each channel now carries its own colour with a 10% wash of it behind the glyph, so the badge takes some of the weight and neither mark has to carry it alone. WhatsApp keeps its brand green; email takes a blue. They read as a pair rather than one loud and one quiet.the last of the upstream backlog, and 24KB of dead code that was shipping
The end of the upstream triage. Of the 19 commits still marked worth porting, **11 are in**; the other 8 depend on subsystems this fork does not have and are listed below with the reason rather than left unexplained. **A stale backup file was shipping inside every install.** `chatCore.js.bak`, a copy of the chat handler taken at 0.5.40, had been tracked ever since — 24KB of dead code inside the npm package, 243 diff-lines behind the live file. Anyone who opened it looking for the real handler would have been reading code with no SSRF guards, no usage accounting, and no clientTool wiring. Deleted, with `*.bak` added to `.gitignore` so the next one cannot be committed by accident. **Claude requests poisoned by a foreign tool id.** Anthropic validates `server_tool_use` ids against `^srvtoolu_` and rejects the whole request with a 400 when one does not match. A combo that falls back to a provider with its own built-in tools — z.ai/glm emits OpenAI-style `call_` ids for its `analyze_image` tool — leaves those blocks in the history, so every later Claude turn carried a poisoned id and kept failing. Those blocks are dropped now, along with the result blocks that referenced them (an orphaned `tool_result` is rejected just as firmly) and any message left with no content at all, since an empty text block is its own 400. (upstream `ed1bd0c5`) **GLM credit plans showed no quota at all.** Quota parsing accepted only `TOKENS_LIMIT` and wrote every limit to the same `session` key. An account on a `CREDIT_LIMIT` plan therefore saw nothing, and an account with more than one window kept only whichever arrived last — a weekly figure displayed as the session one. Both types are accepted now, each window keyed by its own type and period. (upstream `fcfcced4`) **OpenCode Go quota was not tracked at all.** A subscriber saw nothing on the usage dashboard for that provider, including when a window was exhausted and their requests had already started failing. Rolling, weekly and monthly windows are read now, and a missing subscription is reported distinctly from plain forbidden — those fail identically at the transport level, and only the error type separates them. (upstream `0da803ee`) **Antigravity onboarding was tripping Google's anti-abuse limiter.** It retried five times in rapid succession; with several accounts refreshing at once those calls arrived as a burst from one IP and the whole set got rate-limited. Two attempts now, a twelve-second base delay, and jitter so concurrent refreshes do not stay in lockstep. Both knobs are env-overridable for a slow network. (upstream `1442cc73`, the `projectId` half) **Also in:** `GET /v1/models/{provider}/{model}` for single-model lookup, OpenAI-compatible, with a proper `model_not_found` 404 — this replaces the `[kind]` route rather than sitting beside it, since a provider-prefixed id contains a slash and only a catch-all can capture it (`5caa72f5`). The GPT-5.3-Codex-Spark quota window, which is metered separately and previously left users staring at unexplained 429s with no row to explain them (`40eed186`). Antigravity image sizes now resolve to the aspect-ratio model suffix that provider actually expects (`2a9213c5`). A completed Responses call no longer logs a disconnect — codex and droid close the socket on every one, which made a normal finish look like a client hanging up and buried the disconnects that are real (`c4af43fa`). And system-prompt injection is exact-idempotent, closing a latent way to double a persona and bill for it inside a feature meant to save tokens (`cadef6c4`, dedup only — ours has been format-aware across six formats all along). **Not ported, and why.** `7e5f5a88` re-anchors Claude cache breakpoints through a subsystem this fork does not have, and our passthrough deliberately preserves the client's body byte-for-byte so Anthropic's prompt cache is not busted — the opposite strategy. `ac98dd9d` and `1a3db1ef` need an `antigravityQuota` service we do not carry. `ab044e6d` and `acb5c34c` route Muse Spark through a `transports[]` mechanism we do not have. `2ab6a4c9`, `cec672d9`, `e014cb53` and `ed963931` are catalog refreshes that land in a provider registry we do not have. `c24a8542` extends an endpoint- presets module we never had. 1957 tests pass, and the real end-to-end suite passes against live provider credentials.