@workweave/router 0.2.9 → 0.2.11

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/README.md CHANGED
@@ -14,6 +14,22 @@ npx @workweave/router --base-url https://router.acme.internal
14
14
  npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompts (defaults to claude)
15
15
  ```
16
16
 
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):
20
+
21
+ ```bash
22
+ npx @workweave/router --claude # reuses the installed key
23
+ npx @workweave/router --claude --rotate-key # ignore it and prompt for a new one
24
+ npx @workweave/router update --claude # non-interactive refresh in place
25
+ ```
26
+
27
+ For Claude Code the installed statusline and `/force-model`, `/router-*` slash
28
+ commands also refresh themselves in the background about once a week (never
29
+ 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
+
17
33
  Version-pin for reproducible setups:
18
34
 
19
35
  ```bash
@@ -38,6 +54,19 @@ slash commands. The shell equivalent is `npx @workweave/router disable-routing`.
38
54
  Cursor has no config file we own — toggle its base URL override in **Settings →
39
55
  Models** instead.
40
56
 
57
+ Pick which models the router is allowed to route to:
58
+
59
+ ```bash
60
+ npx @workweave/router models --claude # list every model, with its on/off state
61
+ npx @workweave/router models disable gpt-5.6 --claude # take one out of rotation
62
+ npx @workweave/router models enable gpt-5.6 --claude # put it back
63
+ ```
64
+
65
+ Inside Claude Code that's `/router-models` (alias `/models`). Editing needs a
66
+ router that serves the model-selection API; against the Weave-hosted router the
67
+ list still prints and points you at the dashboard, where model selection is an
68
+ organization-wide setting.
69
+
41
70
  Uninstall:
42
71
 
