@workweave/router 0.2.10 → 0.2.12

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.
Files changed (47) hide show
  1. package/README.md +40 -14
  2. package/cc-statusline.sh +274 -23
  3. package/codex-skills/fm/SKILL.md +15 -0
  4. package/codex-skills/fm/scripts/emit.sh +8 -0
  5. package/codex-skills/force-model/SKILL.md +15 -0
  6. package/codex-skills/force-model/scripts/emit.sh +9 -0
  7. package/codex-skills/rf/SKILL.md +15 -0
  8. package/codex-skills/rf/scripts/emit.sh +7 -0
  9. package/codex-skills/router-feedback/SKILL.md +15 -0
  10. package/codex-skills/router-feedback/scripts/emit.sh +9 -0
  11. package/codex-skills/router-models/SKILL.md +51 -0
  12. package/codex-skills/router-off/SKILL.md +22 -0
  13. package/codex-skills/router-on/SKILL.md +22 -0
  14. package/codex-skills/router-status/SKILL.md +19 -0
  15. package/codex-skills/ufm/SKILL.md +14 -0
  16. package/codex-skills/ufm/scripts/emit.sh +3 -0
  17. package/codex-skills/unforce-model/SKILL.md +14 -0
  18. package/codex-skills/unforce-model/scripts/emit.sh +4 -0
  19. package/codex-status.sh +312 -0
  20. package/commands/beta.md +5 -0
  21. package/commands/models.md +46 -0
  22. package/commands/router-models.md +46 -0
  23. package/directives.tsv +13 -0
  24. package/install.sh +1849 -263
  25. package/package.json +7 -1
  26. package/pi-router/README.md +39 -7
  27. package/pi-router/skills/install-lsps/SKILL.md +75 -0
  28. package/pi-router/skills/lsp-guide/SKILL.md +63 -0
  29. package/pi-router/src/beta.ts +21 -0
  30. package/pi-router/src/compaction.ts +46 -8
  31. package/pi-router/src/config.ts +34 -5
  32. package/pi-router/src/context-window.ts +12 -0
  33. package/pi-router/src/dispatch.ts +35 -2
  34. package/pi-router/src/index.ts +12 -1
  35. package/pi-router/src/lsp-broker.ts +255 -0
  36. package/pi-router/src/lsp-client.ts +435 -0
  37. package/pi-router/src/lsp-format.ts +230 -0
  38. package/pi-router/src/lsp-install.ts +215 -0
  39. package/pi-router/src/lsp-protocol.ts +128 -0
  40. package/pi-router/src/lsp-servers.ts +361 -0
  41. package/pi-router/src/lsp.ts +529 -0
  42. package/pi-router/src/pricing.generated.ts +77 -70
  43. package/pi-router/src/provider.ts +15 -2
  44. package/pi-router/src/routed-model.ts +17 -0
  45. package/pi-router/src/savings.ts +1 -1
  46. package/registry.sh +102 -0
  47. package/uninstall.sh +210 -31
package/README.md CHANGED
@@ -15,11 +15,13 @@ npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompt
15
15
  ```
16
16
 
17
17
  Re-running the installer to pick up changes reuses the key already on disk, so
18
- you paste it once and never again. `update` is the never-prompting form of that
19
- (safe for cron; errors instead of asking when no key can be found):
18
+ you paste it once and never again for every client, not just Claude Code.
19
+ `update` is the never-prompting form of that (safe for cron; errors instead of
20
+ asking when no key can be found):
20
21
 
21
22
  ```bash
22
23
  npx @workweave/router --claude # reuses the installed key
24
+ npx @workweave/router --codex # same for Codex, opencode, and pi
23
25
  npx @workweave/router --claude --rotate-key # ignore it and prompt for a new one
24
26
  npx @workweave/router update --claude # non-interactive refresh in place
25
27
  ```
@@ -27,8 +29,11 @@ npx @workweave/router update --claude # non-interactive refresh in place
27
29
  For Claude Code the installed statusline and `/force-model`, `/router-*` slash
28
30
  commands also refresh themselves in the background about once a week (never
29
31
  overwriting a wrapper you edited). Opt out with `WEAVE_STATUSLINE_UPDATE=0`, or
30
- just the commands with `WEAVE_COMMANDS_UPDATE=0`. Codex, opencode, and pi have
31
- no per-turn hook to refresh from re-run the installer for those.
32
+ just the commands with `WEAVE_COMMANDS_UPDATE=0`. Codex installs native `$` skills
33
+ plus managed `SessionStart`/`Stop` hooks: the latest routed model is reflected in
34
+ the terminal title and a compact `Weave Router · …` status message is shown when
35
+ the router reports a new route. Existing Codex hooks are preserved. OpenCode and pi
36
+ have their own target-specific integrations.
32
37
 
33
38
  Version-pin for reproducible setups:
34
39
 
@@ -48,12 +53,28 @@ npx @workweave/router status --codex # is Codex on the router or direct?
48
53
  Claude Code reads its router setting at launch, so quit and reopen it after an
49
54
  on/off. Codex and opencode pick it up on their next run. Inside Claude Code the
50
55
  slash commands `/router-off`, `/router-on`, and `/router-status` do the same.
51
- Codex installs a `$disable-routing` skill that switches its next session back
56
+ Codex installs `$router-status`, `$router-off`, `$router-on`, and
57
+ `$router-models` skills that call the same CLI verbs, plus a
58
+ `$disable-routing` skill that switches its next session back
52
59
  to the normal provider; Codex does not support third-party `/disable-routing`
53
60
  slash commands. The shell equivalent is `npx @workweave/router disable-routing`.
54
61
  Cursor has no config file we own — toggle its base URL override in **Settings →
55
62
  Models** instead.
56
63
 
64
+ Pick which models the router is allowed to route to:
65
+
66
+ ```bash
67
+ npx @workweave/router models --claude # list every model, with its on/off state
68
+ npx @workweave/router models --codex # same, for a Codex install
69
+ npx @workweave/router models disable gpt-5.6 --claude # take one out of rotation
70
+ npx @workweave/router models enable gpt-5.6 --claude # put it back
71
+ ```
72
+
73
+ Inside Claude Code that's `/router-models` (alias `/models`). Editing needs a
74
+ router that serves the model-selection API; against the Weave-hosted router the
75
+ list still prints and points you at the dashboard, where model selection is an
76
+ organization-wide setting.
77
+
57
78
  Uninstall:
58
79
 
59
80
  ```bash
@@ -82,18 +103,24 @@ Four install targets:
82
103
  - **Codex** (`--codex`) — patches `~/.codex/config.toml` (or
83
104
  `<repo>/.codex/config.toml`) with a managed `[model_providers.weave]`
84
105
  block plus `model_provider = "weave"`. The provider preserves the existing
85
- ChatGPT OAuth login. The public hosted endpoint sends
86
- `X-Weave-Router-Strategy: hmm`; `--local` and custom self-hosted URLs keep
87
- their router's configured default because its HMM sidecar is optional. HMM or forced
106
+ ChatGPT OAuth login. No install pins `X-Weave-Router-Strategy`; every
107
+ endpoint keeps its router's configured default. HMM or forced
88
108
  `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` turns use that plan;
89
109
  every other selected model uses its WorkWeave deployment or BYOK credential.
90
110
  The block lives between begin/end markers
91
111
  so re-running the installer rewrites it cleanly and `--uninstall --codex`
92
112
  removes it without touching the rest of your config. Codex does not load
