Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
71dcdc1
fix(headroom): toggle reflects enabled setting even when proxy is down
decolua Aug 13, 2026
cd4003b
feat(usage): dedup + cache Claude quota calls to avoid 429
decolua Aug 13, 2026
456f2a2
feat(usage): wire force flag through client + usage route
decolua Aug 13, 2026
b44bb09
fix(kiro): report real output tokens and stop discarding usable turns
nguyenha935 Aug 13, 2026
e2a4fe0
fix(hermes): add api_key parameter to model block in YAML configuration
ILoveBacteria Aug 13, 2026
01858fe
docs(i18n): add Brazilian Portuguese documentation
yusei21 Aug 13, 2026
10a923d
fix(responses): don't close message on empty tool_calls array
chisewaguri Aug 13, 2026
80afb59
fix(qoder): detect billing blocks at stream start, return 403 for fai…
brimob-sowax Aug 13, 2026
70ba002
fix(translator): preserve prompt_cache_key when converting chat to re…
ntdatt812 Aug 13, 2026
8a527fe
fix(security): SSRF guard on search baseUrl, default-password remote …
Aug 13, 2026
b57c041
fix(providers): add llm7 to provider test support
cokkyturnip Aug 13, 2026
5b417f9
fix(kiro): intercept chat via x-amz-target and prepend initial-respon…
rm1dev Aug 13, 2026
6d30ce6
fix: Fusion strip stream_options + reasoning model test probe
ctdan Aug 13, 2026
b566b20
fix(antigravity): strip competitive system prompts to prevent 429 quo…
stoXmod Aug 13, 2026
67271d8
fix(opencode): send official client headers on free-tier requests
CyrixJD115 Aug 13, 2026
30fec43
fix(models): expose snake_case token limits on /v1/models
Aug 13, 2026
e02bde4
feat(providers): add Kimchi API key support (dual OAuth + API key)
fdjrr Aug 13, 2026
65197ad
feat(auth): add native SAML 2.0 SSO integration
nnmduc Aug 13, 2026
345cdcf
fix(combo): detect images from Hermes and attachment payloads for Vis…
berthojoris Aug 13, 2026
7e5f5a8
fix(claude): re-anchor passthrough cache breakpoints with 1h TTL
decolua Aug 14, 2026
8ed9da7
feat(providers): add glm-5.3 to GLM Coding and GLM (China) registries
Aug 14, 2026
8af5e75
feat(tts): add Fish Audio as a text-to-speech provider
ntdatt812 Aug 14, 2026
86694ed
feat(antigravity): add Gemini 3.7 Flash models (#3286, #3281)
FerryAr Aug 14, 2026
8b2b2fe
docs(i18n): add Spanish and French README translations
AlexNoVibe Aug 14, 2026
b04c03c
feat(providers): add Alibaba Token Plan (token-plan.ap-southeast-1)
ntdatt812 Aug 14, 2026
9225921
fix(security): require proof that x-9r-real-ip came from the socket (…
ntdatt812 Aug 14, 2026
59d858b
fix(usage): read Gemini usageMetadata out of the antigravity response…
ntdatt812 Aug 14, 2026
27f3710
fix(docker): ship sql.js so the pure-JS DB fallback can start
ntdatt812 Aug 14, 2026
e1115e2
feat(opencode-go): route by request format via transports + per-model…
KiMelody Aug 14, 2026
540ebbe
test(baseline): regenerate provider snapshot for opencode-go transports
decolua Aug 14, 2026
699edac
# v0.5.55 (2026-08-14)
decolua Aug 14, 2026
e179178
Merge upstream 9router v0.5.55 into 10router
some-du6e Aug 19, 2026
98eef22
fix(saml): fail closed on missing saml_state to block assertion replay
some-du6e Aug 20, 2026
78364da
fix: address CodeRabbit review findings on upstream v0.5.55 merge
some-du6e Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,92 @@
> Entries below this point are historical and refer to the project under its
> former name, 9Router. They are left as-published.

# v0.5.55 (2026-08-14)

## Features
- **Auth**: native SAML 2.0 SSO alongside OIDC — AuthnRequest generation, ACS
assertion handling, SP metadata export, admin config test, replay-protected
via a `saml_state` cookie matched against `InResponseTo`
- **Providers**: add Alibaba Token Plan (`token-plan.ap-southeast-1`) — the
fourth Alibaba key type, Singapore-only and OpenAI-compatible transport only
- **Providers**: add `glm-5.3` to GLM Coding and GLM (China)
- **Providers**: Kimchi accepts API keys as well as OAuth (dual auth), with a
working Test Connection for both modes
- **Antigravity**: add Gemini 3.7 Flash and its tiered high/medium/low variants
(also in the Gemini registry) with pricing and quota tracking
- **TTS**: add Fish Audio — model id travels in an HTTP `model` header, voice
is a `reference_id` (preset or cloned voice model)
- **OpenCode-Go**: route by request format via declared transports instead of
forcing every client into `/messages` — Codex/OpenAI clients no longer pay a
lossy Responses→OpenAI→Claude double translation. Per-model `supportedFormats`
guard; the bespoke executor is gone (its shared `_lastModel` cache could cross
auth headers between concurrent requests)
- **Usage**: dedup + cache Claude quota calls (120s TTL keyed by access token,
in-flight promise dedup, last-good read on soft failure) to stop multiple
tabs tripping 429; manual refresh (↻) sends `force=1` to bypass the cache

## Fixes
- **Docker**: ship `sql.js` in the image so the pure-JS DB fallback can start —
file tracing carried the package's JS without `dist/sql-wasm.wasm`, so a
container with no native driver aborted with ENOENT and never got a database
(#3248)
- **Usage**: read Gemini `usageMetadata` out of the antigravity `{ response }`
envelope — every non-streaming antigravity request logged `IN 0 | OUT 0`
(#3260)
- **Claude**: re-anchor passthrough cache breakpoints — the client's own
`cache_control` markers point at pre-normalization offsets, so the tail was
re-cached every request. Last system block and last tool pinned at 1h TTL,
last assistant turn at 5m, mid-conversation system messages folded into the
neighbouring user turn instead of hoisted into `body.system`
- **Combos**: detect images from Hermes and attachment payloads (`images[]`,
`experimental_attachments`, message-level `image_url`/`audio_url`, inline
`data:` URIs) so the Vision Adapter auto-switch fires for Hermes/Ollama/
Vercel AI SDK shapes
- **Kiro**: intercept chat via `x-amz-target` — Kiro IDE 1.0.228+ moved
`GenerateAssistantResponse` to `POST /` + header, bypassing MITM. Also emit
the now-mandatory initial-response frame and map the `auto` model slot
- **Kiro**: report real output tokens and stop discarding usable turns
- **Qoder**: detect billing blocks at stream start and return a synthetic 403
so combo/account fallback triggers instead of leaking the error into chat
- **Antigravity**: strip competitive system prompts (Zed IDE's Claude-agent
prompt) that Antigravity flags with a 429 Quota Exhausted
- **OpenCode**: send the official client fingerprint on free-tier requests so
the Console stops classifying traffic as unidentified and rate-limiting it;
session id resolves conversation-stable to preserve prompt caching
- **Responses**: don't close the message on an empty `tool_calls` array — some
providers attach one to every chunk, and the truthy check ended the message
on the first content token (#3234)
- **Translator**: preserve `prompt_cache_key` when converting chat to responses
- **Models**: expose snake_case token limits on `/v1/models`
- **Combos**: strip `stream_options` from the Fusion panel fan-out to avoid a
DeepSeek 400 (#3024); raise the dashboard model-test probe budget to 1024 and
soft-pass reasoning-only responses (#3010)
- **Headroom**: the toggle reflects the `headroomEnabled` setting even when the
proxy is down — it previously showed OFF while the engine kept calling
`/v1/compress`; proxy status stays visible via the status chip
- **Hermes**: add the `api_key` parameter to the model block in YAML config
- **Providers**: add llm7 to provider test support

## Docs
- **i18n**: add Spanish, French, and Brazilian Portuguese README translations

## Security
- **Real IP**: `x-9r-real-ip` and the Host fallback were trusted from
client-controlled headers whenever `custom-server.js` was not in the request
path (`npm run start`, `start:bun`), letting a remote caller pose as local to
skip API key auth and reach `LOCAL_ONLY_PATHS` (`/api/mcp/*`,
`/api/tunnel/enable`, `/api/auth/reset-password`). The server now stamps a
per-process `x-9r-peer-token` on every request it sanitizes and only trusts
`x-9r-real-ip` behind it — falling back to Host in development and failing
closed in production (GHSA-pjm4-8fpg-f9p6). Also fixes IPv6 loopback
detection (`::1`, `::ffff:127.0.0.1`) and routes `npm run start` /
`start:bun` through `custom-server.js`
- **Search**: `resolveBaseUrl()` rejects client-supplied non-public baseUrls
(SSRF guard on `/v1/search`)
- **Login**: fresh-install remote login with the default password returns 403
without issuing a JWT
- **Usage**: `/api/usage/request-details` redacts request/response payloads

# v0.5.50 (2026-08-05)

## Features
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ COPY --from=builder /app/src/mitm ./src/mitm
COPY --from=builder /app/node_modules/node-forge ./node_modules/node-forge
# Ensure `next` is available at runtime in case tracing did not include it.
COPY --from=builder /app/node_modules/next ./node_modules/next
# sql.js loads dist/sql-wasm.wasm by path at runtime; tracing only follows JS imports,
# so the last-resort DB driver would abort with ENOENT on the missing binary.
COPY --from=builder /app/node_modules/sql.js ./node_modules/sql.js

# The on-disk data dir stays ".9router" (not ".10router") for compatibility with existing installs.
RUN mkdir -p /app/data && chown -R node:node /app && \
Expand Down
4 changes: 3 additions & 1 deletion cli/scripts/build-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ function buildCliPackage() {
fs.copyFileSync(customServerSrc, path.join(cliAppDir, "custom-server.js"));
console.log("✅ Copied custom-server.js\n");
} else {
console.warn("⚠️ custom-server.js not found — server will run without real-IP injection\n");
console.error("❌ custom-server.js not found — without it no request can be proven local,");
console.error(" so the packaged CLI would demand an API key for its own dashboard and /v1.");
process.exit(1);
}

// Step 3b: Ensure sql.js (pure JS fallback) bundled in app/cli/app/node_modules.
Expand Down
3 changes: 3 additions & 0 deletions cli/src/cli/menus/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ const PROVIDER_MODELS = {
{ id: "glm-4.7" },
],
ag: [
{ id: "gemini-3.7-flash-high" },
{ id: "gemini-3.7-flash-medium" },
{ id: "gemini-3.7-flash-low" },
{ id: "gemini-3.6-flash-high" },
{ id: "gemini-3.6-flash-medium" },
{ id: "gemini-3.6-flash-low" },
Expand Down
22 changes: 21 additions & 1 deletion custom-server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const http = require("http");
const crypto = require("crypto");
const path = require("path");
const fs = require("fs");
const { pathToFileURL } = require("url");

const origCreate = http.createServer.bind(http);
Expand Down Expand Up @@ -323,6 +324,12 @@ function handleCodexUpgrade(req, socket, head, port) {
socket.on("error", () => { closed = true; });
socket.on("close", () => { closed = true; });
}
// Per-process secret proving x-9r-real-ip was stamped below rather than sent by the client.
// A bare `next start` / `next dev` never loads this file, so it cannot produce a matching
// header even though the env var is inherited by child processes. Named like x-9r-cli-token
// so the request-detail header sanitizer redacts it too.
const PEER_TOKEN = crypto.randomBytes(24).toString("hex");
process.env.NINEROUTER_PEER_TOKEN = PEER_TOKEN;

let backgroundRefreshStarted = false;

Expand Down Expand Up @@ -377,7 +384,9 @@ http.createServer = (...args) => {
delete req.headers["x-9r-real-ip"];
delete req.headers["x-forwarded-for"];
delete req.headers["x-9r-via-proxy"];
delete req.headers["x-9r-peer-token"];
req.headers["x-9r-real-ip"] = ip;
req.headers["x-9r-peer-token"] = PEER_TOKEN;
if (viaProxy) req.headers["x-9r-via-proxy"] = "1";
return handler(req, res);
};
Expand Down Expand Up @@ -446,4 +455,15 @@ http.createServer = (...args) => {
return server;
};

if (require.main === module) require("./server.js");
if (require.main === module) {
const standalone = path.join(__dirname, "server.js");
if (fs.existsSync(standalone)) {
require(standalone);
} else {
// Repo checkout has no standalone build next to us. `next start` builds its HTTP
// server in-process, so the wrapper above still sanitizes every request.
const nextBin = require.resolve("next/dist/bin/next");
process.argv = [process.argv[0], nextBin, "start", ...process.argv.slice(2)];
require(nextBin);
}
}
Binary file added docs/images/saml-admin-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/saml-login-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading