@alexeiled/claude-router 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +8 -8
- package/docs/configuration.md +28 -23
- package/docs/design.md +1 -1
- package/docs/user-guide.md +4 -4
- package/lib/config.mjs +16 -2
- package/lib/gateway.mjs +14 -0
- package/lib/rewrite.mjs +2 -1
- package/lib/router.mjs +2 -1
- package/lib/status.mjs +1 -1
- package/package.json +1 -1
- package/scripts/statusline.mjs +6 -2
- package/skills/setup/SKILL.md +9 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "router",
|
|
3
|
-
"displayName": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"displayName": "jev-router",
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Auto-picks the right Claude model for each turn — small models for quick edits, mid-tier for code, frontier for hard problems. Uses Jev to classify each request.",
|
|
6
6
|
"author": { "name": "Alexei Ledenev", "url": "https://github.com/alexei-led" },
|
|
7
7
|
"repository": "https://github.com/alexei-led/claude-router",
|
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@ runtime dependencies and needs Node 22 or later.
|
|
|
16
16
|
## How it works
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
Claude Code --model router ──▶ gateway 127.0.0.1:43170 ──▶ api.anthropic.com
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
Claude Code --model jev-router ──▶ gateway 127.0.0.1:43170 ──▶ api.anthropic.com
|
|
20
|
+
│
|
|
21
|
+
only requests for `jev-router`: ├─ facts.mjs prompt, continuation, failures
|
|
22
|
+
├─ jev.mjs one Choice (tier) + one Noul (continuation?)
|
|
23
|
+
├─ policy.mjs stickiness, escalation, cost-gated votes
|
|
24
|
+
├─ rewrite.mjs model, effort, thinking per model family
|
|
25
|
+
└─ store.mjs session memory, decisions.jsonl
|
|
26
26
|
responses go through unchanged; the gateway reads `usage` (context size, cache TTL)
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -95,7 +95,7 @@ git config --local core.hooksPath scripts/git-hooks # pre-commit: biome + gitl
|
|
|
95
95
|
npm test # node:test
|
|
96
96
|
npm run check # biome lint and format
|
|
97
97
|
npm run validate # claude plugin validate
|
|
98
|
-
claude --plugin-dir . --model router # with ANTHROPIC_BASE_URL and TYPESAFE_API_KEY set
|
|
98
|
+
claude --plugin-dir . --model jev-router # with ANTHROPIC_BASE_URL and TYPESAFE_API_KEY set
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
Releases: push a signed tag `v<version>` that matches `package.json`. The
|
package/docs/configuration.md
CHANGED
|
@@ -20,16 +20,15 @@ starts it again.
|
|
|
20
20
|
|
|
21
21
|
```json
|
|
22
22
|
{
|
|
23
|
-
"model": "router",
|
|
23
|
+
"model": "jev-router[1m]",
|
|
24
24
|
"env": {
|
|
25
|
-
"ANTHROPIC_BASE_URL": "http://127.0.0.1:43170"
|
|
26
|
-
"CLAUDE_CODE_MAX_CONTEXT_TOKENS": "1000000"
|
|
25
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:43170"
|
|
27
26
|
},
|
|
28
27
|
"modelPicker": {
|
|
29
28
|
"options": [
|
|
30
29
|
{
|
|
31
|
-
"model": "router",
|
|
32
|
-
"label": "Router (auto)",
|
|
30
|
+
"model": "jev-router[1m]",
|
|
31
|
+
"label": "Jev Router (auto)",
|
|
33
32
|
"description": "Auto-selects the model and effort for each turn",
|
|
34
33
|
"behavesAs": "claude-opus-5-5"
|
|
35
34
|
}
|
|
@@ -38,19 +37,23 @@ starts it again.
|
|
|
38
37
|
}
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
The `modelPicker` row adds `Router (auto)` to the `/model` picker, next to the
|
|
42
|
-
built-in rows. `behavesAs` maps `router` to a model that Claude Code knows.
|
|
43
|
-
Without it, Claude Code rejects `router` because the model is not in its
|
|
40
|
+
The `modelPicker` row adds `Jev Router (auto)` to the `/model` picker, next to the
|
|
41
|
+
built-in rows. `behavesAs` maps `jev-router` to a model that Claude Code knows.
|
|
42
|
+
Without it, Claude Code rejects `jev-router` because the model is not in its
|
|
44
43
|
catalog. Setup also removes the `ANTHROPIC_CUSTOM_MODEL_OPTION*` keys that
|
|
45
|
-
older versions wrote.
|
|
44
|
+
older versions wrote. Before 0.4.2 the alias was `router`; the gateway still
|
|
45
|
+
routes it, and setup replaces that row.
|
|
46
46
|
|
|
47
47
|
Setup writes the file as its last step. Restart Claude Code after it. Until
|
|
48
|
-
the restart, the session sends `router` to Anthropic and shows "There's an
|
|
49
|
-
issue with the selected model (router)".
|
|
50
|
-
|
|
51
|
-
Claude Code does not know the model `router`, so it assumes a 200K window.
|
|
52
|
-
`
|
|
53
|
-
|
|
48
|
+
the restart, the session sends `jev-router` to Anthropic and shows "There's an
|
|
49
|
+
issue with the selected model (jev-router[1m])".
|
|
50
|
+
|
|
51
|
+
Claude Code does not know the model `jev-router`, so it assumes a 200K window.
|
|
52
|
+
The `[1m]` suffix declares 1M, the largest `contextWindow` of the default
|
|
53
|
+
routes; Claude Code strips it before the request. `CLAUDE_CODE_MAX_CONTEXT_TOKENS`
|
|
54
|
+
does not work here: Claude Code ignores it for this model. With the suffix,
|
|
55
|
+
Claude Code sends the 1M context beta header on every request; the gateway
|
|
56
|
+
drops it for a model with a smaller window (Haiku answers it with 400). The gateway sends a turn
|
|
54
57
|
only to a model whose window holds the context with room to spare (80%). A
|
|
55
58
|
large session skips Haiku (200K) and goes to Sonnet or Opus. The
|
|
56
59
|
`/router:status` reason for such a turn is `context-fit`.
|
|
@@ -67,7 +70,7 @@ If you agree, it also wraps the status line command:
|
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
The wrapper runs the command after it, then adds one line for a routed
|
|
70
|
-
session, for example `router ▸ opus-5-5 · xhigh`. Without a command
|
|
73
|
+
session, for example `jev-router ▸ opus-5-5 · xhigh`. Without a command
|
|
71
74
|
after it, it prints only that line. The path contains the plugin version, so
|
|
72
75
|
run `/router:setup` again after a plugin update.
|
|
73
76
|
|
|
@@ -89,7 +92,7 @@ path. Nested objects merge.
|
|
|
89
92
|
{
|
|
90
93
|
"gateway": {
|
|
91
94
|
"port": 43170,
|
|
92
|
-
"alias": "router",
|
|
95
|
+
"alias": "jev-router",
|
|
93
96
|
"baselineTier": "low",
|
|
94
97
|
"auxiliaryTier": "low",
|
|
95
98
|
"idleShutdownMs": 7200000
|
|
@@ -137,15 +140,17 @@ frontmatter of `skills/<tier>/SKILL.md`. A test makes sure that they agree.
|
|
|
137
140
|
|
|
138
141
|
### models
|
|
139
142
|
|
|
140
|
-
| Alias | ID | Input | Cache Read | Window | Billing | Efforts |
|
|
141
|
-
| -------- | ------------------ | ----- | ---------- | ------ | ------- | ------------- |
|
|
142
|
-
| `opus` | `claude-opus-5-5` | $4 | $0.2 | 1M | plan | all |
|
|
143
|
-
| `sonnet` | `claude-sonnet-5` | $2 | $0.2 | 1M | plan | low–xhigh–max |
|
|
144
|
-
| `haiku` | `claude-haiku-4-5` | $1 | $0.1 | 200k | plan | none |
|
|
143
|
+
| Alias | ID | Input | Cache Read | Window | Max output | Billing | Efforts |
|
|
144
|
+
| -------- | ------------------ | ----- | ---------- | ------ | ---------- | ------- | ------------- |
|
|
145
|
+
| `opus` | `claude-opus-5-5` | $4 | $0.2 | 1M | as sent | plan | all |
|
|
146
|
+
| `sonnet` | `claude-sonnet-5` | $2 | $0.2 | 1M | as sent | plan | low–xhigh–max |
|
|
147
|
+
| `haiku` | `claude-haiku-4-5` | $1 | $0.1 | 200k | 64k | plan | none |
|
|
145
148
|
|
|
146
149
|
`id` is the model id that the gateway sends to Anthropic. `input` and
|
|
147
150
|
`cacheRead` are list prices in USD per million tokens. `contextWindow` is the
|
|
148
|
-
size of the context window in tokens. `
|
|
151
|
+
size of the context window in tokens. `maxOutput`, when set, caps the
|
|
152
|
+
`max_tokens` that Claude Code sends; the API rejects a request above the
|
|
153
|
+
model's output limit. `billing` is `plan` for models that use
|
|
149
154
|
the subscription limits, or `credits` for models that bill usage credits.
|
|
150
155
|
`policy.cashCapUsd` applies to `credits` models only. `efforts` lists the
|
|
151
156
|
levels that the model accepts. An empty list means that the gateway removes
|
package/docs/design.md
CHANGED
|
@@ -10,7 +10,7 @@ terminal interface.
|
|
|
10
10
|
|
|
11
11
|
## Mechanism: local gateway (2026-09-22)
|
|
12
12
|
|
|
13
|
-
Claude Code runs with `--model router`. `ANTHROPIC_BASE_URL` points at a
|
|
13
|
+
Claude Code runs with `--model jev-router`. `ANTHROPIC_BASE_URL` points at a
|
|
14
14
|
gateway on `127.0.0.1`. The gateway sends each request to `api.anthropic.com`
|
|
15
15
|
unchanged, except a request whose `model` is the alias. For that request, the
|
|
16
16
|
gateway reads facts from the body, asks Jev once for each new user turn, runs
|
package/docs/user-guide.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Start a session
|
|
4
4
|
|
|
5
5
|
After the install steps in the README, run `claude` as usual. Claude Code shows
|
|
6
|
-
`Router (auto)` as the model. The transcript records the model that answered
|
|
6
|
+
`Jev Router (auto)` as the model. The transcript records the model that answered
|
|
7
7
|
each message. The first session starts the gateway, and all sessions share it.
|
|
8
8
|
The gateway continues to run after a session ends. After two hours without
|
|
9
9
|
requests it exits, unless a turn waits for a tool result, for example a
|
|
@@ -14,7 +14,7 @@ To try the gateway in one session without a change to the configuration:
|
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
TYPESAFE_API_KEY=… node scripts/gateway.mjs &
|
|
17
|
-
ANTHROPIC_BASE_URL=http://127.0.0.1:43170 claude --plugin-dir . --model router
|
|
17
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:43170 claude --plugin-dir . --model jev-router
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## What happens at each prompt
|
|
@@ -69,7 +69,7 @@ reason of the last turn in this session.
|
|
|
69
69
|
The status line wrapper from `/router:setup` adds one line to your status line
|
|
70
70
|
while the session uses the router:
|
|
71
71
|
|
|
72
|
-
- `router ▸ opus-5-5 · xhigh`: the model and the effort of the last turn.
|
|
72
|
+
- `jev-router ▸ opus-5-5 · xhigh`: the model and the effort of the last turn.
|
|
73
73
|
- `router: no turn yet`: the session has no routed turn.
|
|
74
74
|
- `router: gateway off, the next prompt starts it`: the gateway stopped after
|
|
75
75
|
idle time, or it crashed. Either way the next prompt starts it.
|
|
@@ -97,7 +97,7 @@ assistant message in a Claude Code transcript.
|
|
|
97
97
|
|
|
98
98
|
## Troubleshooting
|
|
99
99
|
|
|
100
|
-
- If Claude Code does not accept `router` as a model, make sure that the
|
|
100
|
+
- If Claude Code does not accept `jev-router` as a model, make sure that the
|
|
101
101
|
gateway runs and that `ANTHROPIC_BASE_URL` is set. The command
|
|
102
102
|
`curl http://127.0.0.1:43170/v1/models` lists the alias.
|
|
103
103
|
- If Claude Code reports that it does not use the gateway, run
|
package/lib/config.mjs
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
export const TIERS = ['micro', 'low', 'medium', 'high'];
|
|
4
4
|
export const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
5
5
|
|
|
6
|
+
// The alias before 0.4.2. Settings written by an older /router:setup still send it; drop it once they are rare.
|
|
7
|
+
export const LEGACY_ALIAS = 'router';
|
|
8
|
+
|
|
6
9
|
export const DEFAULTS = {
|
|
7
10
|
// `idleShutdownMs`: the gateway exits after this long without requests while no turn waits for a tool result;
|
|
8
11
|
// the hooks start it again before the next prompt. Two hours outlast /loop wakeups and Monitor waits (up to one
|
|
9
12
|
// hour), which reach the gateway without a prompt. 0 keeps it running.
|
|
10
|
-
gateway: { port: 43170, alias: 'router', baselineTier: 'low', auxiliaryTier: 'low', idleShutdownMs: 7_200_000 },
|
|
13
|
+
gateway: { port: 43170, alias: 'jev-router', baselineTier: 'low', auxiliaryTier: 'low', idleShutdownMs: 7_200_000 },
|
|
11
14
|
// `high` and `medium` share one model and differ by effort: Opus 5.5 at xhigh is the strongest
|
|
12
15
|
// setting this router can ask for, and the tier ladder stays four wide for the policy.
|
|
13
16
|
routes: {
|
|
@@ -36,7 +39,16 @@ export const DEFAULTS = {
|
|
|
36
39
|
billing: 'plan',
|
|
37
40
|
efforts: EFFORTS,
|
|
38
41
|
},
|
|
39
|
-
|
|
42
|
+
// `maxOutput` caps `max_tokens`: Claude Code asks for 128K, Haiku 4.5 answers more than 64K with 400.
|
|
43
|
+
haiku: {
|
|
44
|
+
id: 'claude-haiku-4-5',
|
|
45
|
+
input: 1,
|
|
46
|
+
cacheRead: 0.1,
|
|
47
|
+
contextWindow: 200_000,
|
|
48
|
+
maxOutput: 64_000,
|
|
49
|
+
billing: 'plan',
|
|
50
|
+
efforts: [],
|
|
51
|
+
},
|
|
40
52
|
},
|
|
41
53
|
cache: {
|
|
42
54
|
writeMultiplier: { '5m': 1.25, '1h': 2 },
|
|
@@ -105,6 +117,8 @@ function validate(config) {
|
|
|
105
117
|
throw new Error(`models.${alias}.${field} must be a non-negative number`);
|
|
106
118
|
}
|
|
107
119
|
if (typeof model.id !== 'string' || !model.id) throw new Error(`models.${alias}.id is required`);
|
|
120
|
+
if (model.maxOutput !== undefined && !(Number.isInteger(model.maxOutput) && model.maxOutput > 0))
|
|
121
|
+
throw new Error(`models.${alias}.maxOutput must be a positive integer`);
|
|
108
122
|
if (!['plan', 'credits'].includes(model.billing))
|
|
109
123
|
throw new Error(`models.${alias}.billing must be plan or credits`);
|
|
110
124
|
if (!Array.isArray(model.efforts) || model.efforts.some((e) => !EFFORTS.includes(e)))
|
package/lib/gateway.mjs
CHANGED
|
@@ -11,6 +11,19 @@ import { ROUTER_DISPLAY_NAME, STATUS_PATH, statusSnapshot } from './status.mjs';
|
|
|
11
11
|
const HOP_BY_HOP = new Set(['host', 'connection', 'content-length', 'accept-encoding', 'transfer-encoding']);
|
|
12
12
|
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
13
13
|
const MESSAGES_PATH = '/v1/messages';
|
|
14
|
+
const LONG_CONTEXT_BETA = /^context-1m-/;
|
|
15
|
+
const LONG_CONTEXT_WINDOW = 1_000_000;
|
|
16
|
+
|
|
17
|
+
// `jev-router[1m]` makes Claude Code send the 1M context beta on every request; a model with a smaller
|
|
18
|
+
// window answers it with 400 ("The long context beta is not yet available"), so drop it for those.
|
|
19
|
+
function withoutLongContextBeta(headers, modelId, config) {
|
|
20
|
+
const model = Object.values(config.models).find((m) => m.id === modelId);
|
|
21
|
+
const beta = headers['anthropic-beta'];
|
|
22
|
+
if (!model || model.contextWindow >= LONG_CONTEXT_WINDOW || typeof beta !== 'string') return;
|
|
23
|
+
const kept = beta.split(',').filter((flag) => !LONG_CONTEXT_BETA.test(flag.trim()));
|
|
24
|
+
if (kept.length) headers['anthropic-beta'] = kept.join(',');
|
|
25
|
+
else delete headers['anthropic-beta'];
|
|
26
|
+
}
|
|
14
27
|
|
|
15
28
|
export function createGateway({
|
|
16
29
|
router,
|
|
@@ -66,6 +79,7 @@ export function createGateway({
|
|
|
66
79
|
for (const [name, value] of Object.entries(req.headers)) if (!HOP_BY_HOP.has(name)) headers[name] = value;
|
|
67
80
|
headers.host = target.host;
|
|
68
81
|
headers['content-length'] = String(body.length);
|
|
82
|
+
if (routed) withoutLongContextBeta(headers, routed.body.model, router.config);
|
|
69
83
|
up = send(
|
|
70
84
|
{ host: target.hostname, port: target.port || undefined, path: req.url, method: req.method, headers },
|
|
71
85
|
(upRes) => relay(upRes, res, { routed, session, turn, clientLeft: () => closedEarly }),
|
package/lib/rewrite.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Turn a request for the router alias into a request for a concrete model. Only `model`,
|
|
1
|
+
// Turn a request for the router alias into a request for a concrete model. Only `model`, `max_tokens`,
|
|
2
2
|
// `output_config.effort`, `thinking` and thinking edits in `context_management` change;
|
|
3
3
|
// system, tools and messages are never touched.
|
|
4
4
|
import { EFFORTS } from './config.mjs';
|
|
@@ -7,6 +7,7 @@ export function rewriteRequest(body, tier, config) {
|
|
|
7
7
|
const route = config.routes[tier];
|
|
8
8
|
const model = config.models[route.model];
|
|
9
9
|
const out = { ...body, model: model.id };
|
|
10
|
+
if (model.maxOutput && out.max_tokens > model.maxOutput) out.max_tokens = model.maxOutput;
|
|
10
11
|
if (model.efforts.length === 0) return withoutThinking(out, body.output_config);
|
|
11
12
|
const effort = clampEffort(route.effort ?? body.output_config?.effort, model.efforts);
|
|
12
13
|
if (effort) out.output_config = { ...(body.output_config ?? {}), effort };
|
package/lib/router.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Per-request orchestration: facts -> advice -> policy -> rewritten body. Knows nothing about HTTP.
|
|
2
|
+
import { LEGACY_ALIAS } from './config.mjs';
|
|
2
3
|
import { nextContextTokens } from './cost.mjs';
|
|
3
4
|
import { factsFromRequest } from './facts.mjs';
|
|
4
5
|
import { askJev } from './jev.mjs';
|
|
@@ -38,7 +39,7 @@ export class Router {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
isRouted(body) {
|
|
41
|
-
return body?.model === this.config.gateway.alias;
|
|
42
|
+
return body?.model === this.config.gateway.alias || body?.model === LEGACY_ALIAS;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// Returns { body, tier, reason, auxiliary }. The caller forwards `body` and records the response
|
package/lib/status.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export function isOlderVersion(running, current) {
|
|
|
17
17
|
|
|
18
18
|
export const STATUS_PATH = '/router/status';
|
|
19
19
|
// The name of the alias in the /model picker and in discovery.
|
|
20
|
-
export const ROUTER_DISPLAY_NAME = 'Router (auto)';
|
|
20
|
+
export const ROUTER_DISPLAY_NAME = 'Jev Router (auto)';
|
|
21
21
|
const FETCH_TIMEOUT_MS = 300;
|
|
22
22
|
|
|
23
23
|
export function statusSnapshot(config, memory) {
|
package/package.json
CHANGED
package/scripts/statusline.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// A wrapped command (for example claude-powerline) gets the same stdin; its output comes first.
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { text } from 'node:stream/consumers';
|
|
7
|
+
import { LEGACY_ALIAS } from '../lib/config.mjs';
|
|
7
8
|
import { loadRuntime } from '../lib/runtime.mjs';
|
|
8
9
|
import { fetchStatus, ROUTER_DISPLAY_NAME, statusSegment } from '../lib/status.mjs';
|
|
9
10
|
|
|
@@ -15,8 +16,11 @@ try {
|
|
|
15
16
|
payload = JSON.parse(input);
|
|
16
17
|
} catch {}
|
|
17
18
|
|
|
18
|
-
const names = [config.gateway.alias, ROUTER_DISPLAY_NAME];
|
|
19
|
-
|
|
19
|
+
const names = [config.gateway.alias, LEGACY_ALIAS, ROUTER_DISPLAY_NAME];
|
|
20
|
+
// `jev-router[1m]`: the suffix only tells Claude Code the window size.
|
|
21
|
+
const routed = [payload.model?.id, payload.model?.display_name].some((name) =>
|
|
22
|
+
names.includes(name?.replace(/\[1m\]$/, '')),
|
|
23
|
+
);
|
|
20
24
|
const [status] = await Promise.all([
|
|
21
25
|
routed ? fetchStatus(config.gateway.port, payload.session_id) : null,
|
|
22
26
|
command ? runWrapped(command, args, input) : null,
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup
|
|
3
|
-
description: Point Claude Code at the router gateway. Adds `model
|
|
3
|
+
description: Point Claude Code at the router gateway. Adds `model` with the 1M window, `ANTHROPIC_BASE_URL` and the `/model` picker row to the user settings, and offers the status line.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
allowed-tools: Read, Edit, Write
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Configure Claude Code for the router gateway. This session does not use the gateway until Claude Code restarts. When the file has `model: "router"`, this session can fail its next request. So ask all questions first, write the file once, and write it last.
|
|
8
|
+
Configure Claude Code for the router gateway. This session does not use the gateway until Claude Code restarts. When the file has `model: "jev-router[1m]"`, this session can fail its next request. So ask all questions first, write the file once, and write it last.
|
|
9
9
|
|
|
10
10
|
1. Read `~/.claude/settings.json`. If the file does not exist, start from `{}`. Read `~/.claude/router.json` if it exists.
|
|
11
11
|
2. The status line can show the model and effort of the last routed turn. The command is `node ${CLAUDE_PLUGIN_ROOT}/scripts/statusline.mjs`, followed by the current status line command if there is one (for example `node ${CLAUDE_PLUGIN_ROOT}/scripts/statusline.mjs claude-powerline`). Show the user the current `statusLine` value and the new one, and ask. Change `statusLine.command` only if the user agrees. Keep the other `statusLine` keys.
|
|
12
12
|
3. Prepare the new settings object in memory:
|
|
13
|
-
- `model`: `"router"`.
|
|
13
|
+
- `model`: `"jev-router[1m]"`. Claude Code does not know the model `jev-router` and assumes a 200K window; the `[1m]` suffix declares 1M. Claude Code strips the suffix before it sends the request. If `router.json` routes no model with a 1M `contextWindow`, use `"jev-router"`.
|
|
14
14
|
- `env.ANTHROPIC_BASE_URL`: `"http://127.0.0.1:43170"`. If `router.json` sets `gateway.port`, use that port.
|
|
15
|
-
- `env.CLAUDE_CODE_MAX_CONTEXT_TOKENS
|
|
16
|
-
- The `/model` picker row. In `modelPicker.options`, replace the row whose `model` is `"router"`, or append it if there is none:
|
|
17
|
-
`{ "model": "router", "label": "Router (auto)", "description": "Auto-selects the model and effort for each turn", "behavesAs": "claude-opus-5-5" }`.
|
|
18
|
-
If `modelPicker` does not exist, set it to `{ "options": [<the row>] }`. Keep the other rows and `replaceBuiltInOptions`. `behavesAs` names a model that Claude Code knows; without it, Claude Code rejects `router` as a model that is not in its catalog. If `router.json` changes the `high` route, use the `id` of its model.
|
|
15
|
+
- Remove `env.CLAUDE_CODE_MAX_CONTEXT_TOKENS`. Older versions of this setup wrote it; Claude Code ignores it for `jev-router`. The gateway sends a turn only to a model whose window holds the context, and drops the 1M beta header for a model with a smaller window.
|
|
16
|
+
- The `/model` picker row. In `modelPicker.options`, replace the row whose `model` is `"jev-router[1m]"`, `"jev-router"` or `"router"` (the name before 0.4.2), or append it if there is none:
|
|
17
|
+
`{ "model": "jev-router[1m]", "label": "Jev Router (auto)", "description": "Auto-selects the model and effort for each turn", "behavesAs": "claude-opus-5-5" }`.
|
|
18
|
+
If `modelPicker` does not exist, set it to `{ "options": [<the row>] }`. Keep the other rows and `replaceBuiltInOptions`. `behavesAs` names a model that Claude Code knows; without it, Claude Code rejects `jev-router` as a model that is not in its catalog. If `router.json` changes the `high` route, use the `id` of its model.
|
|
19
19
|
- Remove `env.ANTHROPIC_CUSTOM_MODEL_OPTION`, `env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME` and `env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION`. Older versions of this setup wrote them; the `modelPicker` row replaces them.
|
|
20
20
|
- The `statusLine` change from step 2, if the user agreed.
|
|
21
21
|
- Keep every other key unchanged.
|
|
22
22
|
4. Tell the user, before you write the file:
|
|
23
|
-
- Restart Claude Code now. Until the restart, this session can show "There's an issue with the selected model (router)", because it still sends requests to Anthropic and not to the gateway.
|
|
23
|
+
- Restart Claude Code now. Until the restart, this session can show "There's an issue with the selected model (jev-router[1m])", because it still sends requests to Anthropic and not to the gateway.
|
|
24
24
|
- After the restart, the router serves each turn, and `/router:status` shows the routes and the last turn.
|
|
25
25
|
- The status line path contains the plugin version. After a plugin update, run `/router:setup` again.
|
|
26
|
-
- To stop the routing, remove `model`, `env.ANTHROPIC_BASE_URL
|
|
26
|
+
- To stop the routing, remove `model`, `env.ANTHROPIC_BASE_URL` and the `jev-router[1m]` row of `modelPicker.options`, and restore the status line command.
|
|
27
27
|
5. Write the whole object to `~/.claude/settings.json` with one Write call. Do not use a sequence of edits. Do nothing after this step.
|
|
28
28
|
|
|
29
29
|
Do not change any other file.
|