93
- third-party slash-command files; to send a router directive, type it with
94
- one leading space (for example, ` /force-model gpt-5.6-terra`). Its
95
- `$disable-routing` skill returns the next Codex session to the default
96
- provider without logging out or deleting the router configuration.
113
+ third-party slash-command files; the installer provides native skills
114
+ `$force-model` (`$fm`), `$unforce-model` (`$ufm`), and `$router-feedback`
115
+ (`$rf`), each of which execs a local `scripts/emit.sh` that prints the same
116
+ leading-space directive Claude Code uses (for example,
117
+ ` /force-model gpt-5.6-terra`) — you can also type that form directly. It
118
+ also installs `$router-status`, `$router-off`, `$router-on`, and
119
+ `$router-models`, which call this installer's own verbs, plus a
120
+ `$disable-routing` skill that returns the next Codex session to the default
121
+ provider without logging out or deleting the router configuration. The
122
+ managed lifecycle hooks also keep the latest routed model in the terminal
123
+ title and emit a compact status message when the router reports a new route.
97
124
  - **opencode** (`--opencode`) — merges a `provider.weave` entry (backed by
98
125
  opencode's built-in `@ai-sdk/anthropic` provider) into
99
126
  `~/.config/opencode/opencode.json` (or `<repo>/opencode.json` with
@@ -115,8 +142,7 @@ for the full reference.
115
142
 
116
143
  - Node ≥ 18 (ships with `npx`)
117
144
  - `bash` on PATH (macOS / Linux native; Windows needs Git Bash or WSL)
118
- - `jq` on PATH — used by the Claude Code status line and the opencode/pi JSON
119
- merges. Not required for the Codex path.
145
+ - `jq` on PATH — used by the Claude Code status line, the Codex lifecycle helper, and the opencode/pi JSON merges.
120
146
 
121
147
  ## Why npx
122
148
 
package/cc-statusline.sh CHANGED
@@ -16,9 +16,9 @@
16
16
  # Renders:
17
17
  # WEAVE ROUTER — claude-sonnet-4-5 ← claude-opus-4-7 · saved $1.23 · 12.4k in / 3.1k out / 45.2k cached
18
18
  #
19
- # Pricing source of truth: router/eval/pricing.py. Keep these maps in lockstep
20
- # when prices change. Cache multipliers (1.25× / 0.1×) follow Anthropic's
21
- # published cache pricing and are stable across the Claude family.
19
+ # Pricing source of truth: internal/router/catalog. Input/output prices and
20
+ # cache-read multipliers are generated by cmd/genprices. Cache creation remains
21
+ # at 1.25× input pending TTL-aware pricing.
22
22
 
23
23
  set -euo pipefail
24
24
 
@@ -159,6 +159,23 @@ weave_command_tracked_by_git() {
159
159
  git -C "$(dirname "$1")" ls-files --error-unmatch -- "$1" >/dev/null 2>&1
160
160
  }
161
161
 
162
+ # weave_installed_command_names lists the wrappers this install may refresh.
163
+ # The statusline ships standalone (no registry.sh beside it), so the installed
164
+ # set — itself written from the registry — is the only source of truth here.
165
+ #
166
+ # Files written before ownership markers existed carry none, so matching on the
167
+ # marker alone would freeze every pre-marker install out of refreshes forever.
168
+ # List every wrapper instead and let the baseline comparison below decide: a
169
+ # file is replaced only when its bytes still match the last canonical copy, so
170
+ # a user-authored command is never touched whether or not it carries a marker.
171
+ weave_installed_command_names() {
172
+ local dir="$1" file
173
+ for file in "$dir"/*.md; do
174
+ [ -f "$file" ] || continue
175
+ printf '%s\n' "$(basename "$file" .md)"
176
+ done
177
+ }
178
+
162
179
  # weave_render_command prints $1 with the installer's {{SCOPE}} placeholder
163
180
  # replaced by $2, matching how install_slash_commands writes the same file.
164
181
  # Trailing newlines are stripped on both sides of every comparison below.
@@ -228,8 +245,7 @@ weave_sync_commands() {
228
245
  # Detach stdin (CC pipes JSON to us) so curl can't consume it, and silence
229
246
  # everything so no output leaks into the statusline.
230
247
  exec </dev/null
231
- for name in force-model unforce-model router-feedback fm ufm rf \
232
- router-off router-on router-status router-session; do
248
+ while IFS= read -r name; do
233
249
  installed="$cmd_dir/$name.md"
234
250
  # Only ever refresh a wrapper that is already installed: a missing one
235
251
  # was uninstalled or deliberately deleted, and resurrecting it would be
@@ -258,10 +274,10 @@ weave_sync_commands() {
258
274
  if [ -f "$prev" ]; then
259
275
  new_body="$(weave_render_command "$raw" "$scope_args")"
260
276
  prev_body="$(weave_render_command "$prev" "$scope_args")"
261
- installed_body="$(cat "$installed" 2>/dev/null)" || installed_body=""
277
+ installed_body="$(cat "$installed" 2>/dev/null | sed '/^<!-- weave-router managed command: .* -->$/d')" || installed_body=""
262
278
  if [ "$prev_body" = "$installed_body" ] && [ "$new_body" != "$installed_body" ]; then
263
279
  tmp="$installed.tmp.$$"
264
- if printf '%s\n' "$new_body" >"$tmp" 2>/dev/null; then
280
+ if printf '%s\n<!-- weave-router managed command: %s -->' "$new_body" "$name" >"$tmp" 2>/dev/null; then
265
281
  mv "$tmp" "$installed" 2>/dev/null || rm -f "$tmp"
266
282
  else
267
283
  rm -f "$tmp"
@@ -269,13 +285,152 @@ weave_sync_commands() {
269
285
  fi
270
286
  fi
271
287
  mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
272
- done
288
+ done <<EOF
289
+ $(weave_installed_command_names "$cmd_dir")
290
+ EOF
273
291
  ) >/dev/null 2>&1 &
274
292
  disown 2>/dev/null || true
275
293
  return 0
276
294
  }
277
295
  weave_sync_commands 2>/dev/null || true
278
296
 
297
+ # ---------- org "hide terminal surfaces" gate ----------
298
+ #
299
+ # When the org has hidden the router's terminal surfaces, the statusline
300
+ # renders nothing: this prints no output and exits 0, leaving the slot blank.
301
+ # The setting comes from GET /v1/display-settings with the install's own
302
+ # router key, but the foreground path NEVER touches the network: it decides
303
+ # solely from a per-install cache file (TTL WEAVE_DISPLAY_SETTINGS_TTL_SECONDS,
304
+ # default 1h), and a missing or stale cache fails open — the statusline
305
+ # renders normally. When the cache is missing or stale a detached background
306
+ # refresh re-fetches the setting and rewrites the cache atomically, so the
307
+ # next turn picks up the fresh value; a refresh failure simply leaves the
308
+ # cache stale, which keeps failing open rather than pinning the gate closed.
309
+ #
310
+ # Claude Code runs the statusline every turn, so several invocations can see a
311
+ # stale cache at once. Letting each fetch independently is not safe: the
312
+ # responses can land out of order, and the loser's mv would replace a newer
313
+ # setting with an older one AND stamp it fresh, pinning the gate on a stale
314
+ # value for a full TTL. Refreshes are therefore serialized per cache key on a
315
+ # mkdir mutex (atomic everywhere; flock is absent on macOS), held across both
316
+ # the fetch and the write. A refresh that finds the mutex held exits rather
317
+ # than queueing — the in-flight one is at least as fresh as anything it would
318
+ # fetch, so waiting only to overwrite it is the bug. The one path that is not
319
+ # strictly exclusive is reclaiming a lock whose holder died; see below.
320
+ weave_hidden_gate() {
321
+ command -v curl >/dev/null 2>&1 || return 1
322
+ command -v jq >/dev/null 2>&1 || return 1
323
+
324
+ local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
325
+ mkdir -p "$cache_dir" 2>/dev/null || return 1
326
+ local self="${BASH_SOURCE[0]:-$0}"
327
+ local script_slug
328
+ script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
329
+ local cache="$cache_dir/display-settings${script_slug}"
330
+ local ttl="${WEAVE_DISPLAY_SETTINGS_TTL_SECONDS:-3600}"
331
+
332
+ local now mtime fresh="false"
333
+ now="$(date +%s 2>/dev/null)" || now=0
334
+ if [ -f "$cache" ]; then
335
+ mtime="$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null)" || mtime=0
336
+ if [ -n "${mtime:-}" ] && [ "$mtime" -gt 0 ] && [ $(( now - mtime )) -lt "$ttl" ]; then
337
+ fresh="true"
338
+ fi
339
+ fi
340
+
341
+ # Foreground decision: only a fresh cache hides the surfaces. Anything else
342
+ # (no cache, stale cache, unreadable cache) renders normally so a slow or
343
+ # unreachable router can never stall a turn or wedge the statusline blank.
344
+ if [ "$fresh" = "true" ]; then
345
+ [ "$(cat "$cache" 2>/dev/null)" = "1" ]
346
+ return
347
+ fi
348
+
349
+ # Background refresh for the next invocation. Resolve the router base URL
350
+ # and key inside the subshell from the Claude Code settings the installer
351
+ # wrote: project/--dir installs put both under <base>/.claude alongside
352
+ # this script (key in settings.local.json), while a user-scope install
353
+ # lives under ~/.weave and reads ~/.claude. Resolve relative to the
354
+ # script's own location, falling back to user scope, so a project install
355
+ # never reads (or leaks) the user-scope key. ANTHROPIC_BASE_URL and
356
+ # WEAVE_ROUTER_BASE_URL may also be set in the environment. A file:// base
357
+ # URL is the offline/test seam: curl reads it as the response body
358
+ # directly, so the endpoint path is meaningless for it; real router URLs
359
+ # (https) get /v1/display-settings appended.
360
+ (
361
+ exec </dev/null
362
+
363
+ # Take the per-cache-key mutex, or bail. mkdir is the portable atomic
364
+ # test-and-set. A crashed holder would otherwise block refreshes forever,
365
+ # so a lock older than the fetch timeout is treated as abandoned and
366
+ # reclaimed. Releasing from a trap covers every exit path below.
367
+ lock="$cache.lock"
368
+ if ! mkdir "$lock" 2>/dev/null; then
369
+ lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
370
+ lock_now="$(date +%s 2>/dev/null)" || lock_now=0
371
+ if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
372
+ exit 0
373
+ fi
374
+ # Reclaiming an abandoned lock must not be delete-then-recreate. With
375
+ # `rm -rf` + `mkdir`, refreshers that all see the same stale lock each
376
+ # delete the next one's freshly created directory, so many end up holding
377
+ # it at once (measured at 50 claimants: 4-11 concurrent holders) and their
378
+ # writes can land out of order. Renaming is atomic, so only one racer can
379
+ # move a given directory aside and the losers exit instead of clobbering
380
+ # the winner (same measurement: 1-2). It is not a perfect mutex — a
381
+ # straggler can still reclaim the new lock a winner just created, since
382
+ # nothing distinguishes it from the stale one — but real refreshes arrive
383
+ # one per turn rather than 50 at once, and the staleness threshold below
384
+ # is what bounds the rest.
385
+ dead="$lock.dead.$$"
386
+ mv "$lock" "$dead" 2>/dev/null || exit 0
387
+ rm -rf "$dead" 2>/dev/null
388
+ mkdir "$lock" 2>/dev/null || exit 0
389
+ fi
390
+ trap 'rmdir "$lock" 2>/dev/null' EXIT
391
+
392
+ self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd)"
393
+ settings_base="$HOME"
394
+ case "$self_dir" in
395
+ */.claude) settings_base="${self_dir%/.claude}" ;;
396
+ esac
397
+ settings="$settings_base/.claude/settings.json"
398
+ local_settings="$settings_base/.claude/settings.local.json"
399
+ base_url="${WEAVE_ROUTER_BASE_URL:-${ANTHROPIC_BASE_URL:-}}"
400
+ key="${WEAVE_ROUTER_KEY:-}"
401
+ if [ -z "$key" ] && [ -f "$settings" ]; then
402
+ key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$settings" 2>/dev/null | head -n1)"
403
+ fi
404
+ if [ -z "$key" ] && [ -f "$local_settings" ]; then
405
+ key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$local_settings" 2>/dev/null | head -n1)"
406
+ fi
407
+ if [ -z "$base_url" ] && [ -f "$settings" ]; then
408
+ base_url="$(jq -r '.env.ANTHROPIC_BASE_URL // empty' "$settings" 2>/dev/null)"
409
+ fi
410
+ [ -n "$base_url" ] && [ -n "$key" ] || exit 0
411
+ url="${base_url%/}"
412
+ case "$url" in
413
+ file://*) ;;
414
+ *) url="$url/v1/display-settings" ;;
415
+ esac
416
+ body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
417
+ hidden="$(printf '%s' "$body" | jq -r '.hide_terminal_surfaces // false' 2>/dev/null)"
418
+ tmp="$cache.tmp.$$"
419
+ if [ "$hidden" = "true" ]; then
420
+ printf '1' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
421
+ else
422
+ printf '0' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
423
+ fi
424
+ rm -f "$tmp" 2>/dev/null
425
+ ) >/dev/null 2>&1 &
426
+ disown 2>/dev/null || true
427
+ return 1
428
+ }
429
+
430
+ if weave_hidden_gate </dev/null; then
431
+ exit 0
432
+ fi
433
+
279
434
  input="$(cat)"