43
72
  ```bash
@@ -66,9 +95,8 @@ Four install targets:
66
95
  - **Codex** (`--codex`) — patches `~/.codex/config.toml` (or
67
96
  `<repo>/.codex/config.toml`) with a managed `[model_providers.weave]`
68
97
  block plus `model_provider = "weave"`. The provider preserves the existing
69
- ChatGPT OAuth login. The public hosted endpoint sends
70
- `X-Weave-Router-Strategy: hmm`; `--local` and custom self-hosted URLs keep
71
- their router's configured default because its HMM sidecar is optional. HMM or forced
98
+ ChatGPT OAuth login. No install pins `X-Weave-Router-Strategy`; every
99
+ endpoint keeps its router's configured default. HMM or forced
72
100
  `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` turns use that plan;
73
101
  every other selected model uses its WorkWeave deployment or BYOK credential.
74
102
  The block lives between begin/end markers
package/cc-statusline.sh CHANGED
@@ -125,6 +125,312 @@ weave_self_refresh() {
125
125
  }
126
126
  weave_self_refresh 2>/dev/null || true
127
127
 
128
+ # ---------- background slash-command refresh ----------
129
+ #
130
+ # The /force-model, /router-* … wrappers under <install>/.claude/commands/ are
131
+ # written once at install time and then never touched again, so a user who
132
+ # doesn't re-run the installer keeps whatever set shipped that day. This script
133
+ # is the only thing Claude Code invokes on every turn, so it doubles as the
134
+ # refresh point for them: same rate limit, same detached fork, same
135
+ # content-diff no-op as the self-refresh above.
136
+ #
137
+ # Never clobber a wrapper the user edited. The only way to tell an edit from a
138
+ # stale copy is to remember what we last downloaded, so each canonical file is
139
+ # cached (unrendered) per install under the user cache dir and a wrapper is
140
+ # replaced only when its bytes still match that baseline. With no baseline yet
141
+ # we can't prove anything, so the first run only seeds the cache and a swap can
142
+ # happen from the next one on. install.sh seeds it too, so fresh installs skip
143
+ # that warm-up round.
144
+ #
145
+ # Never touch a wrapper git tracks either. A project-scope install writes the
146
+ # wrappers into the repo's own .claude/commands/, and unlike cc-statusline.sh
147
+ # the installer does not gitignore them — so an unattended weekly rewrite would
148
+ # surface as unexplained dirty files (and could ride along in someone's commit).
149
+ # Only the installer changes tracked files, and only when a human runs it.
150
+ #
151
+ # Opt out with WEAVE_COMMANDS_UPDATE=0 (WEAVE_STATUSLINE_UPDATE=0 disables this
152
+ # too, along with every other network path here). Override the source with
153
+ # WEAVE_COMMANDS_URL_BASE=..., e.g. for self-hosters who fork.
154
+
155
+ # weave_command_tracked_by_git returns 0 when $1 is a file git tracks. Used to
156
+ # leave repo-committed wrappers alone; no git (or no repo) means untracked.
157
+ weave_command_tracked_by_git() {
158
+ command -v git >/dev/null 2>&1 || return 1
159
+ git -C "$(dirname "$1")" ls-files --error-unmatch -- "$1" >/dev/null 2>&1
160
+ }
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
+
179
+ # weave_render_command prints $1 with the installer's {{SCOPE}} placeholder
180
+ # replaced by $2, matching how install_slash_commands writes the same file.
181
+ # Trailing newlines are stripped on both sides of every comparison below.
182
+ weave_render_command() {
183
+ local body
184
+ body="$(cat "$1" 2>/dev/null)" || return 1
185
+ printf '%s' "${body//\{\{SCOPE\}\}/$2}"
186
+ }
187
+
188
+ weave_sync_commands() {
189
+ [ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
190
+ [ "${WEAVE_COMMANDS_UPDATE:-1}" = "0" ] && return 0
191
+ command -v curl >/dev/null 2>&1 || return 0
192
+
193
+ local self="${BASH_SOURCE[0]:-$0}" self_dir cmd_dir
194
+ self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd -P)" || return 0
195
+ # User scope installs this script at <base>/.weave/, project and --dir at
196
+ # <base>/.claude/. Claude Code reads commands from <base>/.claude/commands in
197
+ # both layouts.
198
+ case "${self_dir##*/}" in
199
+ .weave) cmd_dir="${self_dir%/*}/.claude/commands" ;;
200
+ .claude) cmd_dir="$self_dir/commands" ;;
201
+ *) return 0 ;;
202
+ esac
203
+ [ -d "$cmd_dir" ] && [ -w "$cmd_dir" ] || return 0
204
+
205
+ local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
206
+ local dir_slug
207
+ dir_slug="$(printf '%s' "$cmd_dir" | tr -c 'A-Za-z0-9._-' '_')"
208
+ # Baseline is keyed by install, not shared: two installs refresh on their own
209
+ # clocks, and a shared baseline updated by one would make the other's still-
210
+ # canonical wrappers look user-edited and freeze them forever.
211
+ local baseline_dir="$cache_dir/commands${dir_slug}"
212
+ local stamp="$cache_dir/checked-at${dir_slug}.commands"
213
+ mkdir -p "$baseline_dir" 2>/dev/null || return 0
214
+
215
+ local interval_days="${WEAVE_STATUSLINE_UPDATE_INTERVAL_DAYS:-7}"
216
+ local now stamp_mtime
217
+ now="$(date +%s 2>/dev/null)" || return 0
218
+ if [ -f "$stamp" ]; then
219
+ stamp_mtime="$(stat -c %Y "$stamp" 2>/dev/null || stat -f %m "$stamp" 2>/dev/null)" || stamp_mtime=0
220
+ else
221
+ stamp_mtime=0
222
+ fi
223
+ if [ -n "${stamp_mtime:-}" ] && [ "$stamp_mtime" -gt 0 ] \
224
+ && [ $(( now - stamp_mtime )) -lt $(( interval_days * 86400 )) ]; then
225
+ return 0
226
+ fi
227
+ # Stamp before forking, same as the self-refresh: Claude Code calls us on
228
+ # every turn and concurrent invocations must not all start downloading.
229
+ : > "$stamp" 2>/dev/null || return 0
230
+
231
+ # router-off/on/status bake this install's scope selector into their npx
232
+ # line (upstream carries {{SCOPE}} there). Recover it from the installed copy
233
+ # so a project or --dir install keeps toggling its own config; when it can't
234
+ # be recovered those three are skipped rather than rewritten to point at the
235
+ # user-scope install.
236
+ local scope_args="" scope_known="false" off="$cmd_dir/router-off.md"
237
+ if [ -f "$off" ] && grep -q '^`npx @workweave/router off --claude.*`$' "$off" 2>/dev/null; then
238
+ scope_known="true"
239
+ scope_args="$(sed -n 's|^`npx @workweave/router off --claude\(.*\)`$|\1|p' "$off" | head -n 1)"
240
+ fi
241
+
242
+ local url_base="${WEAVE_COMMANDS_URL_BASE:-https://raw.githubusercontent.com/workweave/router/main/install/commands}"
243
+ local name installed raw prev tmp new_body prev_body installed_body
244
+ (
245
+ # Detach stdin (CC pipes JSON to us) so curl can't consume it, and silence
246
+ # everything so no output leaks into the statusline.
247
+ exec </dev/null
248
+ while IFS= read -r name; do
249
+ installed="$cmd_dir/$name.md"
250
+ # Only ever refresh a wrapper that is already installed: a missing one
251
+ # was uninstalled or deliberately deleted, and resurrecting it would be
252
+ # a surprise. A symlink is user-owned; leave it alone. A git-tracked
253
+ # wrapper belongs to the repo — rewriting it would dirty a working tree
254
+ # nobody asked us to touch.
255
+ [ -f "$installed" ] || continue
256
+ [ -L "$installed" ] && continue
257
+ weave_command_tracked_by_git "$installed" && continue
258
+
259
+ raw="$baseline_dir/$name.md.tmp.$$"
260
+ curl -fsSL --max-time 15 "$url_base/$name.md" -o "$raw" 2>/dev/null || { rm -f "$raw"; continue; }
261
+ # Shape check: every wrapper opens with YAML front matter, so a 404 page
262
+ # or a truncated body can never be installed as a command.
263
+ if [ ! -s "$raw" ] || [ "$(head -n 1 "$raw")" != "---" ]; then
264
+ rm -f "$raw"
265
+ continue
266
+ fi
267
+
268
+ prev="$baseline_dir/$name.md"
269
+ if grep -q '{{SCOPE}}' "$raw" && [ "$scope_known" != "true" ]; then
270
+ mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
271
+ continue
272
+ fi
273
+
274
+ if [ -f "$prev" ]; then
275
+ new_body="$(weave_render_command "$raw" "$scope_args")"
276
+ prev_body="$(weave_render_command "$prev" "$scope_args")"
277
+ installed_body="$(cat "$installed" 2>/dev/null | sed '/^<!-- weave-router managed command: .* -->$/d')" || installed_body=""
278
+ if [ "$prev_body" = "$installed_body" ] && [ "$new_body" != "$installed_body" ]; then
279
+ tmp="$installed.tmp.$$"
280
+ if printf '%s\n<!-- weave-router managed command: %s -->' "$new_body" "$name" >"$tmp" 2>/dev/null; then
281
+ mv "$tmp" "$installed" 2>/dev/null || rm -f "$tmp"
282
+ else
283
+ rm -f "$tmp"
284
+ fi
285
+ fi
286
+ fi
287
+ mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
288
+ done <<EOF
289
+ $(weave_installed_command_names "$cmd_dir")
290
+ EOF
291
+ ) >/dev/null 2>&1 &
292
+ disown 2>/dev/null || true
293
+ return 0
294
+ }
295
+ weave_sync_commands 2>/dev/null || true
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
+
128
434
  input="$(cat)"
129
435
  transcript_path="$(printf '%s' "$input" | jq -r '.transcript_path // empty')"
130
436
  # Prefer model.id over display_name: pricing keys + the routed model id in
@@ -165,6 +471,7 @@ prices='{
165
471
  "claude-sonnet-5": 0.003,
166
472
  "deepseek/deepseek-v4-flash": 0.0001134,
167
473
  "deepseek/deepseek-v4-pro": 0.00174,
474
+ "deepseek/deepseek-v4-pro-0813": 0.00174,
168
475
  "gemini-2.0-flash": 0.0001,
169
476
  "gemini-2.0-flash-lite": 0.000075,
170
477
  "gemini-2.5-flash": 0.0003,
@@ -177,6 +484,7 @@ prices='{
177
484
  "gemini-3.5-flash": 0.0015,
178
485
  "gemini-3.5-flash-lite": 0.0003,
179
486
  "gemini-3.6-flash": 0.0015,
487
+ "gemini-3.7-flash": 0.0015,
180
488
  "google/gemma-4-26b-a4b-it": 0.00015,
181
489
  "gpt-4.1": 0.002,
182
490
  "gpt-4.1-mini": 0.0004,
@@ -196,7 +504,9 @@ prices='{
196
504
  "gpt-5.5-nano": 0.00015,
197
505
  "gpt-5.5-pro": 0.03,
198
506
  "gpt-5.6-luna": 0.001,
507
+ "gpt-5.6-luna-pro": 0.001,
199
508
  "gpt-5.6-sol": 0.005,
509
+ "gpt-5.6-sol-pro": 0.005,
200
510
  "gpt-5.6-terra": 0.0025,
201
511
  "grok-4.5": 0.002,
202
512
  "grok-4.6": 0.002,
@@ -219,7 +529,8 @@ prices='{
219
529
  "xiaomi/mimo-v2.5-pro": 0.001,
220
530
  "z-ai/glm-5": 0.001,
221
531
  "z-ai/glm-5.1": 0.0014,
222
- "z-ai/glm-5.2": 0.0014
532
+ "z-ai/glm-5.2": 0.0014,
533
+ "z-ai/glm-5.3-flash": 0.00015
223
534
  },
224
535
  "output": {
225
536
  "claude-fable-5": 0.05,
@@ -236,6 +547,7 @@ prices='{
236
547
  "claude-sonnet-5": 0.015,
237
548
  "deepseek/deepseek-v4-flash": 0.0002791,
238
549
  "deepseek/deepseek-v4-pro": 0.00348,
550
+ "deepseek/deepseek-v4-pro-0813": 0.00348,
239
551
  "gemini-2.0-flash": 0.0004,
240
552
  "gemini-2.0-flash-lite": 0.0003,
241
553
  "gemini-2.5-flash": 0.0012,
@@ -248,6 +560,7 @@ prices='{
248
560
  "gemini-3.5-flash": 0.009,
249
561
  "gemini-3.5-flash-lite": 0.0025,
250
562
  "gemini-3.6-flash": 0.0075,
563
+ "gemini-3.7-flash": 0.0075,
251
564
  "google/gemma-4-26b-a4b-it": 0.0006,
252
565
  "gpt-4.1": 0.008,
253
566
  "gpt-4.1-mini": 0.0016,
@@ -267,7 +580,9 @@ prices='{
267
580
  "gpt-5.5-nano": 0.0006,
268
581
  "gpt-5.5-pro": 0.18,
269
582
  "gpt-5.6-luna": 0.006,
583
+ "gpt-5.6-luna-pro": 0.006,
270
584
  "gpt-5.6-sol": 0.03,
585
+ "gpt-5.6-sol-pro": 0.03,
271
586
  "gpt-5.6-terra": 0.015,
272
587
  "grok-4.5": 0.006,
273
588
  "grok-4.6": 0.006,
@@ -290,7 +605,8 @@ prices='{
290
605
  "xiaomi/mimo-v2.5-pro": 0.003,
291
606
  "z-ai/glm-5": 0.0032,
292
607
  "z-ai/glm-5.1": 0.0044,
293
- "z-ai/glm-5.2": 0.0044
608
+ "z-ai/glm-5.2": 0.0044,
609
+ "z-ai/glm-5.3-flash": 0.0005
294
610
  }
295
611
  }'
296
612
  # END_GENERATED_PRICES
@@ -345,6 +661,12 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
345
661
  # * unrecognized model → "… isn't a recognized model · keeping
346
662
  # automatic routing" — a NO-OP: the prior
347
663
  # pin, if any, is left untouched
664
+ # * listing (historical) → "… pick a model by id …" / "no models are
665
+ # available to pin …" — also NO-OPs. The
666
+ # router no longer emits these, but a
667
+ # transcript written while it did still
668
+ # carries them, and they must not read as
669
+ # a clear or a live pin loses its [forced]
348
670
  # These persist on disk (the ingress stripper only scrubs them from upstream
349
671
  # requests). Classify each weave-router turn newest-first, skip the no-op
350
672
  # "rejected" acks, and let the latest real state change decide: an "applied"
@@ -358,6 +680,7 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
358
680
  | ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
359
681
  | if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
360
682
  elif ($t | test("isn.t a recognized model")) then "REJECTED"
683
+ elif ($t | test("pick a model by id|no models are available to pin")) then "REJECTED"
361
684
  else "CLEARED" end' 2>/dev/null \
362
685
  | grep -m1 -v '^REJECTED$' || true)"
363
686
  if [[ "$force_state" == APPLIED\ * ]]; then
@@ -0,0 +1,14 @@
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 asks to use the `$fm` alias), send a normal user message whose first character is one literal space, followed by `/force-model ` and the requested model id. Do not use a Codex slash command and do not omit the leading space. For example, send exactly:
9
+
10
+ ```text
11
+ /force-model gpt-5.6-terra
12
+ ```
13
+
14
+ Preserve any model id exactly as provided. Report the router's response after it returns.
@@ -0,0 +1,14 @@
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 asks to use the `$rf` alias), send a normal user message whose first character is one literal space, followed by `/router-feedback ` and the feedback text. Do not use a Codex slash command and do not omit the leading space. Preserve the feedback text exactly. For example:
9
+
10
+ ```text
11
+ /router-feedback the selected model struggled with this task
12
+ ```
13
+
14
+ Report the router's response after it returns.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: unforce-model
3
+ description: "Clear the active Weave Router model pin for this Codex session."
4
+ ---
5
+
6
+ <!-- weave-router managed unforce-model skill -->
7
+
8
+ When the user invokes `$unforce-model` (or asks to use the `$ufm` alias), send a normal user message whose first character is one literal space, followed by `/unforce-model`. Do not use a Codex slash command and do not omit the leading space. Send exactly:
9
+
10
+ ```text
11
+ /unforce-model
12
+ ```
13
+
14
+ Report the router's response after it returns.
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Alias for /router-models — list the models the Weave Router may route to, and turn them on or off.
3
+ argument-hint: [model or provider to enable/disable]
4
+ allowed-tools: Bash(npx:*)
5
+ ---
6
+
7
+ Show me which models this installation lets the Weave Router pick from, and
8
+ change that selection when I ask. This is the same list — and the same stored
9
+ setting — as the checkboxes on the router dashboard's settings page.
10
+
11
+ Start by running:
12
+
13
+ `npx @workweave/router models --claude{{SCOPE}}`
14
+
15
+ That prints every deployed model grouped by provider, with `[x]` for models the
16
+ router may pick and `[ ]` for models it may not. Present it back to me as a
17
+ compact checklist in that same `[x]` / `[ ]` form, keeping the provider
18
+ grouping and the exact model ids — I select models by id.
19
+
20
+ Then:
21
+
22
+ - If I named models or providers in `$ARGUMENTS`, work out whether I want them
23
+ on or off from how I phrased it, and apply it with
24
+ `npx @workweave/router models enable <id>... --claude{{SCOPE}}` or
25
+ `npx @workweave/router models disable <id>... --claude{{SCOPE}}` (add
26
+ `providers` before `enable`/`disable` to switch a whole provider). Several
27
+ ids can go in one call. Then re-run the list and show me the result.
28
+ - If I named nothing, stop after the list and ask which ones I want to change.
29
+ Don't change anything I didn't ask for.
30
+
31
+ Other things I might ask for:
32
+
33
+ - Rank models by preference:
34
+ `npx @workweave/router models prefer <id> <id>... --claude{{SCOPE}}` (order
35
+ matters), or `npx @workweave/router models prefer clear --claude{{SCOPE}}` to
36
+ drop the ranking.
37
+ - Providers only: `npx @workweave/router models providers --claude{{SCOPE}}`.
38
+
39
+ If the command reports that this router doesn't expose model selection, that's
40
+ a Weave-hosted router: model selection belongs to the whole organization there,
41
+ so tell me to change it at https://router.workweave.ai/dashboard/settings —
42
+ don't try to work around it. Its listing carries no on/off state, so present it
43
+ as a plain list; don't infer which models are enabled.
44
+
45
+ Disabling a model takes effect on the router's next routing decision; no
46
+ restart is needed.
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: List the models the Weave Router may route to, and turn them on or off.
3
+ argument-hint: [model or provider to enable/disable]
4
+ allowed-tools: Bash(npx:*)
5
+ ---
6
+
7
+ Show me which models this installation lets the Weave Router pick from, and
8
+ change that selection when I ask. This is the same list — and the same stored
9
+ setting — as the checkboxes on the router dashboard's settings page.
10
+
11
+ Start by running:
12
+
13
+ `npx @workweave/router models --claude{{SCOPE}}`
14
+
15
+ That prints every deployed model grouped by provider, with `[x]` for models the
16
+ router may pick and `[ ]` for models it may not. Present it back to me as a
17
+ compact checklist in that same `[x]` / `[ ]` form, keeping the provider
18
+ grouping and the exact model ids — I select models by id.
19
+
20
+ Then:
21
+
22
+ - If I named models or providers in `$ARGUMENTS`, work out whether I want them
23
+ on or off from how I phrased it, and apply it with
24
+ `npx @workweave/router models enable <id>... --claude{{SCOPE}}` or
25
+ `npx @workweave/router models disable <id>... --claude{{SCOPE}}` (add
26
+ `providers` before `enable`/`disable` to switch a whole provider). Several
27
+ ids can go in one call. Then re-run the list and show me the result.
28
+ - If I named nothing, stop after the list and ask which ones I want to change.
29
+ Don't change anything I didn't ask for.
30
+
31
+ Other things I might ask for:
32
+
33
+ - Rank models by preference:
34
+ `npx @workweave/router models prefer <id> <id>... --claude{{SCOPE}}` (order
35
+ matters), or `npx @workweave/router models prefer clear --claude{{SCOPE}}` to
36
+ drop the ranking.
37
+ - Providers only: `npx @workweave/router models providers --claude{{SCOPE}}`.
38
+
39
+ If the command reports that this router doesn't expose model selection, that's
40
+ a Weave-hosted router: model selection belongs to the whole organization there,
41
+ so tell me to change it at https://router.workweave.ai/dashboard/settings —
42
+ don't try to work around it. Its listing carries no on/off state, so present it
43
+ as a plain list; don't infer which models are enabled.
44
+
45
+ Disabling a model takes effect on the router's next routing decision; no
46
+ restart is needed.
package/directives.tsv ADDED
@@ -0,0 +1,12 @@
1
+ # Weave Router directive registry
2
+ # canonical|aliases|capability|claude|codex|opencode|pi|cursor|adapter
3
+ # aliases are comma-separated; client columns are yes/no. adapter is the native asset kind.
4
+ force-model|fm|prompt|yes|yes|yes|yes|manual|command,skill
5
+ unforce-model|ufm|prompt|yes|yes|yes|yes|manual|command,skill
6
+ router-feedback|rf|prompt|yes|yes|yes|no|manual|command,skill
7
+ router-off||local-toggle|yes|no|no|no|manual|command
8
+ router-on||local-toggle|yes|no|no|no|manual|command
9
+ router-status||local-toggle|yes|no|no|no|manual|command
10
+ router-session||prompt|yes|no|no|no|manual|command
11
+ router-models|models|local-toggle|yes|no|no|no|manual|command
12
+ disable-routing||local-toggle|no|yes|no|no|manual|skill