## 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
opencode-go hard-coded targetFormat: claude per model, so every client
format was force-routed to /messages (Codex/OpenAI clients paid a lossy
Responses->OpenAI->Claude double translation). Declare the existing
upstream multi-endpoint transports [openai, claude, openai-responses]
and guard per model via registry supportedFormats: kimi/glm/mimo only
support /chat/completions, minimax/qwen add /messages, deepseek adds
/responses. Undeclared models keep the upstream default.
Drop the bespoke OpenCodeGoExecutor (its shared _lastModel cache could
cross auth headers between concurrent requests); DefaultExecutor already
consumes runtimeTransport and injects reasoning content.
Next file tracing follows JS imports, and sql.js loads dist/sql-wasm.wasm by
path at runtime, so the standalone output carries the package's JS without its
wasm binary. When both native drivers fail the last-resort adapter then aborts
with ENOENT on the missing binary and the container never gets a database.
The CLI bundle already guards this explicitly (build-cli.js step 3b,
ensureModuleInBundle("sql.js")); the image just never got the same treatment.
Copy the package the same way node-forge and next already are.
Fixes#3248
Antigravity and gemini-cli wrap their payload in { response: {...} }.
extractUsageFromResponse only tested top-level usageMetadata, so every
non-streaming antigravity request logged zero usage (IN 0 | OUT 0) and
zeroed rows in the usage dashboard. Read the envelope the same way
usageTracking.js and nonStreamingHandler.js already do; top-level
metadata keeps priority and the OpenAI/Claude branches are untouched.
Fixes#3260
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).
custom-server.js now generates a per-process secret at boot and stamps it
as x-9r-peer-token on every request it sanitizes. hasTrustedPeerHeaders()
(src/lib/auth/trustedPeer.js) gates trust in x-9r-real-ip on that secret;
otherwise the guard falls back to Host only in development, and fails
closed in production. Same gate on loginLimiter.getClientIp() so a spoofed
header cannot rotate the login lockout bucket.
Also: fix isLoopbackHostname for IPv6 (::1, ::ffff:127.0.0.1) which the
old split(":")[0] reduced to empty string; route npm run start /
start:bun through custom-server.js (postbuild copies it into
.next/standalone, build-cli.js fails without it) so documented deployments
keep passwordless local access.
Fourth Alibaba key type — Coding Plan (alicode/alicode-intl) and Model Studio
(alims-intl) both reject Token Plan keys. Registry entry only; PROVIDER_MODELS
builds from providers/registry so no executor or translator work is needed.
Singapore-only (eu-central-1 answers IllegalEndpoint) and OpenAI-compatible
transport only (the Anthropic surface is not authorized for this plan).
Closes#2754Closes#2806
Add gemini-3.7-flash and its tiered high/medium/low variants to the
Antigravity and Gemini registries, with matching capabilities, pricing
and Antigravity quota tracking.
extractModel now recognises gemini-3.7-flash-tiered alongside 3.6 and
derives the version from the request, so thinkingLevel still maps to the
right tiered alias.
Closes#3286Closes#3281
Registry entry plus one config-driven FORMAT_HANDLERS handler. The model id
travels in an HTTP `model` header rather than the JSON body, and the voice is
a reference_id (preset or cloned voice model).
Closes#2411
Zhipu released GLM-5.3 on both api.z.ai and open.bigmodel.cn coding
endpoints. Verified live against both, returning model:"glm-5.3" with
native reasoning_content.
No other changes needed: the '*glm-5*' family pattern in capabilities.js
and 'glm-5*' in pricing.js already cover it.
Passthrough kept the client's own cache_control markers, which point at
pre-normalization offsets. Once normalize/dedupe reshaped system and tools,
the breakpoints landed mid-array and the tail was re-cached every request.
- Pin the last system block and last tool at ttl 1h (was the client's 5m)
- Anchor the last assistant turn at 5m, falling back to the final message
so a first turn still gets a breakpoint
- Fold mid-conversation system messages into the neighbouring user turn
instead of hoisting them into body.system, where the volatile token
counters invalidated the prefix on every request
- Run the anchoring after every token saver, at the final body
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inspect images[], experimental_attachments/attachments, message-level
image/image_url/audio_url, and inline data:image|audio|pdf URIs on trailing
user turns so Vision Adapter auto-switch fires for Hermes/Ollama/Vercel AI
SDK shapes. stripOpenAI now also drops msg.images and image attachments when
the active model lacks vision support.
Kimchi's transport is OpenAI-compatible (Authorization: Bearer) but the
registry declared it OAuth-only, so the dashboard, /api/providers, and
the connection test all rejected API keys. Enable dual auth
(authModes: ["oauth", "apikey"]) and add a kimchi case to
testApiKeyConnection so the Test Connection button works for both modes.
Regenerate the golden snapshot with the Kimchi entries (+ other
previously-missing providers).
Mirror the official opencode CLI fingerprint (User-Agent, x-opencode-session, x-opencode-request, x-opencode-project) on free-tier requests so the Console no longer classifies traffic as an unidentified client and rate-limits it with FreeUsageLimitError / HTTP 429.
Session id resolves conversation-stable via resolveSessionId (client session to assistant-text hash to connection) to preserve prompt caching, normalized into opencode ses_ format with a generated fallback. When the downstream client is already opencode, its headers are forwarded as-is.
Zed IDE injects a Claude-agent system prompt that Antigravity flags as
competitive, blocking the request with a 429 Quota Exhausted response.
Scan systemInstruction.parts and remove the prompt before dispatch.
Kiro IDE 1.0.228+ moved GenerateAssistantResponse from path
/generateAssistantResponse to POST / + x-amz-target header, so chat turns
bypassed MITM. The SmithyMessageDecoderStream also now requires an
initial-response frame at stream start, and agent/vibe mode sends
modelId "auto" which had no mappable slot.
- Add isChatRequest() header-based match for kiro in mitm/config.js
- Add buildInitialResponseFrame/withInitialFrame to emit the mandatory
initial-response once per stream (kiro.js)
- Add "auto" model slot and update mitmDomain to runtime.us-east-1.kiro.dev
Peek the first SSE frame in wrapQoderSSE; if statusCodeValue != 200 and the
body carries a billing signature (code 112/10605 or pricingUrl), return a
synthetic 403 so chatCore marks the connection unavailable and triggers
combo/account fallback instead of leaking the error text into chat.
wrapQoderSSE becomes async; consumed peek bytes are re-processed in the
stream start() seed loop so nothing is dropped.
Some providers (e.g. codebuddy/cbcn) attach an empty tool_calls array to every streaming chunk. An empty array is truthy in JS, so the guard 'if (delta.tool_calls)' closed the message on the first content token and emitted response.output_text.done early, dropping the remaining deltas. Guard on a non-empty array; finish_reason still closes the message and real tool calls still close it before emitting function_call items.
fixes#3234
Manual refresh (↻) sends ?force=1 so it bypasses the Claude quota cache (dedup + TTL) added in cd4003bc. Auto-refresh and multi-tab stays cached, so Anthropic's usage endpoint is no longer hammered.
Co-Authored-By: Claude <noreply@anthropic.com>
Multiple tabs/accounts/auto-refresh funneled straight to Anthropic and tripped 429. Add a 120s TTL cache keyed by access token with in-flight promise dedup, serve the last good read on soft failure, and thread a force flag through getUsageForProvider for manual refresh. Also lower the dashboard poll cadence (180s to 600s) and stable group-by-provider so connection order stops jumping.
Co-Authored-By: Claude <noreply@anthropic.com>
Toggle was checked={headroomEnabled && headroomRunning} and disabled when the proxy was down, so a downed proxy showed OFF while headroomEnabled stayed true in the DB. The engine only checks headroomEnabled, so it kept calling /v1/compress. Toggle now reflects the user setting; proxy up/down stays visible via the status chip.
Co-Authored-By: Claude <noreply@anthropic.com>
gitbook-pages.yml deployed to upstream org's 9router.github.io which a fork
cannot (no GH_PAGES_DEPLOY_KEY) — renamed to .yml.disabled. Dockerfile gains
a /api/health HEALTHCHECK. .env.example documents the new pool geostrobes
env knobs (POOL_GEO_PROBE_DISABLED, GEO_PROBE_URL).
New /api/system/memory reports db + data-dir sizes and in-memory state
(fitness/geo registries, freebuff session/cooldown counts); profile page
'Local Mode' card gets a Check Size Memory button rendering the summary.
Geo-probe toggle hint localized to English.
Header toggle in Proxy Fitness persists settings.poolGeoProbeEnabled
(default on); the probe scheduler reads it each pass and skips when off.
The env POOL_GEO_PROBE_DISABLED remains a hard override. Smart rotation is
unaffected — it runs off the fitness registry from real request failures.
Try ipwho.is -> ip-api.com -> ipapi.co -> ipinfo.io in order so a
rate-limited/broken provider falls through to the next; ipinfo (most
quota-bound) is last. Normalize each payload to {ip,country,region,city,org},
keep 15s timeout per endpoint, and support GEO_PROBE_URL to replace the
chain with a single custom endpoint.
Refuse re-probing failures too fast: 500/429 failures get a 2h backoff,
other errors 30m (flapping relays/quota stops hammering ipinfo every pass,
passes every 30 min instead of 15). One-line summary per pass
(geo N/M · fail: rate×a server×b) replaces the per-pool error wall.
New env POOL_GEO_PROBE_DISABLED=1 turns the feature off.
Remove [DBG:STREAM] chunk/pipe/EOF instrumentation and [DBG:SSE] flush +
fetch success lines that spam the dev console every request. Keep the stream
stall watchdog and the fetch-error (✖) diagnostics.