280
435
  transcript_path="$(printf '%s' "$input" | jq -r '.transcript_path // empty')"
281
436
  # Prefer model.id over display_name: pricing keys + the routed model id in
@@ -316,6 +471,7 @@ prices='{
316
471
  "claude-sonnet-5": 0.003,
317
472
  "deepseek/deepseek-v4-flash": 0.0001134,
318
473
  "deepseek/deepseek-v4-pro": 0.00174,
474
+ "deepseek/deepseek-v4-pro-0813": 0.00174,
319
475
  "gemini-2.0-flash": 0.0001,
320
476
  "gemini-2.0-flash-lite": 0.000075,
321
477
  "gemini-2.5-flash": 0.0003,
@@ -328,6 +484,7 @@ prices='{
328
484
  "gemini-3.5-flash": 0.0015,
329
485
  "gemini-3.5-flash-lite": 0.0003,
330
486
  "gemini-3.6-flash": 0.0015,
487
+ "gemini-3.7-flash": 0.0015,
331
488
  "google/gemma-4-26b-a4b-it": 0.00015,
332
489
  "gpt-4.1": 0.002,
333
490
  "gpt-4.1-mini": 0.0004,
@@ -347,7 +504,9 @@ prices='{
347
504
  "gpt-5.5-nano": 0.00015,
