@alexeiled/claude-router 0.4.2 → 0.5.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "router",
3
- "displayName": "Router",
4
- "version": "0.4.2",
3
+ "displayName": "jev-router",
4
+ "version": "0.5.1",
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",
@@ -20,15 +20,15 @@ starts it again.
20
20
 
21
21
  ```json
22
22
  {
23
- "model": "jev-router",
23
+ "model": "jev-router[1m]",
24
24
  "env": {
25
25
  "ANTHROPIC_BASE_URL": "http://127.0.0.1:43170",
26
- "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "1000000"
26
+ "ENABLE_TOOL_SEARCH": "true"
27
27
  },
28
28
  "modelPicker": {
29
29
  "options": [
30
30
  {
31
- "model": "jev-router",
31
+ "model": "jev-router[1m]",
32
32
  "label": "Jev Router (auto)",
33
33
  "description": "Auto-selects the model and effort for each turn",
34
34
  "behavesAs": "claude-opus-5-5"
@@ -38,6 +38,11 @@ starts it again.
38
38
  }
39
39
  ```
40
40
 
41
+ With a custom `ANTHROPIC_BASE_URL`, Claude Code turns tool search off: every
42
+ MCP tool schema goes into each request, about 50K tokens with the claude.ai
43
+ connectors. `ENABLE_TOOL_SEARCH` keeps the schemas deferred, as with the
44
+ Anthropic API; `/context` lists them as "MCP tools (deferred)".
45
+
41
46
  The `modelPicker` row adds `Jev Router (auto)` to the `/model` picker, next to the
42
47
  built-in rows. `behavesAs` maps `jev-router` to a model that Claude Code knows.
43
48
  Without it, Claude Code rejects `jev-router` because the model is not in its
@@ -47,11 +52,14 @@ routes it, and setup replaces that row.
47
52
 
48
53
  Setup writes the file as its last step. Restart Claude Code after it. Until
49
54
  the restart, the session sends `jev-router` to Anthropic and shows "There's an
50
- issue with the selected model (jev-router)".
55
+ issue with the selected model (jev-router[1m])".
51
56
 
52
57
  Claude Code does not know the model `jev-router`, so it assumes a 200K window.
53
- `CLAUDE_CODE_MAX_CONTEXT_TOKENS` declares the real window: the largest
54
- `contextWindow` of the routed models, 1M by default. The gateway sends a turn
58
+ The `[1m]` suffix declares 1M, the largest `contextWindow` of the default
59
+ routes; Claude Code strips it before the request. `CLAUDE_CODE_MAX_CONTEXT_TOKENS`
60
+ does not work here: Claude Code ignores it for this model. With the suffix,
61
+ Claude Code sends the 1M context beta header on every request; the gateway
62
+ drops it for a model with a smaller window (Haiku answers it with 400). The gateway sends a turn
55
63
  only to a model whose window holds the context with room to spare (80%). A
56
64
  large session skips Haiku (200K) and goes to Sonnet or Opus. The
57
65
  `/router:status` reason for such a turn is `context-fit`.
@@ -138,15 +146,17 @@ frontmatter of `skills/<tier>/SKILL.md`. A test makes sure that they agree.
138
146
 
139
147
  ### models
140
148
 
141
- | Alias | ID | Input | Cache Read | Window | Billing | Efforts |
142
- | -------- | ------------------ | ----- | ---------- | ------ | ------- | ------------- |
143
- | `opus` | `claude-opus-5-5` | $4 | $0.2 | 1M | plan | all |
144
- | `sonnet` | `claude-sonnet-5` | $2 | $0.2 | 1M | plan | low–xhigh–max |
145
- | `haiku` | `claude-haiku-4-5` | $1 | $0.1 | 200k | plan | none |
149
+ | Alias | ID | Input | Cache Read | Window | Max output | Billing | Efforts |
150
+ | -------- | ------------------ | ----- | ---------- | ------ | ---------- | ------- | ------------- |
151
+ | `opus` | `claude-opus-5-5` | $4 | $0.2 | 1M | as sent | plan | all |
152
+ | `sonnet` | `claude-sonnet-5` | $2 | $0.2 | 1M | as sent | plan | low–xhigh–max |
153
+ | `haiku` | `claude-haiku-4-5` | $1 | $0.1 | 200k | 64k | plan | none |
146
154
 
147
155
  `id` is the model id that the gateway sends to Anthropic. `input` and
148
156
  `cacheRead` are list prices in USD per million tokens. `contextWindow` is the
