Write the standalone app bundle and MITM bundle to NINEROUTER_CLI_APP_DIR
when set, so staged deploys can build to a separate destination before
swapping. Default output stays at cli/app when the env var is unset.
Set process DPI awareness (Per-Monitor V2 with fallbacks) and enable
WinForms visual styles before creating the tray NotifyIcon, so the
Windows context menu renders sharply on DPI-scaled displays.
Closes#2161
Co-authored-by: Cursor <cursoragent@cursor.com>
Next.js 16 nests standalone output under the project directory name
when NEXT_TRACING_ROOT_MODE=workspace (e.g. standalone/9router/server.js
instead of standalone/server.js), causing build-cli.js to fail with
"Next.js standalone build not found".
Detect nested layout with backward-compatible fallback to existing
standalone/server.js and standalone/app/ paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Build on the optional Headroom Token Saver from Carmelo Campos
(PR: feat: add optional Headroom token saver). Add managed start/stop
of the local headroom proxy from the dashboard, install detection,
status probing, and a simplified Token Saver UI.
- detect headroom CLI + python>=3.10, probe proxy /health
- spawn/stop proxy as a detached, pid-tracked process
- /api/headroom/{status,start,stop} routes, gated local-only in dashboardGuard
- one-click Start/Stop Headroom modal, no manual config needed
- claude<->openai shape conversion for /v1/compress via 9router translators
Thanks to Carmelo Campos (@carmelogunsroses) for the original Headroom integration.
Co-authored-by: Cursor <cursoragent@cursor.com>
## Fixes
- **Kiro**: honor thinking effort budgets
- **AG/Kiro/Xiaomi**: provider fixes
- **Combo/Fusion**: flatten tool history in panel calls to prevent 503
- **LLM selector**: show custom vision models in selector and model list
- **Image**: prevent compatible nodes from shadowing provider aliases
## Fixes
- **Kiro**: honor thinking effort budgets
- **AG/Kiro/Xiaomi**: provider fixes
- **Combo/Fusion**: flatten tool history in panel calls to prevent 503
- **LLM selector**: show custom vision models in selector and model list
- **Image**: prevent compatible nodes from shadowing provider aliases
## Features
- **Combo Fusion strategy** — fans the prompt out to all member models in parallel, then a configurable judge model synthesizes one final answer (quorum-grace, anonymized sources, graceful degradation)
- **Per-combo strategy selector** — pick `fallback` / `round-robin` / `fusion` / `capacity` per combo (replaces the old round-robin toggle), with a judge picker for fusion
- **Capacity auto-switch** — reorders models per request so images/PDFs route to capable models first
- **Kiro headless API-key auth** (`ksk_`) + direct `claude↔kiro` route that avoids the lossy OpenAI two-hop pivot
- **Claude auto-ping** — warms the 5h quota window right after reset so a fresh window starts immediately (per-connection toggle)
## Fixes
- **Claude 429**: stop hammering the OAuth usage endpoint — cache resetAt, throttle quota refresh to 3 min, cool down after a 429 (chat unaffected)
- **Usage logs always empty**: missing `await` on `getAdapter()` in `getRecentLogs` made `/api/usage/logs` & `/api/usage/request-logs` return nothing
- **Executors**: strip params unsupported by the provider/model (drops deprecated `temperature` for claude-opus-4 → Anthropic 400)
- **Translator**: derive deterministic tool_call ids for gemini/antigravity → OpenAI so function call/response pair correctly (fixes tool-pairing 400s)
- **Antigravity**: strip `optional` from tool schemas before sending to Gemini
- **Claude-to-OpenAI**: handle OpenAI-format responses in the non-streaming path (e.g. xiaomi-tokenplan)
- **Usage views**: show edited connection names consistently across Providers & Quota Tracker
- **Security**: hardened reverse-proxy local-access trust
- **Security**: SSRF hardening on web fetch
## Internal
- Large **open-sse / translator refactor** (~40 commits): unified provider/model registry (LiteLLM-style `models[]` + `kind` field, 100 co-located registry files), single-sourced media/OAuth/refresh/token URLs, registry-based dispatch for usage & token-refresh, DRY translator concerns (buildUsage, encodeDataUri, finishReasonMap, chunkBuilder, reasoningDelta…), ESM-safe registry init, large-file splits, dead-code removal, and golden/no-regression test gates
Features
Vercel AI Gateway: support embeddings, images and credit usage (#1183)
Add MiMo Free no-auth provider (#1789)
Vertex: support ADC authorized_user credential
Cowork: re-enable Claude Cowork with preset-only stdio MCP
Codex: bulk add accounts via JSON (#1719)
Kiro: enable multi-endpoint failover for GenerateAssistantResponse (#1722)
Fixes
Security: re-auth on DB export/import + SSRF guard on web fetch
Auth: real client IP rate-limiting + remote default-password guard
Cerebras/Mistral: strip unsupported client_metadata from downstream requests (#1742)
SiliconFlow: update baseUrl .cn → .com + curate verified model list (#1760)
Gemini-to-OpenAI: route unsigned thought parts to reasoning_content (#1752)
Claude-to-OpenAI: strip Anthropic billing header from system prompt (#1765)
Anthropic-compatible: send Bearer auth for third-party gateways (#1795)
Usage-stats: avoid partial stats on initial SSE race (#1767)
Proxy: use export default in proxy.js for Next.js 16 middleware detection
Claude passthrough: add body normalization
GitHub Copilot: refresh missing/expired token on models discovery (#1727) + add mappable gpt-5-mini/gpt-5.4-nano slots for Copilot MITM (#1653)
Kiro: auto-resolve profileArn to prevent 403 on IDC login, enhance profile ARN resolution, update endpoint to runtime.us-east-1.kiro.dev (#1713)
Tunnel: detect system-installed Tailscale via dual-socket probe (#1723) + non-blocking probes to prevent UI freeze
CommandCode: force stream=true in transformRequest (#1706)
Qoder: increase timeouts for reasoning models and improve stream handling
Dashboard: show provider node name instead of connection name in topology (#1770) + show explicit kind="llm" combos on combos page (#1684)
Docs
README: add Indonesian 9Router tutorial video (#1709)
- Add custom-server.js: inject unspoofable socket IP, strip client XFF
(wired into Docker CMD + CLI spawn + build-cli copy)
- loginLimiter: key on trusted x-9r-real-ip, TRUST_PROXY opt-in, global fallback
- Force password change on first remote login while default is in use
- Add /api/auth/reset-password (local-only) so CLI reset writes live SQLite
- CLI settings: reset via API instead of stale db.json
- Fix OAuth modals opening duplicate browser tabs on add-connection
- Add cli:pack / cli:publish scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add gemini-3.5-flash-extra-low across CLI menu, provider models, usage, pricing
- Add MITM synonyms (high/medium/extra-low) and split pattern so Low no longer falls through to Medium
- Strip models/ prefix in getMappedModel for AG public name normalization
Co-authored-by: Cursor <cursoragent@cursor.com>
## Features
- Add Qoder provider: device-flow OAuth, COSY signing, WAF-bypass body encoding, live model catalog, dashboard quota tracker, 11 models (#1372)
- Add new models: Claude Opus 4.8 (Claude Code), GPT 5.4 Mini (Codex)
## Fixes
- DeepSeek thinking mode: echo `reasoning_content` back on follow-up/tool-call turns so OpenCode-free and custom providers no longer 400 with "reasoning_content must be passed back" (#1543)
- Reasoning injector: match deepseek/kimi model ids case-insensitively (covers custom providers using capitalized model names)
- OpenCode suggested-models: include free models without the `-free` suffix, e.g. `big-pickle` (#1535)
## Improvements
- Codex: trim sunset models, keep gpt-5.5 / gpt-5.4 / gpt-5.3-codex family, add gpt-5.4-mini
- volcengine-ark: refresh model list (add DeepSeek-V4-Flash/Pro, drop EOL entries)
- Lower stream stall timeout 35s → 30s for faster hang detection
## Fixes
- Codex: auto-retry when upstream drops mid-stream (no more hangs)
- Codex: fix random 400/404 errors, tool-calling failures, and unstable prompt cache
- MITM: support Antigravity 2.x (updated IDE version detection and DNS/cert flow)
- Sanitize Read tool args to prevent retry loops from non-Anthropic models (#1144)
- Implement json_schema fallback for OpenAI-compatible providers without native Structured Output (#1343)
- Strip empty Read pages argument in OpenAI-to-Claude translator (#1354)
- Forward Gemini output dimensions for embeddings (#1366)
- Resolve setState-in-effect errors in dashboard components (#1362)
- Gemini CLI: reuse stored OAuth project IDs for quota checks and show clearer setup guidance when the project is missing (#1271, #1428)
## Features
- Xiaomi MiMo Token Plan: region selector (Singapore / China / Europe) — keys are cluster-specific
- Antigravity: risk confirmation dialog before first connection
- Gemini CLI: surface upstream retry delay on 429 errors
## Fixes
- MITM: cannot kill process on macOS under sudo (lsof not found in PATH)
- Stream: false-positive stall timeout on Claude reasoning / Kiro responses
- Tunnel: cannot re-enable after disable (stuck state)
- Tunnel: cloudflared error messages now include log tail for easier debugging
- Language switcher: applies selected locale immediately on close (#1234)
- Antigravity OAuth: metadata now matches the official client
## Improvements
- Gemini CLI: bump engine to 0.34.0
- Re-hide `qwen` (OAuth EOL) and `iflow` (not ready) providers
## Features
- Add Vercel AI Gateway provider support (#1183)
- rtk: Kiro format tool result compression — handle conversationState.history & currentMessage, preserve error results, ~13.6% savings (#1194)
## Fixes
- openclaw: normalize agent.model object form `{primary, fallbacks}` before .startsWith → fix TypeError & 'not configured' status (#1216)
- Usage Details pagination: stay inside mobile viewport <640px (#1218)
- Fix test model error
- Fix MIMO provider in Codex
- Disable log file creation when using MITM AG
- Fix duplicate tray icon on macOS when hiding to tray
- Fix tray not showing in background mode on macOS
- Fix hide to tray broken on Windows/Linux
- Fix Shutdown button in web UI not working
## Features
- Add Blackbox provider with `bb` alias (#1143)
- Add Xiaomi token plan provider
- Enhance model select modal UX + modal traffic lights (#1111)
- Default Usage dashboard period to Today (#1141)
## Fixes
- Fix Cowork model selection and Windows CLI packaging (#1129)
- Update provider name retrieval for compatibility provider (#1135)
- Update JWT_SECRET handling
Cherry-picked from upstream PR #1129 + local improvements:
- dedupe inline remove-model handler -> use handleRemoveModel
- add .next-cli-build/ and cli/.build-home/ to .gitignore
- Persistent raw mode across menus avoids per-prompt latency
- Suspend raw temporarily for line-buffered text input
- Update CHANGELOG v0.4.41
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(autostart): resolve cli.js path locally, register with launchctl, verify state
The current `cli/src/cli/tray/autostart.js` is silently broken on every nvm
install (and likely Volta / asdf / Homebrew / user-prefix npm) and on every
npm version >= 9. Enabling auto-start from the tray menu writes a launchd
plist that references a non-existent script; the menu then reports
"✓ Auto-start Enabled" because the existence check only verifies the file is
on disk. On the next OS boot launchd fails with MODULE_NOT_FOUND. None of
this surfaces to the user.
This rewrite addresses the four problems reported in #1082:
1. `npm bin -g` was removed in npm 9. autostart.js called it as a primary
resolution mechanism and silently fell back to a hardcoded
`/usr/local/lib/node_modules/9router/cli.js` path on failure. Replaced
with a `getCliJsPath()` helper that tries (in order): the explicit
`cliPath` argument, `process.argv[1]` when it's our own cli.js, and a
path computed relative to autostart.js's own location (since this file
always lives at `<pkg>/src/cli/tray/autostart.js`, cli.js is three
levels up regardless of install layout). Returns null on no match.
2. The `/usr/local/...` fallback was outright wrong for nvm/Volta/asdf
installs. Dropped entirely. If no candidate resolves, return false
instead of writing a plist pointing at a missing script.
3. `enableMacOS()` never called `launchctl load -w`, only `unload`. The
plist was therefore inert until the next user login, with no signal to
the user that anything was wrong. Now it unloads (defensive, in case of
re-enable) and then loads, so the agent is active in the current session.
4. `isAutoStartEnabled()` on macOS only checked file existence — so the
tray menu reported "✓ Enabled" even when launchd had the agent in a
failed state or hadn't loaded it. Now also runs `launchctl list
${APP_LABEL}` and only returns true if launchd recognizes the label.
Additional changes for robustness:
- The macOS plist now invokes node + cli.js directly with absolute paths
instead of wrapping in `zsh -l -c "..."`. The shell-wrapper approach
depended on the user's login shell sourcing nvm/PATH correctly, which is
fragile (nvm.sh sourcing varies between users; some setups don't add
node to PATH from a non-interactive login shell).
- `EnvironmentVariables.PATH` in the plist now explicitly includes node's
bin directory plus the standard system paths, so child processes spawned
by cli.js (e.g. the runtime `npm install` calls) can still resolve `npm`
even under launchd's minimal default env.
- Windows and Linux paths were calling `npm bin -g` with the same fallback
problem; both now use `getCliJsPath()` consistently. The Windows VBS
branch is simplified (always run node+cli.js, drop the `9router.cmd`
lookup that depended on the npm prefix path).
- Removed the unused `getStartCommand()` helper that was never imported.
Fixes#1082
* fix(autostart): skip launchctl unload/load when current process is the agent
When the running 9router cli.js was itself spawned by the autostart launchd
agent (after a reboot when autostart was previously enabled), clicking the
tray menu's "Disable Auto-start" item would unload the agent — and that
unload sends SIGTERM to the running process, killing the click handler and
the tray icon before the menu could flip its label.
Add a small `isAgentSelfMacOS()` probe (parses `launchctl list ${label}` and
compares the PID against `process.pid`). When we're the agent itself,
disable skips the unload (the plist file removal is enough to prevent the
agent from starting on the next login) and enable skips the load (the plist
is already loaded under our own PID, the on-disk update is what matters for
next boot). External callers — e.g. enable from a manually-launched 9router
or from a script — still get the full unload/load behavior they need.
Without this, the tray UX after a reboot was: click Disable -> tray icon
silently disappears, no menu label change. Now: click Disable -> label
flips to "Enable Auto-start", tray stays, plist removed; click Enable again
-> label flips back, plist re-created.