348
505
  "gpt-5.5-pro": 0.03,
349
506
  "gpt-5.6-luna": 0.001,
507
+ "gpt-5.6-luna-pro": 0.001,
350
508
  "gpt-5.6-sol": 0.005,
509
+ "gpt-5.6-sol-pro": 0.005,
351
510
  "gpt-5.6-terra": 0.0025,
352
511
  "grok-4.5": 0.002,
353
512
  "grok-4.6": 0.002,
@@ -370,7 +529,9 @@ prices='{
370
529
  "xiaomi/mimo-v2.5-pro": 0.001,
371
530
  "z-ai/glm-5": 0.001,
372
531
  "z-ai/glm-5.1": 0.0014,
373
- "z-ai/glm-5.2": 0.0014
532
+ "z-ai/glm-5.2": 0.0014,
533
+ "z-ai/glm-5.3": 0.0014,
534
+ "z-ai/glm-5.3-flash": 0.00015
374
535
  },
375
536
  "output": {
376
537
  "claude-fable-5": 0.05,
@@ -387,6 +548,7 @@ prices='{
387
548
  "claude-sonnet-5": 0.015,
388
549
  "deepseek/deepseek-v4-flash": 0.0002791,
389
550
  "deepseek/deepseek-v4-pro": 0.00348,
551
+ "deepseek/deepseek-v4-pro-0813": 0.00348,
390
552
  "gemini-2.0-flash": 0.0004,
391
553
  "gemini-2.0-flash-lite": 0.0003,
392
554
  "gemini-2.5-flash": 0.0012,
@@ -399,6 +561,7 @@ prices='{
399
561
  "gemini-3.5-flash": 0.009,
400
562
  "gemini-3.5-flash-lite": 0.0025,
401
563
  "gemini-3.6-flash": 0.0075,
564
+ "gemini-3.7-flash": 0.0075,
402
565
  "google/gemma-4-26b-a4b-it": 0.0006,
403
566
  "gpt-4.1": 0.008,
404
567
  "gpt-4.1-mini": 0.0016,
@@ -418,7 +581,9 @@ prices='{
418
581
  "gpt-5.5-nano": 0.0006,
419
582
  "gpt-5.5-pro": 0.18,
420
583
  "gpt-5.6-luna": 0.006,
584
+ "gpt-5.6-luna-pro": 0.006,
421
585
  "gpt-5.6-sol": 0.03,
586
+ "gpt-5.6-sol-pro": 0.03,
422
587
  "gpt-5.6-terra": 0.015,
423
588
  "grok-4.5": 0.006,
424
589
  "grok-4.6": 0.006,
@@ -441,7 +606,86 @@ prices='{
441
606
  "xiaomi/mimo-v2.5-pro": 0.003,
442
607
  "z-ai/glm-5": 0.0032,
443
608
  "z-ai/glm-5.1": 0.0044,
444
- "z-ai/glm-5.2": 0.0044
609
+ "z-ai/glm-5.2": 0.0044,
610
+ "z-ai/glm-5.3": 0.0044,
611
+ "z-ai/glm-5.3-flash": 0.0005
612
+ },
613
+ "cache_read": {
614
+ "claude-fable-5": 0.1,
615
+ "claude-haiku-4-5": 0.1,
616
+ "claude-opus-4-0": 0.1,
617
+ "claude-opus-4-1": 0.1,
618
+ "claude-opus-4-5": 0.1,
619
+ "claude-opus-4-6": 0.1,
620
+ "claude-opus-4-7": 0.1,
621
+ "claude-opus-4-8": 0.1,
622
+ "claude-opus-5": 0.1,
623
+ "claude-sonnet-4-5": 0.1,
624
+ "claude-sonnet-4-6": 0.1,
625
+ "claude-sonnet-5": 0.1,
626
+ "deepseek/deepseek-v4-flash": 0.2,
627
+ "deepseek/deepseek-v4-pro": 0.11494252873563218,
628
+ "deepseek/deepseek-v4-pro-0813": 0.11494252873563218,
629
+ "gemini-2.0-flash": 0.25,
630
+ "gemini-2.0-flash-lite": 0.25,
631
+ "gemini-2.5-flash": 0.1,
632
+ "gemini-2.5-flash-lite": 0.1,
633
+ "gemini-2.5-pro": 0.1,
634
+ "gemini-3-flash-preview": 0.1,
635
+ "gemini-3-pro-preview": 0.1,
636
+ "gemini-3.1-flash-lite-preview": 0.1,
637
+ "gemini-3.1-pro-preview": 0.1,
638
+ "gemini-3.5-flash": 0.1,
639
+ "gemini-3.5-flash-lite": 0.1,
640
+ "gemini-3.6-flash": 0.1,
641
+ "gemini-3.7-flash": 0.1,
642
+ "google/gemma-4-26b-a4b-it": 0.1,
643
+ "gpt-4.1": 0.25,
644
+ "gpt-4.1-mini": 0.25,
645
+ "gpt-4.1-nano": 0.25,
646
+ "gpt-4o": 0.5,
647
+ "gpt-4o-mini": 0.5,
648
+ "gpt-5": 0.1,
649
+ "gpt-5-chat": 0.1,
650
+ "gpt-5-mini": 0.1,
651
+ "gpt-5-nano": 0.1,
652
+ "gpt-5.4": 0.1,
653
+ "gpt-5.4-mini": 0.1,
654
+ "gpt-5.4-nano": 0.1,
655
+ "gpt-5.4-pro": 1,
656
+ "gpt-5.5": 0.1,
657
+ "gpt-5.5-mini": 0.1,
658
+ "gpt-5.5-nano": 0.1,
659
+ "gpt-5.5-pro": 1,
660
+ "gpt-5.6-luna": 0.1,
661
+ "gpt-5.6-luna-pro": 0.1,
662
+ "gpt-5.6-sol": 0.1,
663
+ "gpt-5.6-sol-pro": 0.1,
664
+ "gpt-5.6-terra": 0.1,
665
+ "grok-4.5": 0.25,
666
+ "grok-4.6": 0.25,
667
+ "minimax/minimax-m2.7": 0.2,
668
+ "minimax/minimax-m3": 0.2,
669
+ "mistralai/mistral-small-2603": 0.1,
670
+ "moonshotai/kimi-k2.5": 0.5,
671
+ "moonshotai/kimi-k2.6": 0.1684,
672
+ "moonshotai/kimi-k2.7": 0.2,
673
+ "moonshotai/kimi-k3": 0.1,
674
+ "qwen/qwen3-235b-a22b-2507": 0.5,
675
+ "qwen/qwen3-30b-a3b-instruct-2507": 0.1684,
676
+ "qwen/qwen3-coder": 0.1684,
677
+ "qwen/qwen3-coder-next": 0.5,
678
+ "qwen/qwen3-next-80b-a3b-instruct": 0.5,
679
+ "qwen/qwen3.5-flash-02-23": 0.1,
680
+ "qwen/qwen3.6-35b-a3b": 0.1,
681
+ "qwen/qwen3.7-plus": 0.2,
682
+ "qwen/qwen3.8-max": 0.125,
683
+ "xiaomi/mimo-v2.5-pro": 0.1,
684
+ "z-ai/glm-5": 0.2,
685
+ "z-ai/glm-5.1": 0.18571428571428572,
686
+ "z-ai/glm-5.2": 0.18571428571428572,
687
+ "z-ai/glm-5.3": 0.18571428571428572,
688
+ "z-ai/glm-5.3-flash": 0.2
445
689
  }
