@alexeiled/claude-router 0.2.2 → 0.4.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 +4 -4
- package/README.md +41 -15
- package/docs/configuration.md +28 -13
- package/docs/design.md +42 -2
- package/docs/user-guide.md +26 -12
- package/hooks/hooks.json +12 -0
- package/lib/config.mjs +10 -5
- package/lib/facts.mjs +7 -0
- package/lib/gateway.mjs +143 -58
- package/lib/idle.mjs +41 -0
- package/lib/jev.mjs +29 -8
- package/lib/policy.mjs +17 -0
- package/lib/router.mjs +82 -18
- package/lib/sse.mjs +7 -2
- package/lib/status.mjs +27 -7
- package/lib/store.mjs +40 -1
- package/package.json +2 -2
- package/scripts/ensure-gateway.mjs +78 -16
- package/scripts/gateway.mjs +64 -5
- package/skills/setup/SKILL.md +7 -6
- package/skills/status/SKILL.md +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "router",
|
|
3
3
|
"displayName": "Router",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.4.0",
|
|
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",
|
|
8
8
|
"license": "MIT",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"userConfig": {
|
|
11
11
|
"typesafe_api_key": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"title": "
|
|
14
|
-
"description": "
|
|
13
|
+
"title": "Jev API key (typesafe.ai)",
|
|
14
|
+
"description": "API key from typesafe.ai. Jev reads each prompt and picks the right model tier. Without a key every turn runs on Sonnet.",
|
|
15
15
|
"sensitive": true
|
|
16
16
|
}
|
|
17
17
|
}
|
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# claude-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/alexei-led/claude-router/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@alexeiled/claude-router)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
A Claude Code plugin that auto-picks the right model and effort for each turn.
|
|
4
9
|
|
|
5
10
|
A local gateway on `127.0.0.1` receives each request from Claude Code. For a
|
|
6
|
-
new user turn, the gateway asks TypeSafe
|
|
7
|
-
the gateway
|
|
11
|
+
new user turn, the gateway asks Jev (TypeSafe) which tier the turn needs. Then
|
|
12
|
+
the gateway rewrites `model`, `effort` and `thinking` in the request and sends
|
|
8
13
|
it to Anthropic. All other data goes through unchanged. The gateway has no
|
|
9
14
|
runtime dependencies and needs Node 22 or later.
|
|
10
15
|
|
|
@@ -21,14 +26,14 @@ Claude Code --model router ──▶ gateway 127.0.0.1:43170 ──▶ api.
|
|
|
21
26
|
responses go through unchanged; the gateway reads `usage` (context size, cache TTL)
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
The tiers are `micro` (
|
|
25
|
-
high effort) and `high` (
|
|
29
|
+
The tiers are `micro` (Haiku), `low` (Sonnet, the baseline), `medium` (Opus at
|
|
30
|
+
high effort) and `high` (Opus at xhigh effort). The exact model IDs are in
|
|
31
|
+
`~/.claude/router.json` and default to the current generation of each family.
|
|
26
32
|
|
|
27
|
-
A tool continuation
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
with their own `model` work.
|
|
33
|
+
A tool continuation keeps the route of its turn — the gateway does not ask Jev.
|
|
34
|
+
Side requests, for example session titles, get the baseline tier. A request for
|
|
35
|
+
any other model goes through unchanged. This is how `/router:<tier>` pins and
|
|
36
|
+
subagents with their own `model` work.
|
|
32
37
|
|
|
33
38
|
Module dependencies point in one direction: `gateway.mjs` (HTTP) →
|
|
34
39
|
`router.mjs` (orchestration) → `facts`, `jev`, `policy` → `cost`, `rewrite`,
|
|
@@ -44,16 +49,37 @@ Only `store` writes files. The Jev transport is injected.
|
|
|
44
49
|
claude plugin install router@alexei-led-claude-router
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
2. When Claude Code asks, enter the
|
|
48
|
-
macOS Keychain.
|
|
52
|
+
2. When Claude Code asks, enter the Jev API key from [typesafe.ai](https://typesafe.ai).
|
|
53
|
+
The key goes to the macOS Keychain and persists across updates.
|
|
49
54
|
3. In Claude Code, run `/router:setup`. It writes `model`,
|
|
50
55
|
`env.ANTHROPIC_BASE_URL` and the `/model` picker row to
|
|
51
56
|
`~/.claude/settings.json`, and offers a status line segment.
|
|
52
57
|
4. Restart Claude Code. `/router:status` shows the routes and the last turn.
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
One gateway serves all Claude Code sessions on the machine. The plugin hooks
|
|
60
|
+
start it at session start and before each prompt when it does not answer, so a
|
|
61
|
+
stopped or crashed gateway comes back on the next prompt. After two hours
|
|
62
|
+
without requests it exits by itself.
|
|
63
|
+
|
|
64
|
+
A claude.ai login continues to work: the gateway forwards the authorization
|
|
65
|
+
header and the `anthropic-beta` OAuth value unchanged.
|
|
66
|
+
|
|
67
|
+
## Update
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
claude plugin marketplace update alexei-led-claude-router
|
|
71
|
+
claude plugin update router@alexei-led-claude-router
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then restart Claude Code or run `/reload-plugins`. The next prompt replaces the
|
|
75
|
+
running gateway with the new version: the old gateway releases the port at once
|
|
76
|
+
and finishes the responses it is streaming. A session that still runs an older
|
|
77
|
+
plugin never replaces a newer gateway. The Jev API key stays in the macOS
|
|
78
|
+
Keychain. Run `/router:setup` once after an update: the status line command
|
|
79
|
+
path contains the plugin version.
|
|
80
|
+
|
|
81
|
+
Updating from 0.3.0 or earlier: these gateways cannot hand over, so stop the
|
|
82
|
+
old one once with `pkill -f scripts/gateway.mjs`.
|
|
57
83
|
|
|
58
84
|
## Documentation
|
|
59
85
|
|
package/docs/configuration.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
The gateway ignores project files. To use another file, set
|
|
13
13
|
`ROUTER_CONFIG=/path/to/file.json`. The gateway reads the file at start. After
|
|
14
|
-
an edit, stop the gateway with `pkill -f scripts/gateway.mjs`. The next
|
|
14
|
+
an edit, stop the gateway with `pkill -f scripts/gateway.mjs`. The next prompt
|
|
15
15
|
starts it again.
|
|
16
16
|
|
|
17
17
|
## Claude Code settings
|
|
@@ -23,9 +23,10 @@ starts it again.
|
|
|
23
23
|
"model": "router",
|
|
24
24
|
"env": {
|
|
25
25
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:43170",
|
|
26
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS": "1000000",
|
|
26
27
|
"ANTHROPIC_CUSTOM_MODEL_OPTION": "router",
|
|
27
28
|
"ANTHROPIC_CUSTOM_MODEL_OPTION_NAME": "Router (auto)",
|
|
28
|
-
"ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION": "Picks Opus 5.5 / Sonnet
|
|
29
|
+
"ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION": "Picks Opus 5.5 / Sonnet 5 / Haiku 4.5 and the effort for each turn"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
```
|
|
@@ -33,6 +34,13 @@ starts it again.
|
|
|
33
34
|
The three `ANTHROPIC_CUSTOM_MODEL_OPTION*` keys add a `Router (auto)` row to
|
|
34
35
|
the `/model` picker.
|
|
35
36
|
|
|
37
|
+
Claude Code does not know the model `router`, so it assumes a 200K window.
|
|
38
|
+
`CLAUDE_CODE_MAX_CONTEXT_TOKENS` declares the real window: the largest
|
|
39
|
+
`contextWindow` of the routed models, 1M by default. The gateway sends a turn
|
|
40
|
+
only to a model whose window holds the context with room to spare (80%). A
|
|
41
|
+
large session skips Haiku (200K) and goes to Sonnet or Opus. The
|
|
42
|
+
`/router:status` reason for such a turn is `context-fit`.
|
|
43
|
+
|
|
36
44
|
If you agree, it also wraps the status line command:
|
|
37
45
|
|
|
38
46
|
```json
|
|
@@ -45,7 +53,7 @@ If you agree, it also wraps the status line command:
|
|
|
45
53
|
```
|
|
46
54
|
|
|
47
55
|
The wrapper runs the command after it, then adds one line for a routed
|
|
48
|
-
session, for example `router ▸ opus-5-5 · xhigh
|
|
56
|
+
session, for example `router ▸ opus-5-5 · xhigh`. Without a command
|
|
49
57
|
after it, it prints only that line. The path contains the plugin version, so
|
|
50
58
|
run `/router:setup` again after a plugin update.
|
|
51
59
|
|
|
@@ -69,7 +77,8 @@ path. Nested objects merge.
|
|
|
69
77
|
"port": 43170,
|
|
70
78
|
"alias": "router",
|
|
71
79
|
"baselineTier": "low",
|
|
72
|
-
"auxiliaryTier": "low"
|
|
80
|
+
"auxiliaryTier": "low",
|
|
81
|
+
"idleShutdownMs": 7200000
|
|
73
82
|
},
|
|
74
83
|
"routes": {
|
|
75
84
|
"high": { "model": "opus", "effort": "xhigh" },
|
|
@@ -79,12 +88,12 @@ path. Nested objects merge.
|
|
|
79
88
|
},
|
|
80
89
|
"models": {
|
|
81
90
|
"sonnet": {
|
|
82
|
-
"id": "claude-sonnet-
|
|
83
|
-
"input":
|
|
84
|
-
"cacheRead": 0.
|
|
91
|
+
"id": "claude-sonnet-5",
|
|
92
|
+
"input": 2,
|
|
93
|
+
"cacheRead": 0.2,
|
|
85
94
|
"contextWindow": 1000000,
|
|
86
95
|
"billing": "plan",
|
|
87
|
-
"efforts": ["low", "medium", "high", "max"]
|
|
96
|
+
"efforts": ["low", "medium", "high", "xhigh", "max"]
|
|
88
97
|
}
|
|
89
98
|
},
|
|
90
99
|
"policy": {
|
|
@@ -114,11 +123,11 @@ frontmatter of `skills/<tier>/SKILL.md`. A test makes sure that they agree.
|
|
|
114
123
|
|
|
115
124
|
### models
|
|
116
125
|
|
|
117
|
-
| Alias | ID
|
|
118
|
-
| -------- |
|
|
119
|
-
| `opus` | `claude-opus-5-5`
|
|
120
|
-
| `sonnet` | `claude-sonnet-
|
|
121
|
-
| `haiku` | `claude-haiku-4-5`
|
|
126
|
+
| Alias | ID | Input | Cache Read | Window | Billing | Efforts |
|
|
127
|
+
| -------- | ------------------ | ----- | ---------- | ------ | ------- | ------------- |
|
|
128
|
+
| `opus` | `claude-opus-5-5` | $4 | $0.2 | 1M | plan | all |
|
|
129
|
+
| `sonnet` | `claude-sonnet-5` | $2 | $0.2 | 1M | plan | low–xhigh–max |
|
|
130
|
+
| `haiku` | `claude-haiku-4-5` | $1 | $0.1 | 200k | plan | none |
|
|
122
131
|
|
|
123
132
|
`id` is the model id that the gateway sends to Anthropic. `input` and
|
|
124
133
|
`cacheRead` are list prices in USD per million tokens. `contextWindow` is the
|
|
@@ -134,6 +143,7 @@ effort and thinking from the request.
|
|
|
134
143
|
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
135
144
|
| `gateway.baselineTier` | The tier when nothing else decides: Jev abstains, Jev fails, or the session is new. |
|
|
136
145
|
| `gateway.auxiliaryTier` | The tier for side requests, for example session titles. |
|
|
146
|
+
| `gateway.idleShutdownMs` | The gateway exits after this long without requests, when no turn waits for a tool result. Two hours by default; `0` keeps it running. |
|
|
137
147
|
| `upgradeVotes` | The number of consecutive votes above the current tier before an upgrade of one tier. |
|
|
138
148
|
| `upgradeBase`, `upgradeSlope`, `upgradePivotUsd` | The required probability mass: `base + slope * tax / (tax + pivot)`. The `tax` is the extra input cost to read the context on the new model. |
|
|
139
149
|
| `jumpConfidence` | The mass that lets a jump of two tiers skip the vote delay. |
|
|
@@ -150,3 +160,8 @@ effort and thinking from the request.
|
|
|
150
160
|
| `ROUTER_CONFIG` | The path of the configuration file. |
|
|
151
161
|
| `ROUTER_FORCE_TIER` | `micro`, `low`, `medium` or `high`. Skips Jev and the policy and always routes to that tier. For tests. |
|
|
152
162
|
| `CLAUDE_PLUGIN_DATA` | Set by Claude Code for hooks. The directory holds `sessions/`, `decisions.jsonl` and `gateway.log`. |
|
|
163
|
+
|
|
164
|
+
The gateway keeps the directory bounded. Above 20 MB, `decisions.jsonl` moves
|
|
165
|
+
to `decisions.jsonl.1` (checked every hour), and `gateway.log` moves to
|
|
166
|
+
`gateway.log.1` when a gateway starts. One previous generation is kept. Session
|
|
167
|
+
files unused for 30 days are removed.
|
package/docs/design.md
CHANGED
|
@@ -61,7 +61,7 @@ keeps its route. The baseline is a configuration value.
|
|
|
61
61
|
| ------ | ------ | ------- | -------------------- |
|
|
62
62
|
| high | opus | xhigh | claude-opus-5-5 |
|
|
63
63
|
| medium | opus | high | claude-opus-5-5 |
|
|
64
|
-
| low | sonnet | as sent | claude-sonnet-
|
|
64
|
+
| low | sonnet | as sent | claude-sonnet-5 |
|
|
65
65
|
| micro | haiku | none | claude-haiku-4-5 |
|
|
66
66
|
|
|
67
67
|
The gateway lowers the effort to a level that the model family accepts. Sonnet
|
|
@@ -79,6 +79,46 @@ account.
|
|
|
79
79
|
`format` is a side request.
|
|
80
80
|
- Any other `model`: unchanged. This covers `/router:<tier>` pins,
|
|
81
81
|
`/model` changes and subagents with their own model.
|
|
82
|
+
- A resent request (the same history length and the same last message): the
|
|
83
|
+
route of the turn, without a Jev call or a vote. Claude Code resends after a
|
|
84
|
+
429, a 529 or a dropped connection.
|
|
85
|
+
- A side endpoint with the alias, such as `/v1/messages/count_tokens`: the
|
|
86
|
+
model of the session's last route. Only `POST /v1/messages` is a turn.
|
|
87
|
+
|
|
88
|
+
## Failure handling
|
|
89
|
+
|
|
90
|
+
One gateway serves every Claude Code session on the machine, so a failure in one
|
|
91
|
+
request must not reach the others.
|
|
92
|
+
|
|
93
|
+
- Anthropic errors (429, 529, 5xx) pass through unchanged, with `Retry-After`.
|
|
94
|
+
Claude Code owns retries and backoff; a second retry layer in the gateway
|
|
95
|
+
would multiply attempts and cannot replay a stream that has started.
|
|
96
|
+
- The response is relayed with `pipeline()`. An upstream reset destroys the
|
|
97
|
+
client response, so Claude Code sees a reset and retries at once. A client
|
|
98
|
+
that leaves (Esc) destroys the upstream request, so the model stops
|
|
99
|
+
generating an answer that nobody reads.
|
|
100
|
+
- Disk errors while the gateway saves session memory or the decision log are
|
|
101
|
+
logged. The routing decision stands.
|
|
102
|
+
- Jev: one retry on a network error or a transient status, after
|
|
103
|
+
`Retry-After` when it fits the 1.5 s budget. After three failures in a row,
|
|
104
|
+
new turns skip Jev for a minute, then try once.
|
|
105
|
+
- The daemon logs a stray exception instead of exiting. On `SIGTERM` it
|
|
106
|
+
releases the port at once and finishes open streams for up to 10 minutes.
|
|
107
|
+
A second daemon on a busy port exits quietly.
|
|
108
|
+
- The `SessionStart` and `UserPromptSubmit` hooks start the gateway when the
|
|
109
|
+
port does not answer, and replace a gateway older than the plugin. They never
|
|
110
|
+
replace a newer one.
|
|
111
|
+
- The gateway exits after `gateway.idleShutdownMs` (two hours) without
|
|
112
|
+
requests. Claude Code holds no connection open between requests, so the
|
|
113
|
+
gateway cannot tell a closed session from an idle one; time is the signal.
|
|
114
|
+
Two cases keep it running: a request in flight, and a turn whose last
|
|
115
|
+
response asked for a tool (`stop_reason: tool_use`), such as an unanswered
|
|
116
|
+
permission prompt. The answer to that prompt reaches the gateway without a
|
|
117
|
+
new prompt, so without the hook that would start it again. A session that
|
|
118
|
+
died mid-turn stops counting after a day. Two hours outlast `/loop` wakeups
|
|
119
|
+
and Monitor waits, which also arrive without a prompt.
|
|
120
|
+
- The gateway refuses requests with a non-loopback `Host` (DNS rebinding) or a
|
|
121
|
+
web `Origin` (cross-site requests from a browser).
|
|
82
122
|
|
|
83
123
|
## Cache and cost inputs
|
|
84
124
|
|
|
@@ -141,7 +181,7 @@ Agreed with Codex on 2026-09-22. The thresholds are start values.
|
|
|
141
181
|
## Test results (2026-09-22, Max plan, Claude Code 2.1.278)
|
|
142
182
|
|
|
143
183
|
- Easy prompt: Jev gave `micro` at confidence 1. It was the first vote, so
|
|
144
|
-
`low` served (`claude-sonnet-
|
|
184
|
+
`low` served (`claude-sonnet-5`, 103,799 context tokens on the first
|
|
145
185
|
request).
|
|
146
186
|
- Hard prompt: Jev gave `high` at 0.66. A cold Fable write cost $1.30 against
|
|
147
187
|
the cap of $0.50 at that time, so `medium` served (`claude-opus-5`). Four
|
package/docs/user-guide.md
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
After the install steps in the README, run `claude` as usual. Claude Code shows
|
|
6
6
|
`Router (auto)` as the model. The transcript records the model that answered
|
|
7
|
-
each message. The first session starts the gateway
|
|
8
|
-
run after
|
|
7
|
+
each message. The first session starts the gateway, and all sessions share it.
|
|
8
|
+
The gateway continues to run after a session ends. After two hours without
|
|
9
|
+
requests it exits, unless a turn waits for a tool result, for example a
|
|
10
|
+
permission prompt that you have not answered. The next prompt starts it again
|
|
11
|
+
(`gateway.idleShutdownMs` in [configuration](configuration.md)).
|
|
9
12
|
|
|
10
13
|
To try the gateway in one session without a change to the configuration:
|
|
11
14
|
|
|
@@ -34,7 +37,12 @@ ANTHROPIC_BASE_URL=http://127.0.0.1:43170 claude --plugin-dir . --model router
|
|
|
34
37
|
prompt for these credits, so the cap is the only guard. No default model
|
|
35
38
|
bills credits; this applies once you add one in `router.json`.
|
|
36
39
|
- When Jev fails or times out, or when there is no key, the baseline tier
|
|
37
|
-
serves the turn.
|
|
40
|
+
serves the turn. After three failures in a row, new turns skip Jev for one
|
|
41
|
+
minute and then try it once, so an outage costs one slow turn a minute, not
|
|
42
|
+
a second on every turn. `/router:status` shows the pause.
|
|
43
|
+
- When Claude Code sends the same request again (after a 429, a 529 or a
|
|
44
|
+
dropped connection), the gateway keeps the route of that turn. A retry is
|
|
45
|
+
not a new vote and makes no Jev call.
|
|
38
46
|
- Jev receives the prompt and the last six turns of text. Tool results are not
|
|
39
47
|
sent. No other data leaves the machine, except the usual Anthropic request.
|
|
40
48
|
|
|
@@ -61,10 +69,10 @@ reason of the last turn in this session.
|
|
|
61
69
|
The status line wrapper from `/router:setup` adds one line to your status line
|
|
62
70
|
while the session uses the router:
|
|
63
71
|
|
|
64
|
-
- `router ▸ opus-5-5 · xhigh
|
|
65
|
-
last turn.
|
|
72
|
+
- `router ▸ opus-5-5 · xhigh`: the model and the effort of the last turn.
|
|
66
73
|
- `router: no turn yet`: the session has no routed turn.
|
|
67
|
-
- `router: gateway
|
|
74
|
+
- `router: gateway off, the next prompt starts it`: the gateway stopped after
|
|
75
|
+
idle time, or it crashed. Either way the next prompt starts it.
|
|
68
76
|
|
|
69
77
|
The line updates when Claude Code redraws the status line, after each message.
|
|
70
78
|
|
|
@@ -94,10 +102,16 @@ assistant message in a Claude Code transcript.
|
|
|
94
102
|
`curl http://127.0.0.1:43170/v1/models` lists the alias.
|
|
95
103
|
- If Claude Code reports that it does not use the gateway, run
|
|
96
104
|
`/router:setup` and restart Claude Code.
|
|
97
|
-
- If each turn runs on
|
|
98
|
-
|
|
105
|
+
- If each turn runs on the default tier, make sure that the key is set, and
|
|
106
|
+
read `/router:status`: it shows when Jev is paused after failures. The
|
|
107
|
+
timestamped `router:` lines in `gateway.log` next to `decisions.jsonl` give
|
|
108
|
+
the reason, for example `jev http 401` for a rejected key.
|
|
99
109
|
- If the effort is not what you set, read the `efforts` list of the model. The
|
|
100
|
-
gateway lowers the effort to a level that the model accepts.
|
|
101
|
-
|
|
102
|
-
- To stop the gateway, run `pkill -f scripts/gateway.mjs`.
|
|
103
|
-
|
|
110
|
+
gateway lowers the effort to a level that the model accepts. Haiku has no
|
|
111
|
+
effort and no thinking.
|
|
112
|
+
- To stop the gateway, run `pkill -f scripts/gateway.mjs`. It releases the port
|
|
113
|
+
at once and finishes the responses it is streaming. The next prompt starts it
|
|
114
|
+
again.
|
|
115
|
+
- Rate limits and overload errors from Anthropic (429, 529) reach Claude Code
|
|
116
|
+
unchanged. Claude Code waits and retries; the gateway does not add a second
|
|
117
|
+
layer of retries.
|
package/hooks/hooks.json
CHANGED
package/lib/config.mjs
CHANGED
|
@@ -4,7 +4,10 @@ export const TIERS = ['micro', 'low', 'medium', 'high'];
|
|
|
4
4
|
export const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
5
5
|
|
|
6
6
|
export const DEFAULTS = {
|
|
7
|
-
gateway
|
|
7
|
+
// `idleShutdownMs`: the gateway exits after this long without requests while no turn waits for a tool result;
|
|
8
|
+
// the hooks start it again before the next prompt. Two hours outlast /loop wakeups and Monitor waits (up to one
|
|
9
|
+
// 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 },
|
|
8
11
|
// `high` and `medium` share one model and differ by effort: Opus 5.5 at xhigh is the strongest
|
|
9
12
|
// setting this router can ask for, and the tier ladder stays four wide for the policy.
|
|
10
13
|
routes: {
|
|
@@ -26,12 +29,12 @@ export const DEFAULTS = {
|
|
|
26
29
|
efforts: EFFORTS,
|
|
27
30
|
},
|
|
28
31
|
sonnet: {
|
|
29
|
-
id: 'claude-sonnet-
|
|
30
|
-
input:
|
|
31
|
-
cacheRead: 0.
|
|
32
|
+
id: 'claude-sonnet-5',
|
|
33
|
+
input: 2,
|
|
34
|
+
cacheRead: 0.2,
|
|
32
35
|
contextWindow: 1_000_000,
|
|
33
36
|
billing: 'plan',
|
|
34
|
-
efforts:
|
|
37
|
+
efforts: EFFORTS,
|
|
35
38
|
},
|
|
36
39
|
haiku: { id: 'claude-haiku-4-5', input: 1, cacheRead: 0.1, contextWindow: 200_000, billing: 'plan', efforts: [] },
|
|
37
40
|
},
|
|
@@ -113,6 +116,8 @@ function validate(config) {
|
|
|
113
116
|
if (!Number.isInteger(config.gateway.port) || config.gateway.port < 1 || config.gateway.port > 65535)
|
|
114
117
|
throw new Error('gateway.port must be a port number');
|
|
115
118
|
if (typeof config.gateway.alias !== 'string' || !config.gateway.alias) throw new Error('gateway.alias is required');
|
|
119
|
+
if (!(Number.isFinite(config.gateway.idleShutdownMs) && config.gateway.idleShutdownMs >= 0))
|
|
120
|
+
throw new Error('gateway.idleShutdownMs must be a non-negative number (0 keeps the gateway running)');
|
|
116
121
|
const p = config.policy;
|
|
117
122
|
for (const field of ['upgradeBase', 'jumpConfidence', 'downgradeMass', 'continuationMass']) {
|
|
118
123
|
if (!(p[field] >= 0 && p[field] <= 1)) throw new Error(`policy.${field} must be between 0 and 1`);
|
package/lib/facts.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Facts the policy needs, derived from one Messages request body plus what the router remembers
|
|
2
2
|
// about the conversation. Pure: body and memory in, plain object out.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
5
6
|
const FAILURE_WINDOW = 40;
|
|
@@ -28,6 +29,8 @@ export function factsFromRequest(body, memory, { recentTurns, maxTextChars }) {
|
|
|
28
29
|
turns: turns.slice(-recentTurns),
|
|
29
30
|
prompt: last?.role === 'user' ? textOf(lastBlocks) : '',
|
|
30
31
|
continuation: last?.role === 'user' && lastBlocks.some((b) => b.type === 'tool_result'),
|
|
32
|
+
// The same history length and the same last message: Claude Code resent the request (429, 529, a dropped stream).
|
|
33
|
+
turnKey: last?.role === 'user' ? `${messages.length}:${hash(JSON.stringify(last.content))}` : null,
|
|
31
34
|
failure: repeatedFailure(errors, edits, messages.length - 1),
|
|
32
35
|
lastRoute: memory.lastRoute,
|
|
33
36
|
lastRequest: memory.lastRequest,
|
|
@@ -52,6 +55,10 @@ function signatureOf(block) {
|
|
|
52
55
|
return textOf(blocks(block.content)).toLowerCase().replace(/\d+/g, '#').replace(/\s+/g, ' ').slice(0, 120);
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function hash(text) {
|
|
59
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
function blocks(content) {
|
|
56
63
|
if (typeof content === 'string') return [{ type: 'text', text: content }];
|
|
57
64
|
return Array.isArray(content) ? content : [];
|