149
- size of the context window in tokens. `billing` is `plan` for models that use
157
+ size of the context window in tokens. `maxOutput`, when set, caps the
158
+ `max_tokens` that Claude Code sends; the API rejects a request above the
159
+ model's output limit. `billing` is `plan` for models that use
150
160
  the subscription limits, or `credits` for models that bill usage credits.
151
161
  `policy.cashCapUsd` applies to `credits` models only. `efforts` lists the
152
162
  levels that the model accepts. An empty list means that the gateway removes
package/lib/config.mjs CHANGED
@@ -39,7 +39,16 @@ export const DEFAULTS = {
39
39
  billing: 'plan',
40
40
  efforts: EFFORTS,
41
41
  },
42
- haiku: { id: 'claude-haiku-4-5', input: 1, cacheRead: 0.1, contextWindow: 200_000, billing: 'plan', efforts: [] },
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
+ },
43
52
  },
44
53
  cache: {
45
54
  writeMultiplier: { '5m': 1.25, '1h': 2 },
@@ -108,6 +117,8 @@ function validate(config) {
108
117
  throw new Error(`models.${alias}.${field} must be a non-negative number`);
109
118
  }
110
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`);
111
122
  if (!['plan', 'credits'].includes(model.billing))
112
123
  throw new Error(`models.${alias}.billing must be plan or credits`);
113
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/claude-router",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Claude Code plugin: auto-picks the right model and effort for each turn using Jev routing.",
5
5
  "license": "MIT",
6
6
  "author": "Alexei Ledenev",
@@ -17,7 +17,10 @@ try {
17
17
  } catch {}
18
18
 
19
19
  const names = [config.gateway.alias, LEGACY_ALIAS, ROUTER_DISPLAY_NAME];
20
- const routed = [payload.model?.id, payload.model?.display_name].some((name) => names.includes(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
+ );
21
24
  const [status] = await Promise.all([
22
25
  routed ? fetchStatus(config.gateway.port, payload.session_id) : null,
23
26
  command ? runWrapped(command, args, input) : null,
@@ -1,29 +1,30 @@
1
1
  ---
2
2
  name: setup
3
- description: Point Claude Code at the router gateway. Adds `model`, `ANTHROPIC_BASE_URL`, the context window and the `/model` picker row to the user settings, and offers the status line.
3
+ description: Point Claude Code at the router gateway. Adds `model` with the 1M window, `ANTHROPIC_BASE_URL`, tool search 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: "jev-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`: `"jev-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`: `"1000000"`. Claude Code does not know the model `jev-router` and assumes a 200K window without this key. If `router.json` changes `routes` or `models`, use the largest `contextWindow` of the models that the routes use. The gateway sends a turn only to a model whose window holds the context.
16
- - The `/model` picker row. In `modelPicker.options`, replace the row whose `model` is `"jev-router"` or `"router"` (the name before 0.4.2), or append it if there is none:
17
- `{ "model": "jev-router", "label": "Jev Router (auto)", "description": "Auto-selects the model and effort for each turn", "behavesAs": "claude-opus-5-5" }`.
15
+ - `env.ENABLE_TOOL_SEARCH`: `"true"`. With a custom `ANTHROPIC_BASE_URL`, Claude Code turns tool search off and loads every MCP tool schema into each request (about 50K tokens with the claude.ai connectors). This key keeps the schemas deferred, as with the Anthropic API.
16
+ - 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.
17
+ - 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:
18
+ `{ "model": "jev-router[1m]", "label": "Jev Router (auto)", "description": "Auto-selects the model and effort for each turn", "behavesAs": "claude-opus-5-5" }`.
18
19
  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
20
  - 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
21
  - The `statusLine` change from step 2, if the user agreed.
21
22
  - Keep every other key unchanged.
22
23
  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 (jev-router)", because it still sends requests to Anthropic and not to the gateway.
24
+ - 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
25
  - After the restart, the router serves each turn, and `/router:status` shows the routes and the last turn.
25
26
  - 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`, `env.CLAUDE_CODE_MAX_CONTEXT_TOKENS` and the `jev-router` row of `modelPicker.options`, and restore the status line command.
27
+ - To stop the routing, remove `model`, `env.ANTHROPIC_BASE_URL`, `env.ENABLE_TOOL_SEARCH` and the `jev-router[1m]` row of `modelPicker.options`, and restore the status line command.
27
28
  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
29
 
29
30
  Do not change any other file.