446
690
  }'
447
691
  # END_GENERATED_PRICES
@@ -496,6 +740,12 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
496
740
  # * unrecognized model → "… isn't a recognized model · keeping
497
741
  # automatic routing" — a NO-OP: the prior
498
742
  # pin, if any, is left untouched
743
+ # * listing (historical) → "… pick a model by id …" / "no models are
744
+ # available to pin …" — also NO-OPs. The
745
+ # router no longer emits these, but a
746
+ # transcript written while it did still
747
+ # carries them, and they must not read as
748
+ # a clear or a live pin loses its [forced]
499
749
  # These persist on disk (the ingress stripper only scrubs them from upstream
500
750
  # requests). Classify each weave-router turn newest-first, skip the no-op
501
751
  # "rejected" acks, and let the latest real state change decide: an "applied"
@@ -509,6 +759,7 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
509
759
  | ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
510
760
  | if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
511
761
  elif ($t | test("isn.t a recognized model")) then "REJECTED"
762
+ elif ($t | test("pick a model by id|no models are available to pin")) then "REJECTED"
512
763
  else "CLEARED" end' 2>/dev/null \
513
764
  | grep -m1 -v '^REJECTED$' || true)"
514
765
  if [[ "$force_state" == APPLIED\ * ]]; then
@@ -519,12 +770,9 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
519
770
  # Compute a session running total: savings across every assistant turn
