# AI Router profiles — design ## Goal Configure named AI Router endpoints (9Router, OpenRouter, …) once in Settings, then pick one when creating a lane so that lane's Claude Code or Codex session talks to the router instead of the vendor's own API — without hand-editing a settings file per project, and without a second copy of the whole settings file drifting out of sync with the first. ## What the reference setup showed The user today keeps two whole files, `~/.claude/settings.json` (native) and `~/.claude/settings2.json` (9Router), and swaps them. Diffing the two: - They differ in exactly one place that matters — the `env` block, 8 keys: `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL`, `CLAUDE_CODE_SUBAGENT_MODEL`. - Every other difference (`hooks`, `enabledPlugins`, `statusLine`, `effortLevel`, …) is drift between two hand-maintained copies. Removing that drift is half the point of this feature. - "Native" is **not** the absence of the connection variables: the native file sets `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` to empty strings and carries real `claude-*` model names. - The 9Router file sets all six model slots to one symbolic value (`Highest`), which the router resolves. Model values are therefore free text, and "one value for every tier" is the common case, not the exception. ## Decisions (from brainstorming) 1. **The Settings tab only stores profiles.** It does not attach anything. Attaching happens when a lane is created. 2. **Native is a built-in profile, not a code branch.** Its `baseUrl`/`apiKey` are empty and its six model slots hold the native `claude-*` names. It cannot be edited or deleted. Switching native ↔ router is therefore one write path with no special case. 3. **A profile carries six model slots plus an "apply to all" convenience field**, mirroring exactly the env keys Claude Code reads. 4. **The API key never leaves the server.** Profiles are stored one JSON file each under `/agenthub-routers/` (dir `0700`, file `0600`), following `cc-profiles.js`. `GET` returns metadata plus `hasKey: true`, never the key. 5. **A lane's router is fixed at creation**, like `lanes.tool`: a new nullable `lanes.router_profile` column, deliberately absent from `PATCHABLE`. 6. **AgentHub never reads `~/.9router/db.json`.** That is another application's private store; parsing it would couple this dashboard to a format its owner is free to change. The user pastes a base URL and a key, as they do today. ## Attaching, per tool The two tools differ because the tools themselves differ, not by choice. **Claude lane** — write the profile's eight env keys into `/.claude/settings.local.json`, merging into any `env` already there and leaving every other key in the file untouched. `settings.local.json` rather than `settings.json` because the value written includes an API key and that file is the one Claude Code's own convention keeps out of git. **Codex lane** — Codex has no project-level config: `codex doctor` reports one loaded file, `~/.codex/config.toml`. It does have `-p, --profile `, documented as *"Layer `$CODEX_HOME/.config.toml` on top of the base user config"*. So each router profile is written once to `~/.codex/.config.toml` (`model_provider` plus a `[model_providers.]` table with `base_url`, `env_key`, `wire_api`), and the lane's spawn adds `-p `. The base `~/.codex/config.toml` is never modified. Codex reads the key from the environment variable named by `env_key`, so the spawn also injects that variable into the tmux pane — `pty-run.js` already passes an `env` map to `tmux new-session -e`. ## Surface - `server/lib/ai-routers.js` — store: list/get/create/update/remove, name validation, `0700`/`0600`, key never returned by the list shape. - `server/lib/router-apply.js` — the two writers above, taking a lane row and a profile. Target directories come from the lane row in the DB, never from a request body — the same narrowing every other lane write path uses. - `server/routes/ai-routers.js` → `/api/ai-routers`: `GET /`, `POST /`, `PATCH /:name`, `DELETE /:name`, reusing `cc-profiles.js`'s error-code→status table. - `POST /api/lanes/:id/router/apply` — a fourth member of the auto-setup set the Add-lane modal already fires (`profileInit`, `agentsInstall`, `mcpSync`). - `client/src/pages/AiRouterConfig.tsx` + one line in `SettingsHub.tsx`'s `TOOL_TABS`, placed after Codex. - `AddLaneModal`: a Native / AI Router radio, and a profile select shown only when AI Router is chosen. - DB: `ALTER TABLE lanes ADD COLUMN router_profile TEXT`, nullable. ## Out of scope Changing an existing lane's router (create-time only, exactly like `tool`), attaching at user/global scope, editing Codex's base `config.toml`, and reading any router vendor's own configuration store. ## Verification Server tests against a temporary `CLAUDE_HOME`/`CODEX_HOME`: store CRUD and its permission bits, the key never appearing in a list response, the `settings.local.json` merge preserving unrelated keys, and the generated `.config.toml`. Client tests: the tab renders and creates a profile, and the Add-lane radio sends the chosen profile only when AI Router is selected. The load-bearing assumption — a project-level `settings.local.json` `env` block overrides the user-level one per key — is confirmed by the user from their own earlier testing.