Conversation
`connect` wrote hardcoded defaults (cc/claude-opus-5, …) and ignored the model mapping the operator had already set on the server, so a client ended up on models the server may not even route. Read the server's own cli-tools config and reuse its model ids: - GET /api/cli-tools/claude-settings → per-tier Claude models - GET /api/cli-tools/opencode-settings → active model for non-Claude tools Only model ids are taken. The server's baseUrl and apiKey in that response belong to that host and are never copied — this machine keeps its own key and the remote base URL. Best-effort: an unreadable/500 response falls back to the built-in defaults. Precedence: explicit flag > server value > built-in default, with `--no-inherit` to skip inheriting. Also strips Claude Code's "[1m]" context marker before the /v1/models availability check, which was warning falsely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Non-Claude tools all took the server's OpenCode model, ignoring what the operator configured for that specific tool on the dashboard. Each tool now declares the dashboard route it maps to plus a `serverModel(data)` extractor that reads only its 9router-owned model id: - codex: `model` from the TOML, only when `model_provider = "9router"` - opencode: `model` with the `9router/` prefix - droid: first `custom:9Router*` entry - crush: `providers.9router.models[0].id` - cline: `openAiModelId`, only when the provider is `openai` - kilo: no route — its GET exposes only auth key names, not the model Precedence per tool: --model > that tool's own server model > the server's OpenCode model > built-in default. Routes are fetched in parallel and only for the selected tools (plus OpenCode as the shared fallback). A tool whose server config is missing, unreadable or malformed just falls back. As before, only model ids are read; the server's own apiKey / Authorization header / base URL in those responses are never copied. Tests assert the server key never appears in any written file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fallback tiers still pointed at the previous generation (cc/claude-fable-5, cc/claude-opus-5, cc/claude-sonnet-5) even though the claude registry has shipped 5.1/5.5 ids for a while, so a server with no cli-tools config of its own handed clients older models. Bump the Claude fallbacks and the shared non-Claude DEFAULT_MODEL to cc/claude-sonnet-5-5. Haiku is unchanged — 4-5-20251001 is still the newest haiku in the registry. Tests now read the expected value from CLAUDE_MODELS instead of hardcoding an id, so the next bump cannot leave them stale. Note: src/shared/constants/cliTools.js carries the same defaults for the dashboard, but it also seeds the server-side Claude combo presets (src/lib/comboPresets.js), so bumping it changes the `opus`/`sonnet` alias targets. Left out of this CLI-only change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…defaults Models now always come from the server's own cli-tools config; flags override. There are no built-in model ids any more, so `--no-inherit` (which only existed to pick those defaults) is removed. When the server has nothing for a slot: - Claude tier: left unset, so Claude Code applies its own default. A tier written by an earlier run is deleted rather than kept stale. - Non-Claude tool: skipped with a message (pass --model to configure it), and the run exits 1 since it didn't do what was asked. With --api-key there is no session to read the server's models, so the command says so and expects model flags. Docs: cli/README.md gains a Models section (precedence table), override samples for single/all Claude tiers, all non-Claude tools, mixed runs and per-tool runs, plus an options table. `connect --help` shows the same rules with two examples. Tests (23): unset tiers stay unset, a stale tier is cleared, a tool with no model is skipped and exits 1 unless --model is given, and `--no-inherit` is rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connectconnect models from the server, not hardcoded defaults
…nfig `9router show [tool…]` reads each CLI tool's LOCAL config and prints the base URL, the API key (masked) and the models it is set to — e.g. `9router show claude` lists every Claude Code tier. `--json` gives the same data machine-readably; tools can be named positionally or with --tools, aliases included. Each tool gains a `show()` next to its apply/reset writers in connectTools.js, so the reader and the writer of a format stay together. Tools not pointed at 9router are listed as "not configured", and Codex, OpenCode and Cline warn when a 9router entry exists but another provider is active. Read-only and offline: it never contacts a server, and never prints a full key — the masked form is applied before output, including in --json. A config that fails to parse is reported per tool and the command exits 1 while still showing the others. Docs: README "Checking the current config" section with samples; the launcher's --help lists the command. Tests (7): empty home, apply→show round-trip for every tool, key never in output, unset Claude tier not invented, codex inactive warning, --json, arg parsing, broken config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running `node cli/cli.js connect` from a checkout resolves modules from the root node_modules (cli/ has none). Root package.json mirrored the CLI's other runtime deps but not enquirer, so `connect` threw "Cannot find module 'enquirer'". Published package unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a successful login, --save writes the password to ~/.9router/connect.env (or $DATA_DIR/connect.env), mode 600, bound to the server URL. Later runs for the same server reuse it instead of prompting; it is never sent to a different host. Precedence: --password / NINE_ROUTER_PASSWORD > saved > prompt. A failed login with a saved password points at the file to delete. Stored in plain text — opt-in only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te bare values The --save tests stubbed only DATA_DIR and homedir(), but on Windows savedEnvPath() resolves %APPDATA%\9router, so the suite wrote a test password into the developer's real profile and its path assertion failed. Stub APPDATA to the temp home too, assert the path stays under it, and make the expected path platform-aware. loadSavedPassword() JSON-parsed every value, so a hand-edited connect.env with an unquoted value crashed the whole command with a raw "is not valid JSON" error. Fall back to the bare string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connect models from the server, not hardcoded defaultsconnect models from the server; add show and --save
- pi: writes providers.9router to ~/.pi/agent/models.json (or an existing legacy ~/.pi/models.json); model from --model, the server's Pi config, then the server's OpenCode model. - omp: writes providers.9router to ~/.omp/agent/models.yml with proxy discovery, so it needs no model id and lists every server model. The dashboard's best-effort agent.db mirror is skipped (needs a native sqlite module; models.yml is primary). - Aliases: pi-coding-agent, oh-my-pi. Reset removes only the 9router entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connect models from the server; add show and --saveconnect models from the server; add show, --save, Pi and Oh My Pi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #4497.
connectwrote hardcoded model ids and ignored the models the operator had already configured on the server, so a client could end up pointed at models that server doesn't even route.Now models always come from the server's own CLI-tools config, and flags override. Nothing is hardcoded.
This PR also adds conveniences around
connect:9router show [tool…]prints each tool's current 9router config (base URL, masked key, models).connect --saveremembers the dashboard password for that server, so later runs skip the prompt.pi(Pi coding agent) andomp(Oh My Pi).Observed against a real server whose dashboard had
opus=claude-opus-5-5,sonnet=gcli/grok-4.7,haiku=glm-5.3-flash[1m]:Where each model comes from (first match wins)
claude--opus…) → server's Claude tier → left unset--model→ that tool's own model on the server → server's OpenCode model → tool skippedEach tool declares its dashboard route and a
serverModel(data)extractor next to its writer inconnectTools.js, reading only a 9router-owned model id:/api/cli-tools/claude-settingsmodelin the TOMLmodel_provider = "9router"model9router/custom:9Router*entryproviders.9router.models[0].idopenAiModelIdopenaiproviders.9router.models[0]The "only trusted when" checks stop a client from copying a model that belongs to another provider (e.g. a Codex set to OpenAI's own
gpt-6).When the server has nothing for a slot
1. Pass--modelto configure it.--api-key(no login): there's no session to read the server's models, so the command says so and expects model flags.Overriding the server's models
New:
9router showPrints what each CLI tool on this machine is currently set to — base URL, masked API key and models — so you can check the result of
connect(or of a manual edit) without opening config files:Each tool gains a
show()beside itsapply/resetinconnectTools.js, so the reader and writer of each format live together. Tools not pointed at 9router show asnot configured; Codex, OpenCode and Cline warn when a 9router entry exists but another provider is active. A config that fails to parse is reported per tool (exit1) while the others are still shown.New:
connect --saveAfter a successful login,
--savewrites the password toconnect.envin the CLI data dir (~/.9router/on macOS/Linux,%APPDATA%\9router\on Windows, or$DATA_DIR), bound to that server's URL. Later runs for the same server reuse it instead of prompting.--password/NINE_ROUTER_PASSWORD> saved > prompt.--savenothing is written. Delete the file to forget the password.New: Pi and Oh My Pi
Both are set up the same way as the server's own Pi and Oh My Pi setup routes (
pi-settings/omp-settings):pi(aliaspi-coding-agent)~/.pi/agent/models.json(or an existing legacy~/.pi/models.json)--model→ server's Pi model → server's OpenCode modelomp(aliasoh-my-pi)~/.omp/agent/models.ymldiscovery: { type: proxy }makes every server model appear under9routerin/modelOther providers and settings in those files are kept;
--resetremoves only the 9router entry.ompis never skipped for lack of a model. The dashboard also saves the key into omp'sagent.dbwhen it can;connectskips that, since it needs a native sqlite module the npx command doesn't ship, andmodels.ymlis what Oh My Pi reads first. YAML is read and written withconfbox, already a dependency here.Other changes
[1m]context marker before the/v1/modelsavailability check, which was reporting a valid model as "not listed".cli/README.mdgains a Models section (precedence table), the override samples above, and an options table.connect --helpshows the same rules with two examples.package.json: addsenquirer, so runningnode cli/cli.js connectfrom a checkout resolves the prompt library (the published package already listed it).--no-inherit(it only existed to choose those defaults). An intermediate commit in this PR bumped the defaults to Fable 5.1 / Opus 5.5 / Sonnet 5.5; the final commit removes them entirely.Security notes
ANTHROPIC_AUTH_TOKEN, OpenCode/DroidapiKey, Codex'sAuthorizationheader — and its loopbackbaseUrl(http://127.0.0.1:20128/v1). Copying either would hand this machine a credential that isn't its own and point it at the wrong host, so both are ignored: the client keeps its per-machine key from/api/keysand the remote base URL. Tests assert the server key never appears in any written file.--savestores the dashboard password in plain text. It is opt-in, bound to one server URL (a test asserts it is never offered to a different host), and written mode600— owner-only on macOS/Linux. On Windows the mode is a no-op: the file is protected only by the user profile's folder permissions, so anything running as that user can read it. Users who can't accept that should keep using the prompt orNINE_ROUTER_PASSWORD.600(a no-op on Windows) with a one-time*.bak-9routerbackup.showis read-only and offline — it contacts no server — and never prints a full key: the masked form is applied before output, including in--json. A test asserts the full key never appears in its output.Test plan
npx vitest run unit/cli-connect.test.js unit/cli-show.test.js— 37/37 pass.--save: password stored mode 600 and bound to its server, reused on the next run, never offered to another host; nothing written without--save; a hand-edited unquoted value is read instead of crashing. The tests redirect bothhomedir()and%APPDATA%to a temp folder — an earlier version wrote into the developer's real Windows profile, fixed ineb580b2e.cli-connect: Claude tiers from the server; unset tiers staying unset; a stale tier being cleared; each tool using its own server model and falling back to OpenCode's;--modeloverriding every tool; a tier flag beating the server; a tool with no model being skipped and exiting 1 unless--modelis given; extractors ignoring other providers' models;--no-inheritrejected. Run-level cases assert the server's key never lands in a written file.--tools pi,ompinto a temp home: Pi got the server's OpenCode model, omp got proxy discovery,show pi ompmatched; neither file contains the server's key or a loopback URL.cli-show(7): empty home, apply→show round-trip for every tool, full key never in output, unset Claude tier not invented, Codex inactive-provider warning,--json, broken config.show claudeagainst the real~/.claude/settings.jsonwritten byconnect: base URL, masked key and all tiers shown.unit/combo-presets.test.jsstill passes — the dashboard'ssrc/shared/constants/cliTools.jsis untouched.--tools claude,codex,kilo: all 4 Claude tiers taken from the server; codex and kilo (no own model there) got the server's OpenCode model.--tools all: scanned all 8 written files — this machine's key in each one that holds a key, the server's key in none, no loopback URL.🤖 Generated with Claude Code