520
771
  # whose marker reports a requested ≠ routed swap, plus cumulative token
521
772
  # counts across every assistant turn (rerouted or not — total work the
522
- # session has done). cache_creation is priced at 1.25× input, cache_read
523
- # at 0.1× both ratios are stable across the Claude family and a no-op
524
- # when the provider doesn't return those fields. Cache reads ARE included
525
- # in the savings comparison: both costs apply the same 0.1× weight to
526
- # cache_read_input_tokens, so the delta reflects the model-price
527
- # difference on the cached portion as well.
773
+ # session has done). cache_creation is priced at 1.25× input; cache_read
774
+ # uses each model's generated catalog multiplier. Both are no-ops when the
775
+ # provider does not return those fields.
528
776
  #
529
777
  # The marker regex tolerates the optional "(<provider>)" segment and a
530
778
  # `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
@@ -557,14 +805,17 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
557
805
  } as $t |
558
806
  (if $requested == "" or $requested == $rm then 0
559
807
  else
560
- ($p.input[$rm] // null) as $rin | ($p.output[$rm] // null) as $rout |
561
- ($p.input[$requested] // null) as $sin | ($p.output[$requested] // null) as $sout |
562
- if ($rin == null or $rout == null or $sin == null or $sout == null) then 0
808
+ ($p.input[$rm] // null) as $rin | ($p.output[$rm] // null) as $rout |
809
+ ($p.cache_read[$rm] // null) as $rcr |
810
+ ($p.input[$requested] // null) as $sin | ($p.output[$requested] // null) as $sout |
811
+ ($p.cache_read[$requested] // null) as $scr |
812
+ if ($rin == null or $rout == null or $rcr == null or $sin == null or $sout == null or $scr == null) then 0
563
813
  else
564
- (($t.in + 1.25 * $t.cwrt + 0.1 * $t.crd) / 1000) as $input_units |
565
- ($t.out / 1000) as $output_units |
566
- ($input_units * $rin + $output_units * $rout) as $routed_cost |
567
- ($input_units * $sin + $output_units * $sout) as $requested_cost |
814
+ (($t.in + 1.25 * $t.cwrt + $rcr * $t.crd) / 1000) as $routed_input_units |
815
+ (($t.in + 1.25 * $t.cwrt + $scr * $t.crd) / 1000) as $requested_input_units |
816
+ ($t.out / 1000) as $output_units |
817
+ ($routed_input_units * $rin + $output_units * $rout) as $routed_cost |
818
+ ($requested_input_units * $sin + $output_units * $sout) as $requested_cost |
568
819
  ($requested_cost - $routed_cost)
569
820
  end
570
821
  end) as $savings |
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: fm
3
+ description: "Alias for force-model — pin this Codex session to a specific model through the Weave Router."
4
+ ---
5
+
6
+ <!-- weave-router managed fm skill -->
7
+
8
+ When the user invokes `$fm <model-id>`, run this skill's `scripts/emit.sh` using the
9
+ absolute path shown in this skill's `<path>` (its directory + `/scripts/emit.sh`).
10
+ Pass the model id as arguments, quoted as a single argument list.
11
+ Do not send a user message and do not type a Codex slash command.
12
+
13
+ The script prints a leading-space `/force-model` line. The Weave Router reads that
14
+ exec output, pins the session, and continues this turn — so run the script once and
15
+ do not repeat its output as your answer. Then report the router's response.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # Alias of force-model/scripts/emit.sh
3
+ set -euo pipefail
4
+ if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
5
+ echo "usage: emit.sh <model-id>" >&2
6
+ exit 1
7
+ fi
8
+ printf ' /force-model %s\n' "$*"
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: force-model
3
+ description: "Pin this Codex session to a specific model through the Weave Router."
4
+ ---
5
+
6
+ <!-- weave-router managed force-model skill -->
7
+
8
+ When the user invokes `$force-model <model-id>` (or `$fm`), run this skill's `scripts/emit.sh` using the
9
+ absolute path shown in this skill's `<path>` (its directory + `/scripts/emit.sh`).
10
+ Pass the model id as arguments, quoted as a single argument list.
11
+ Do not send a user message and do not type a Codex slash command.
12
+
13
+ The script prints a leading-space `/force-model` line. The Weave Router reads that
14
+ exec output, pins the session, and continues this turn — so run the script once and
15
+ do not repeat its output as your answer. Then report the router's response.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Print a leading-space /force-model directive. Codex runs this via exec;
3
+ # the Weave Router intercepts the tool output and pins the session.
4
+ set -euo pipefail
5
+ if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
6
+ echo "usage: emit.sh <model-id>" >&2
7
+ exit 1
8
+ fi
9
+ printf ' /force-model %s\n' "$*"
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: rf
3
+ description: "Alias for router-feedback — submit feedback about a Weave Router decision or model performance."
4
+ ---
5
+
6
+ <!-- weave-router managed rf skill -->
7
+
8
+ When the user invokes `$rf <feedback>`, run this skill's `scripts/emit.sh` using the
9
+ absolute path shown in this skill's `<path>` (its directory + `/scripts/emit.sh`).
10
+ Pass the feedback text as arguments, quoted as a single argument list.
11
+ Do not send a user message and do not type a Codex slash command.
12
+
13
+ The script prints a leading-space `/router-feedback` line. The Weave Router reads that
14
+ exec output, records the feedback, and continues this turn — so run the script once and
15
+ do not repeat its output as your answer. Then report the router's response.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
4
+ echo "usage: emit.sh <feedback>" >&2
5
+ exit 1
6
+ fi
7
+ printf ' /router-feedback %s\n' "$*"
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: router-feedback
3
+ description: "Submit feedback about a Weave Router decision or model performance."
4
+ ---
5
+
6
+ <!-- weave-router managed router-feedback skill -->
7
+
8
+ When the user invokes `$router-feedback <feedback>` (or `$rf`), run this skill's `scripts/emit.sh` using the
9
+ absolute path shown in this skill's `<path>` (its directory + `/scripts/emit.sh`).
10
+ Pass the feedback text as arguments, quoted as a single argument list.
11
+ Do not send a user message and do not type a Codex slash command.
12
+
13
+ The script prints a leading-space `/router-feedback` line. The Weave Router reads that
14
+ exec output, records the feedback, and continues this turn — so run the script once and
15
+ do not repeat its output as your answer. Then report the router's response.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Print a leading-space /router-feedback directive. Codex runs this via exec;
3
+ # the Weave Router intercepts the tool output and records the feedback.
4
+ set -euo pipefail
5
+ if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
6
+ echo "usage: emit.sh <feedback>" >&2
7
+ exit 1
8
+ fi
9
+ printf ' /router-feedback %s\n' "$*"