@workweave/router 0.2.5 → 0.2.7
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 +28 -8
- package/cc-statusline.sh +76 -2
- package/commands/router-session.md +15 -0
- package/install.sh +148 -40
- package/opencode-weave/README.md +3 -3
- package/package.json +3 -2
- package/pi-router/README.md +23 -6
- package/pi-router/src/config.ts +2 -0
- package/pi-router/src/force-model.ts +126 -0
- package/pi-router/src/index.ts +3 -1
- package/pi-router/src/pricing.generated.ts +79 -0
- package/pi-router/src/routed-model.ts +151 -17
- package/pi-router/src/savings.ts +191 -0
- package/pi-router/src/ui.ts +80 -0
- package/pi-router/src/wooly.ts +408 -0
- package/uninstall.sh +39 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @workweave/router
|
|
2
2
|
|
|
3
|
-
One command, anywhere, to point Claude Code, Codex, or
|
|
3
|
+
One command, anywhere, to point Claude Code, Codex, opencode, or pi at the Weave Router.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx @workweave/router # interactive: pick Claude Code / Codex / opencode, then scope
|
|
6
|
+
npx @workweave/router # interactive: pick Claude Code / Codex / opencode / pi, then scope
|
|
7
7
|
npx @workweave/router --claude # skip the picker, target Claude Code
|
|
8
8
|
npx @workweave/router --codex # skip the picker, target the OpenAI Codex CLI
|
|
9
9
|
npx @workweave/router --opencode # skip the picker, target opencode
|
|
10
|
+
npx @workweave/router --pi # skip the picker, target pi + Loom UI
|
|
10
11
|
npx @workweave/router --scope project # per-repo install, commit settings.json (or .codex/ / opencode.json)
|
|
11
12
|
npx @workweave/router --local # self-hosted via docker-compose (localhost:8080)
|
|
12
13
|
npx @workweave/router --base-url https://router.acme.internal
|
|
@@ -40,6 +41,7 @@ Uninstall:
|
|
|
40
41
|
npx @workweave/router --uninstall # Claude Code, user scope
|
|
41
42
|
npx @workweave/router --uninstall --codex # Codex, user scope
|
|
42
43
|
npx @workweave/router --uninstall --opencode # opencode, user scope
|
|
44
|
+
npx @workweave/router --uninstall --pi # pi, user scope
|
|
43
45
|
npx @workweave/router --uninstall --scope project # Claude Code, inside the repo
|
|
44
46
|
npx @workweave/router --uninstall --codex --scope project
|
|
45
47
|
```
|
|
@@ -52,7 +54,7 @@ Node ≥ 18 — no `curl | sh`, no Git clone, no PATH fiddling. Everything the
|
|
|
52
54
|
shell installer documents (targets, scopes, flags, environment variables)
|
|
53
55
|
works identically here.
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
Four install targets:
|
|
56
58
|
|
|
57
59
|
- **Claude Code** (default) — patches `~/.claude/settings.json` (or
|
|
58
60
|
`<repo>/.claude/settings.json` with `--scope project`) so `claude` routes
|
|
@@ -71,6 +73,12 @@ Three install targets:
|
|
|
71
73
|
natively, so opencode talks to it unmodified. Re-install rewrites only
|
|
72
74
|
the managed `provider.weave` block; `--uninstall --opencode` strips it
|
|
73
75
|
and leaves your other providers and settings alone.
|
|
76
|
+
- **pi** (`--pi`) — registers the `weave` provider and installs this package as
|
|
77
|
+
a pi extension. Stock pi then gets the Loom startup header, Wooly's animated
|
|
78
|
+
mascot, the persistent actual-route display, cumulative session savings,
|
|
79
|
+
`/fm` + `/ufm` model-pin commands with a `[forced]` status, and the
|
|
80
|
+
context-isolated `dispatch` tool. There is no forked pi binary and no separate
|
|
81
|
+
Loom runtime.
|
|
74
82
|
|
|
75
83
|
See the [main installer docs](https://github.com/workweave/router/tree/main/install)
|
|
76
84
|
for the full reference.
|
|
@@ -79,11 +87,23 @@ for the full reference.
|
|
|
79
87
|
|
|
80
88
|
- Node ≥ 18 (ships with `npx`)
|
|
81
89
|
- `bash` on PATH (macOS / Linux native; Windows needs Git Bash or WSL)
|
|
82
|
-
- `jq` on PATH — used by the Claude Code status line
|
|
83
|
-
|
|
90
|
+
- `jq` on PATH — used by the Claude Code status line and the opencode/pi JSON
|
|
91
|
+
merges. Not required for the Codex path.
|
|
84
92
|
|
|
85
93
|
## Why npx
|
|
86
94
|
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
`npx @workweave/router` gives Windows support via Git Bash, painless version
|
|
96
|
+
pinning, and discoverability via the npm registry.
|
|
97
|
+
|
|
98
|
+
## Older npm
|
|
99
|
+
|
|
100
|
+
On npm ≤ 6 the bundled `npx` treats an undeclared `-y` as consuming the next
|
|
101
|
+
token, so `npx -y @workweave/router --claude` silently drops the package name
|
|
102
|
+
and resolves the following argument as the command instead. Either upgrade
|
|
103
|
+
(`npm i -g npm@latest`) or name the binary explicitly:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx --package @workweave/router -y -- weave-router --claude
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
That form is correct on every npm version.
|
package/cc-statusline.sh
CHANGED
|
@@ -33,10 +33,21 @@ set -euo pipefail
|
|
|
33
33
|
# lives in their per-user cache dir, and on no-content-change days we skip
|
|
34
34
|
# the mv entirely so the repo working tree stays clean. When upstream does
|
|
35
35
|
# change, the first teammate's commit propagates the new version to the rest.
|
|
36
|
+
# A pricing-table miss also triggers a refresh off-schedule — see
|
|
37
|
+
# weave_refresh_on_price_miss below.
|
|
36
38
|
#
|
|
37
39
|
# Opt out entirely with `export WEAVE_STATUSLINE_UPDATE=0`. Override the
|
|
38
40
|
# source with `WEAVE_STATUSLINE_URL=...`, e.g. for self-hosters who fork.
|
|
41
|
+
#
|
|
42
|
+
# $1 is an optional stamp suffix, so a caller refreshing for a reason other than
|
|
43
|
+
# "the interval elapsed" rate-limits on its own clock rather than sharing the
|
|
44
|
+
# periodic check's budget. $2=1 drops the stamp again if the download fails, for
|
|
45
|
+
# callers recovering from a known-bad state where waiting out the full interval
|
|
46
|
+
# on a transient network error is worse than retrying next turn.
|
|
39
47
|
weave_self_refresh() {
|
|
48
|
+
local stamp_suffix="${1:-}"
|
|
49
|
+
local retry_on_fail="${2:-0}"
|
|
50
|
+
|
|
40
51
|
[ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
|
|
41
52
|
command -v curl >/dev/null 2>&1 || return 0
|
|
42
53
|
|
|
@@ -53,7 +64,7 @@ weave_self_refresh() {
|
|
|
53
64
|
mkdir -p "$cache_dir" 2>/dev/null || return 0
|
|
54
65
|
local script_slug
|
|
55
66
|
script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
|
|
56
|
-
local stamp="$cache_dir/checked-at${script_slug}"
|
|
67
|
+
local stamp="$cache_dir/checked-at${script_slug}${stamp_suffix}"
|
|
57
68
|
|
|
58
69
|
local now stamp_mtime
|
|
59
70
|
now="$(date +%s 2>/dev/null)" || return 0
|
|
@@ -77,7 +88,13 @@ weave_self_refresh() {
|
|
|
77
88
|
: > "$stamp" 2>/dev/null || return 0
|
|
78
89
|
|
|
79
90
|
local url="${WEAVE_STATUSLINE_URL:-https://raw.githubusercontent.com/workweave/router/main/install/cc-statusline.sh}"
|
|
80
|
-
|
|
91
|
+
# $$ alone is not unique: two calls can run in one invocation (the periodic
|
|
92
|
+
# check and a pricing-miss retry both fire on a cold cache) and would then
|
|
93
|
+
# curl -o into the same path and mv over each other, installing a truncated
|
|
94
|
+
# script that can never self-heal. The stamp suffix is the right key — it is
|
|
95
|
+
# what makes two callers mutually exclusive in the first place, so callers
|
|
96
|
+
# that could overlap necessarily have different suffixes.
|
|
97
|
+
local tmp="${self}.tmp.$$${stamp_suffix}"
|
|
81
98
|
(
|
|
82
99
|
# Detach stdin (CC pipes JSON to us) so curl can't accidentally consume
|
|
83
100
|
# it, and silence all output so nothing leaks into the statusline.
|
|
@@ -96,6 +113,11 @@ weave_self_refresh() {
|
|
|
96
113
|
fi
|
|
97
114
|
else
|
|
98
115
|
rm -f "$tmp"
|
|
116
|
+
# A download that never landed shouldn't spend the caller's whole
|
|
117
|
+
# interval; drop the stamp so the next turn can try again.
|
|
118
|
+
if [ "$retry_on_fail" = "1" ]; then
|
|
119
|
+
rm -f "$stamp"
|
|
120
|
+
fi
|
|
99
121
|
fi
|
|
100
122
|
) >/dev/null 2>&1 &
|
|
101
123
|
disown 2>/dev/null || true
|
|
@@ -137,6 +159,7 @@ prices='{
|
|
|
137
159
|
"claude-opus-4-6": 0.005,
|
|
138
160
|
"claude-opus-4-7": 0.005,
|
|
139
161
|
"claude-opus-4-8": 0.005,
|
|
162
|
+
"claude-opus-5": 0.005,
|
|
140
163
|
"claude-sonnet-4-5": 0.003,
|
|
141
164
|
"claude-sonnet-4-6": 0.003,
|
|
142
165
|
"claude-sonnet-5": 0.003,
|
|
@@ -152,6 +175,8 @@ prices='{
|
|
|
152
175
|
"gemini-3.1-flash-lite-preview": 0.0001,
|
|
153
176
|
"gemini-3.1-pro-preview": 0.002,
|
|
154
177
|
"gemini-3.5-flash": 0.0015,
|
|
178
|
+
"gemini-3.5-flash-lite": 0.0003,
|
|
179
|
+
"gemini-3.6-flash": 0.0015,
|
|
155
180
|
"google/gemma-4-26b-a4b-it": 0.00015,
|
|
156
181
|
"gpt-4.1": 0.002,
|
|
157
182
|
"gpt-4.1-mini": 0.0004,
|
|
@@ -170,12 +195,17 @@ prices='{
|
|
|
170
195
|
"gpt-5.5-mini": 0.0005,
|
|
171
196
|
"gpt-5.5-nano": 0.00015,
|
|
172
197
|
"gpt-5.5-pro": 0.03,
|
|
198
|
+
"gpt-5.6-luna": 0.001,
|
|
199
|
+
"gpt-5.6-sol": 0.005,
|
|
200
|
+
"gpt-5.6-terra": 0.0025,
|
|
201
|
+
"grok-4.5": 0.002,
|
|
173
202
|
"minimax/minimax-m2.7": 0.0003,
|
|
174
203
|
"minimax/minimax-m3": 0.0003,
|
|
175
204
|
"mistralai/mistral-small-2603": 0.0002,
|
|
176
205
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
177
206
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
178
207
|
"moonshotai/kimi-k2.7": 0.00095,
|
|
208
|
+
"moonshotai/kimi-k3": 0.003,
|
|
179
209
|
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
180
210
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
181
211
|
"qwen/qwen3-coder": 0.0009,
|
|
@@ -198,6 +228,7 @@ prices='{
|
|
|
198
228
|
"claude-opus-4-6": 0.025,
|
|
199
229
|
"claude-opus-4-7": 0.025,
|
|
200
230
|
"claude-opus-4-8": 0.025,
|
|
231
|
+
"claude-opus-5": 0.025,
|
|
201
232
|
"claude-sonnet-4-5": 0.015,
|
|
202
233
|
"claude-sonnet-4-6": 0.015,
|
|
203
234
|
"claude-sonnet-5": 0.015,
|
|
@@ -213,6 +244,8 @@ prices='{
|
|
|
213
244
|
"gemini-3.1-flash-lite-preview": 0.0004,
|
|
214
245
|
"gemini-3.1-pro-preview": 0.008,
|
|
215
246
|
"gemini-3.5-flash": 0.009,
|
|
247
|
+
"gemini-3.5-flash-lite": 0.0025,
|
|
248
|
+
"gemini-3.6-flash": 0.0075,
|
|
216
249
|
"google/gemma-4-26b-a4b-it": 0.0006,
|
|
217
250
|
"gpt-4.1": 0.008,
|
|
218
251
|
"gpt-4.1-mini": 0.0016,
|
|
@@ -231,12 +264,17 @@ prices='{
|
|
|
231
264
|
"gpt-5.5-mini": 0.0025,
|
|
232
265
|
"gpt-5.5-nano": 0.0006,
|
|
233
266
|
"gpt-5.5-pro": 0.18,
|
|
267
|
+
"gpt-5.6-luna": 0.006,
|
|
268
|
+
"gpt-5.6-sol": 0.03,
|
|
269
|
+
"gpt-5.6-terra": 0.015,
|
|
270
|
+
"grok-4.5": 0.006,
|
|
234
271
|
"minimax/minimax-m2.7": 0.0012,
|
|
235
272
|
"minimax/minimax-m3": 0.0012,
|
|
236
273
|
"mistralai/mistral-small-2603": 0.0006,
|
|
237
274
|
"moonshotai/kimi-k2.5": 0.003,
|
|
238
275
|
"moonshotai/kimi-k2.6": 0.004,
|
|
239
276
|
"moonshotai/kimi-k2.7": 0.004,
|
|
277
|
+
"moonshotai/kimi-k3": 0.015,
|
|
240
278
|
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
241
279
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
242
280
|
"qwen/qwen3-coder": 0.0027,
|
|
@@ -383,6 +421,42 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
383
421
|
) || true
|
|
384
422
|
fi
|
|
385
423
|
|
|
424
|
+
# ---------- refresh when a model isn't in the pricing table ----------
|
|
425
|
+
#
|
|
426
|
+
# A model that shipped after this copy of the script has no price entry, so the
|
|
427
|
+
# jq guard above zeroes savings on every turn and the line reads "saved $0.00"
|
|
428
|
+
# — indistinguishable from "the router ran and didn't beat your selection". The
|
|
429
|
+
# periodic check heals that eventually, but a week late, and a model launch is
|
|
430
|
+
# exactly when the number gets looked at. Refresh off-schedule instead.
|
|
431
|
+
#
|
|
432
|
+
# Keyed on its own stamp per unpriced id: a model we never price (self-hosted,
|
|
433
|
+
# unrecognized) then costs one download per interval, not one per turn, and
|
|
434
|
+
# can't starve the periodic check.
|
|
435
|
+
weave_refresh_on_price_miss() {
|
|
436
|
+
local candidates="" m
|
|
437
|
+
for m in "$@"; do
|
|
438
|
+
case "$m" in
|
|
439
|
+
"" | "?" | failure | weave-router | "<synthetic>") continue ;;
|
|
440
|
+
esac
|
|
441
|
+
candidates="${candidates}${m}"$'\n'
|
|
442
|
+
done
|
|
443
|
+
[ -n "$candidates" ] || return 0
|
|
444
|
+
|
|
445
|
+
# A malformed $prices emits nothing here, which fails closed (no refresh)
|
|
446
|
+
# rather than re-downloading every turn.
|
|
447
|
+
local missing
|
|
448
|
+
missing="$(printf '%s' "$candidates" \
|
|
449
|
+
| jq -rR --argjson p "$prices" \
|
|
450
|
+
'select($p.input[.] == null or $p.output[.] == null)' 2>/dev/null \
|
|
451
|
+
| head -n 1)" || return 0
|
|
452
|
+
[ -n "$missing" ] || return 0
|
|
453
|
+
|
|
454
|
+
local model_slug
|
|
455
|
+
model_slug="$(printf '%s' "$missing" | tr -c 'A-Za-z0-9._-' '_')"
|
|
456
|
+
weave_self_refresh ".miss.${model_slug}" 1
|
|
457
|
+
}
|
|
458
|
+
weave_refresh_on_price_miss "$requested_norm" "$routed" 2>/dev/null || true
|
|
459
|
+
|
|
386
460
|
# Brand color (#FF6C47) on terminals that grok 24-bit truecolor — that's
|
|
387
461
|
# every modern one (iTerm2, Apple Terminal, vscode, ghostty, alacritty,
|
|
388
462
|
# wezterm, kitty). Falls back gracefully on any escape-stripping terminal.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Print the session id that correlates this session in router telemetry and logs.
|
|
3
|
+
allowed-tools: Bash(echo:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Print the session id used by the Weave Router to correlate this session's
|
|
7
|
+
telemetry, logs, and feedback submissions. Also works as a transcript
|
|
8
|
+
filename key for manual lookups in the router dashboard.
|
|
9
|
+
|
|
10
|
+
The router carries this id as `X-Claude-Code-Session-Id` on every request and
|
|
11
|
+
stores it in `model_router_users.SessionID` for analytics joins.
|
|
12
|
+
|
|
13
|
+
Run `echo "$CLAUDE_CODE_SESSION_ID"`.
|
|
14
|
+
|
|
15
|
+
Then tell me the session id in one line, formatted as inline code so I can copy it.
|
package/install.sh
CHANGED
|
@@ -127,7 +127,10 @@ skip() { printf "%s⊙%s %s%s%s\n" "$C_DIM" "$C_RESET" "$C_DIM" "$*" "$C_RESET";
|
|
|
127
127
|
# print after a successful install is copy-paste-correct. Kept in sync with
|
|
128
128
|
# uninstall.sh's flag surface.
|
|
129
129
|
uninstall_cmd() {
|
|
130
|
-
|
|
130
|
+
# --package + `--` is load-bearing: npm <= 6's bundled npx treats an
|
|
131
|
+
# undeclared `-y` as consuming the NEXT token, so `npx -y @workweave/router`
|
|
132
|
+
# loses the package name and resolves whatever follows as the command.
|
|
133
|
+
local cmd="npx --package @workweave/router -y -- weave-router --uninstall"
|
|
131
134
|
case "$target" in
|
|
132
135
|
codex) cmd="$cmd --codex" ;;
|
|
133
136
|
opencode) cmd="$cmd --opencode" ;;
|
|
@@ -577,11 +580,10 @@ write_opencode_config() {
|
|
|
577
580
|
| (if $name != "" then . + {"X-Weave-User-Name": $name } else . end)
|
|
578
581
|
')"
|
|
579
582
|
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
#
|
|
583
|
-
#
|
|
584
|
-
# turn, its matching subscription (if connected) pays.
|
|
583
|
+
# Surface one Auto choice in opencode's picker. The router chooses the
|
|
584
|
+
# upstream model for every request, so presenting pinned model names would
|
|
585
|
+
# imply a choice that the router intentionally does not honor. Whichever
|
|
586
|
+
# model serves a turn uses its matching subscription when one is connected.
|
|
585
587
|
#
|
|
586
588
|
# npm is @ai-sdk/openai and baseURL KEEPS its /v1 here: opencode's
|
|
587
589
|
# @ai-sdk/openai provider appends /responses, yielding the router's
|
|
@@ -601,12 +603,7 @@ write_opencode_config() {
|
|
|
601
603
|
name: "Weave Router",
|
|
602
604
|
options: { apiKey: $key, baseURL: $url, headers: $headers },
|
|
603
605
|
models: {
|
|
604
|
-
|
|
605
|
-
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6 (via Weave Router)" },
|
|
606
|
-
"claude-opus-4-8": { name: "Claude Opus 4.8 (via Weave Router)" },
|
|
607
|
-
"claude-haiku-4-5": { name: "Claude Haiku 4.5 (via Weave Router)" },
|
|
608
|
-
"gpt-5.5": { name: "GPT-5.5 (via Weave Router)" },
|
|
609
|
-
"gpt-5.4": { name: "GPT-5.4 (via Weave Router)" }
|
|
606
|
+
auto: { name: "Auto" }
|
|
610
607
|
}
|
|
611
608
|
}
|
|
612
609
|
')"
|
|
@@ -656,8 +653,9 @@ write_opencode_config() {
|
|
|
656
653
|
|
|
657
654
|
# Merge into any existing opencode.json. We always overwrite provider.weave
|
|
658
655
|
# so re-install reflects the latest key/identity, but we leave the rest of the
|
|
659
|
-
# file (other providers, mcp, agent settings) untouched.
|
|
660
|
-
#
|
|
656
|
+
# file (other providers, mcp, agent settings) untouched. A previously
|
|
657
|
+
# installed `weave/*` model is migrated to `weave/auto`; unrelated provider
|
|
658
|
+
# choices stay untouched.
|
|
661
659
|
#
|
|
662
660
|
# The weave-claude login provider AND the plugin entry are written together
|
|
663
661
|
# only when the bundled plugin was present and copied ($plugin non-empty): the
|
|
@@ -685,12 +683,12 @@ write_opencode_config() {
|
|
|
685
683
|
then (.plugin -= [$pluginspec]) | (if (.plugin | length) == 0 then del(.plugin) else . end)
|
|
686
684
|
else . end)
|
|
687
685
|
end)
|
|
688
|
-
| (if (.model // "") == "" then .model = "weave/
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
686
|
+
| (if (.model // "") == "" then .model = "weave/auto" else . end)
|
|
687
|
+
# Replace any legacy Weave model choice with the single auto-routing
|
|
688
|
+
# choice. Models from unrelated providers remain unchanged.
|
|
689
|
+
| (if (.model // "" | tostring) as $model
|
|
690
|
+
| ($model | startswith("weave/") or startswith("weave-codex/") or startswith("weave-claude/"))
|
|
691
|
+
then .model = "weave/auto" else . end)
|
|
694
692
|
| (.["$schema"] //= "https://opencode.ai/config.json")
|
|
695
693
|
' "$config_file")"
|
|
696
694
|
else
|
|
@@ -700,7 +698,7 @@ write_opencode_config() {
|
|
|
700
698
|
--arg plugin "$plugin_arg" '
|
|
701
699
|
{
|
|
702
700
|
"$schema": "https://opencode.ai/config.json",
|
|
703
|
-
model: "weave/
|
|
701
|
+
model: "weave/auto",
|
|
704
702
|
provider: { weave: $block }
|
|
705
703
|
}
|
|
706
704
|
| (if $plugin != "" then .provider["weave-claude"] = $claude | .plugin = [$plugin] else . end)
|
|
@@ -771,10 +769,12 @@ write_pi_models_config() {
|
|
|
771
769
|
headers: $headers,
|
|
772
770
|
models: [
|
|
773
771
|
{ id: "claude-fable-5", name: "Claude Fable 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
|
|
774
|
-
{ id: "claude-opus-
|
|
772
|
+
{ id: "claude-opus-5", name: "Claude Opus 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
|
|
775
773
|
{ id: "claude-opus-4-7", name: "Claude Opus 4.7 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
|
|
776
774
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
|
|
777
|
-
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 32000 }
|
|
775
|
+
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 32000 },
|
|
776
|
+
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1050000, maxTokens: 128000 },
|
|
777
|
+
{ id: "grok-4.5", name: "Grok 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 500000, maxTokens: 131072 }
|
|
778
778
|
]
|
|
779
779
|
}
|
|
780
780
|
')"
|
|
@@ -1573,30 +1573,34 @@ toggle_opencode() {
|
|
|
1573
1573
|
;;
|
|
1574
1574
|
on)
|
|
1575
1575
|
if [ "$on" = "true" ]; then ok "opencode is already on — nothing to do."; return 0; fi
|
|
1576
|
-
restore_model="weave/
|
|
1576
|
+
restore_model="weave/auto"
|
|
1577
1577
|
if [ "$parked_present" = "true" ]; then
|
|
1578
|
-
restore_model="$(jq -r '.model // "weave/
|
|
1578
|
+
restore_model="$(jq -r '.model // "weave/auto"' "$parked")"
|
|
1579
1579
|
elif [ "$has_weave" != "true" ]; then
|
|
1580
1580
|
warn "opencode isn't configured for the router. Run the installer first."; return 0
|
|
1581
1581
|
else
|
|
1582
1582
|
# No parked model (sidecar deleted by hand). Derive the default from the
|
|
1583
1583
|
# installed provider.weave.models block rather than a hardcoded literal
|
|
1584
|
-
# that silently diverges when the installer's default changes — prefer
|
|
1585
|
-
#
|
|
1584
|
+
# that silently diverges when the installer's default changes — prefer
|
|
1585
|
+
# the auto-routing entry, else the first model the installer registered.
|
|
1586
1586
|
restore_model="$(jq -r '
|
|
1587
1587
|
(.provider.weave.models // {} | keys) as $k
|
|
1588
|
-
| (([$k[] | select(
|
|
1588
|
+
| (([$k[] | select(. == "auto")] | first) // $k[0] // "auto")
|
|
1589
1589
|
| "weave/" + .
|
|
1590
|
-
' "$f" 2>/dev/null || echo "weave/
|
|
1590
|
+
' "$f" 2>/dev/null || echo "weave/auto")"
|
|
1591
1591
|
fi
|
|
1592
|
-
#
|
|
1593
|
-
#
|
|
1594
|
-
# `weave` default so `on` can't leave opencode pointing at a missing
|
|
1595
|
-
# provider.
|
|
1592
|
+
# Restore only a registered `weave` model. Legacy installations may have
|
|
1593
|
+
# parked a pinned model, but a current install exposes only `weave/auto`.
|
|
1596
1594
|
case "$restore_model" in
|
|
1595
|
+
weave/*)
|
|
1596
|
+
local model_id="${restore_model#weave/}"
|
|
1597
|
+
if ! jq -e --arg id "$model_id" '(.provider.weave.models // {}) | has($id)' "$f" >/dev/null 2>&1; then
|
|
1598
|
+
restore_model="weave/auto"
|
|
1599
|
+
fi
|
|
1600
|
+
;;
|
|
1597
1601
|
weave-codex/*)
|
|
1598
1602
|
if [ "$(jq -r '((.provider // {}) | has("weave-codex"))' "$f" 2>/dev/null || true)" != "true" ]; then
|
|
1599
|
-
restore_model="weave/
|
|
1603
|
+
restore_model="weave/auto"
|
|
1600
1604
|
fi
|
|
1601
1605
|
;;
|
|
1602
1606
|
esac
|
|
@@ -1689,6 +1693,7 @@ fi
|
|
|
1689
1693
|
# Layout:
|
|
1690
1694
|
# Claude: <settings_dir>/commands/{force-model,unforce-model}.md → /force-model
|
|
1691
1695
|
# Codex: <codex_dir>/prompts/{force-model,unforce-model}.md → /prompts:force-model
|
|
1696
|
+
# opencode: <commands_dir>/{force-model,unforce-model}.md → /force-model
|
|
1692
1697
|
#
|
|
1693
1698
|
# Files come from install/commands/ in the repo (or the colocated commands/
|
|
1694
1699
|
# directory the npm package ships alongside install.sh).
|
|
@@ -1719,8 +1724,8 @@ install_slash_commands() {
|
|
|
1719
1724
|
installed="force-model, unforce-model, router-feedback, fm, ufm, rf"
|
|
1720
1725
|
cmds="force-model unforce-model router-feedback fm ufm rf"
|
|
1721
1726
|
if [ "$target" = "claude" ]; then
|
|
1722
|
-
cmds="$cmds router-off router-on router-status"
|
|
1723
|
-
installed="$installed, router-off, router-on, router-status"
|
|
1727
|
+
cmds="$cmds router-off router-on router-status router-session"
|
|
1728
|
+
installed="$installed, router-off, router-on, router-status, router-session"
|
|
1724
1729
|
fi
|
|
1725
1730
|
|
|
1726
1731
|
# Bake the same scope selector the toggle needs to find this install: --dir
|
|
@@ -1830,6 +1835,26 @@ if [ "$target" = "opencode" ]; then
|
|
|
1830
1835
|
ok "Updated $gitignore (ignored opencode.json, .weave/)"
|
|
1831
1836
|
fi
|
|
1832
1837
|
|
|
1838
|
+
# Slash command wrappers: opencode discovers commands in *.md files under
|
|
1839
|
+
# ~/.config/opencode/commands/ (user scope) and .opencode/commands/ (project
|
|
1840
|
+
# scope). Install command wrappers for /rf (±), /force-model, /unforce-model
|
|
1841
|
+
# so typing /rf + in the TUI expands to /router-feedback + and reaches the
|
|
1842
|
+
# router's server-side feedback interceptor, same as Claude Code + Codex.
|
|
1843
|
+
#
|
|
1844
|
+
# Router on/off/status are not installed — they run npx shell commands
|
|
1845
|
+
# specific to the Claude Code settings model and don't apply to opencode.
|
|
1846
|
+
if [ "$scope" = "project" ]; then
|
|
1847
|
+
opencode_commands_dir="$opencode_dir/.opencode/commands"
|
|
1848
|
+
else
|
|
1849
|
+
# User scope and --dir installs: use the global commands path that opencode
|
|
1850
|
+
# discovers regardless of working directory.
|
|
1851
|
+
opencode_commands_dir="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/commands"
|
|
1852
|
+
fi
|
|
1853
|
+
install_slash_commands "$opencode_commands_dir"
|
|
1854
|
+
if [ "$scope" = "user" ] || [ -n "$install_dir" ]; then
|
|
1855
|
+
info "opencode restart required for commands to take effect."
|
|
1856
|
+
fi
|
|
1857
|
+
|
|
1833
1858
|
# Post-install verification: same probes the Claude/Codex paths run.
|
|
1834
1859
|
if [ "$quiet" != "true" ]; then
|
|
1835
1860
|
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
@@ -1974,10 +1999,21 @@ set -euo pipefail
|
|
|
1974
1999
|
# lives in their per-user cache dir, and on no-content-change days we skip
|
|
1975
2000
|
# the mv entirely so the repo working tree stays clean. When upstream does
|
|
1976
2001
|
# change, the first teammate's commit propagates the new version to the rest.
|
|
2002
|
+
# A pricing-table miss also triggers a refresh off-schedule — see
|
|
2003
|
+
# weave_refresh_on_price_miss below.
|
|
1977
2004
|
#
|
|
1978
2005
|
# Opt out entirely with `export WEAVE_STATUSLINE_UPDATE=0`. Override the
|
|
1979
2006
|
# source with `WEAVE_STATUSLINE_URL=...`, e.g. for self-hosters who fork.
|
|
2007
|
+
#
|
|
2008
|
+
# $1 is an optional stamp suffix, so a caller refreshing for a reason other than
|
|
2009
|
+
# "the interval elapsed" rate-limits on its own clock rather than sharing the
|
|
2010
|
+
# periodic check's budget. $2=1 drops the stamp again if the download fails, for
|
|
2011
|
+
# callers recovering from a known-bad state where waiting out the full interval
|
|
2012
|
+
# on a transient network error is worse than retrying next turn.
|
|
1980
2013
|
weave_self_refresh() {
|
|
2014
|
+
local stamp_suffix="${1:-}"
|
|
2015
|
+
local retry_on_fail="${2:-0}"
|
|
2016
|
+
|
|
1981
2017
|
[ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
|
|
1982
2018
|
command -v curl >/dev/null 2>&1 || return 0
|
|
1983
2019
|
|
|
@@ -1994,7 +2030,7 @@ weave_self_refresh() {
|
|
|
1994
2030
|
mkdir -p "$cache_dir" 2>/dev/null || return 0
|
|
1995
2031
|
local script_slug
|
|
1996
2032
|
script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
|
|
1997
|
-
local stamp="$cache_dir/checked-at${script_slug}"
|
|
2033
|
+
local stamp="$cache_dir/checked-at${script_slug}${stamp_suffix}"
|
|
1998
2034
|
|
|
1999
2035
|
local now stamp_mtime
|
|
2000
2036
|
now="$(date +%s 2>/dev/null)" || return 0
|
|
@@ -2018,7 +2054,13 @@ weave_self_refresh() {
|
|
|
2018
2054
|
: > "$stamp" 2>/dev/null || return 0
|
|
2019
2055
|
|
|
2020
2056
|
local url="${WEAVE_STATUSLINE_URL:-https://raw.githubusercontent.com/workweave/router/main/install/cc-statusline.sh}"
|
|
2021
|
-
|
|
2057
|
+
# $$ alone is not unique: two calls can run in one invocation (the periodic
|
|
2058
|
+
# check and a pricing-miss retry both fire on a cold cache) and would then
|
|
2059
|
+
# curl -o into the same path and mv over each other, installing a truncated
|
|
2060
|
+
# script that can never self-heal. The stamp suffix is the right key — it is
|
|
2061
|
+
# what makes two callers mutually exclusive in the first place, so callers
|
|
2062
|
+
# that could overlap necessarily have different suffixes.
|
|
2063
|
+
local tmp="${self}.tmp.$$${stamp_suffix}"
|
|
2022
2064
|
(
|
|
2023
2065
|
# Detach stdin (CC pipes JSON to us) so curl can't accidentally consume
|
|
2024
2066
|
# it, and silence all output so nothing leaks into the statusline.
|
|
@@ -2037,6 +2079,11 @@ weave_self_refresh() {
|
|
|
2037
2079
|
fi
|
|
2038
2080
|
else
|
|
2039
2081
|
rm -f "$tmp"
|
|
2082
|
+
# A download that never landed shouldn't spend the caller's whole
|
|
2083
|
+
# interval; drop the stamp so the next turn can try again.
|
|
2084
|
+
if [ "$retry_on_fail" = "1" ]; then
|
|
2085
|
+
rm -f "$stamp"
|
|
2086
|
+
fi
|
|
2040
2087
|
fi
|
|
2041
2088
|
) >/dev/null 2>&1 &
|
|
2042
2089
|
disown 2>/dev/null || true
|
|
@@ -2078,6 +2125,7 @@ prices='{
|
|
|
2078
2125
|
"claude-opus-4-6": 0.005,
|
|
2079
2126
|
"claude-opus-4-7": 0.005,
|
|
2080
2127
|
"claude-opus-4-8": 0.005,
|
|
2128
|
+
"claude-opus-5": 0.005,
|
|
2081
2129
|
"claude-sonnet-4-5": 0.003,
|
|
2082
2130
|
"claude-sonnet-4-6": 0.003,
|
|
2083
2131
|
"claude-sonnet-5": 0.003,
|
|
@@ -2093,6 +2141,8 @@ prices='{
|
|
|
2093
2141
|
"gemini-3.1-flash-lite-preview": 0.0001,
|
|
2094
2142
|
"gemini-3.1-pro-preview": 0.002,
|
|
2095
2143
|
"gemini-3.5-flash": 0.0015,
|
|
2144
|
+
"gemini-3.5-flash-lite": 0.0003,
|
|
2145
|
+
"gemini-3.6-flash": 0.0015,
|
|
2096
2146
|
"google/gemma-4-26b-a4b-it": 0.00015,
|
|
2097
2147
|
"gpt-4.1": 0.002,
|
|
2098
2148
|
"gpt-4.1-mini": 0.0004,
|
|
@@ -2111,12 +2161,17 @@ prices='{
|
|
|
2111
2161
|
"gpt-5.5-mini": 0.0005,
|
|
2112
2162
|
"gpt-5.5-nano": 0.00015,
|
|
2113
2163
|
"gpt-5.5-pro": 0.03,
|
|
2164
|
+
"gpt-5.6-luna": 0.001,
|
|
2165
|
+
"gpt-5.6-sol": 0.005,
|
|
2166
|
+
"gpt-5.6-terra": 0.0025,
|
|
2167
|
+
"grok-4.5": 0.002,
|
|
2114
2168
|
"minimax/minimax-m2.7": 0.0003,
|
|
2115
2169
|
"minimax/minimax-m3": 0.0003,
|
|
2116
2170
|
"mistralai/mistral-small-2603": 0.0002,
|
|
2117
2171
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
2118
2172
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
2119
2173
|
"moonshotai/kimi-k2.7": 0.00095,
|
|
2174
|
+
"moonshotai/kimi-k3": 0.003,
|
|
2120
2175
|
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
2121
2176
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
2122
2177
|
"qwen/qwen3-coder": 0.0009,
|
|
@@ -2139,6 +2194,7 @@ prices='{
|
|
|
2139
2194
|
"claude-opus-4-6": 0.025,
|
|
2140
2195
|
"claude-opus-4-7": 0.025,
|
|
2141
2196
|
"claude-opus-4-8": 0.025,
|
|
2197
|
+
"claude-opus-5": 0.025,
|
|
2142
2198
|
"claude-sonnet-4-5": 0.015,
|
|
2143
2199
|
"claude-sonnet-4-6": 0.015,
|
|
2144
2200
|
"claude-sonnet-5": 0.015,
|
|
@@ -2154,6 +2210,8 @@ prices='{
|
|
|
2154
2210
|
"gemini-3.1-flash-lite-preview": 0.0004,
|
|
2155
2211
|
"gemini-3.1-pro-preview": 0.008,
|
|
2156
2212
|
"gemini-3.5-flash": 0.009,
|
|
2213
|
+
"gemini-3.5-flash-lite": 0.0025,
|
|
2214
|
+
"gemini-3.6-flash": 0.0075,
|
|
2157
2215
|
"google/gemma-4-26b-a4b-it": 0.0006,
|
|
2158
2216
|
"gpt-4.1": 0.008,
|
|
2159
2217
|
"gpt-4.1-mini": 0.0016,
|
|
@@ -2172,12 +2230,17 @@ prices='{
|
|
|
2172
2230
|
"gpt-5.5-mini": 0.0025,
|
|
2173
2231
|
"gpt-5.5-nano": 0.0006,
|
|
2174
2232
|
"gpt-5.5-pro": 0.18,
|
|
2233
|
+
"gpt-5.6-luna": 0.006,
|
|
2234
|
+
"gpt-5.6-sol": 0.03,
|
|
2235
|
+
"gpt-5.6-terra": 0.015,
|
|
2236
|
+
"grok-4.5": 0.006,
|
|
2175
2237
|
"minimax/minimax-m2.7": 0.0012,
|
|
2176
2238
|
"minimax/minimax-m3": 0.0012,
|
|
2177
2239
|
"mistralai/mistral-small-2603": 0.0006,
|
|
2178
2240
|
"moonshotai/kimi-k2.5": 0.003,
|
|
2179
2241
|
"moonshotai/kimi-k2.6": 0.004,
|
|
2180
2242
|
"moonshotai/kimi-k2.7": 0.004,
|
|
2243
|
+
"moonshotai/kimi-k3": 0.015,
|
|
2181
2244
|
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
2182
2245
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
2183
2246
|
"qwen/qwen3-coder": 0.0027,
|
|
@@ -2324,6 +2387,42 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
2324
2387
|
) || true
|
|
2325
2388
|
fi
|
|
2326
2389
|
|
|
2390
|
+
# ---------- refresh when a model isn't in the pricing table ----------
|
|
2391
|
+
#
|
|
2392
|
+
# A model that shipped after this copy of the script has no price entry, so the
|
|
2393
|
+
# jq guard above zeroes savings on every turn and the line reads "saved $0.00"
|
|
2394
|
+
# — indistinguishable from "the router ran and didn't beat your selection". The
|
|
2395
|
+
# periodic check heals that eventually, but a week late, and a model launch is
|
|
2396
|
+
# exactly when the number gets looked at. Refresh off-schedule instead.
|
|
2397
|
+
#
|
|
2398
|
+
# Keyed on its own stamp per unpriced id: a model we never price (self-hosted,
|
|
2399
|
+
# unrecognized) then costs one download per interval, not one per turn, and
|
|
2400
|
+
# can't starve the periodic check.
|
|
2401
|
+
weave_refresh_on_price_miss() {
|
|
2402
|
+
local candidates="" m
|
|
2403
|
+
for m in "$@"; do
|
|
2404
|
+
case "$m" in
|
|
2405
|
+
"" | "?" | failure | weave-router | "<synthetic>") continue ;;
|
|
2406
|
+
esac
|
|
2407
|
+
candidates="${candidates}${m}"$'\n'
|
|
2408
|
+
done
|
|
2409
|
+
[ -n "$candidates" ] || return 0
|
|
2410
|
+
|
|
2411
|
+
# A malformed $prices emits nothing here, which fails closed (no refresh)
|
|
2412
|
+
# rather than re-downloading every turn.
|
|
2413
|
+
local missing
|
|
2414
|
+
missing="$(printf '%s' "$candidates" \
|
|
2415
|
+
| jq -rR --argjson p "$prices" \
|
|
2416
|
+
'select($p.input[.] == null or $p.output[.] == null)' 2>/dev/null \
|
|
2417
|
+
| head -n 1)" || return 0
|
|
2418
|
+
[ -n "$missing" ] || return 0
|
|
2419
|
+
|
|
2420
|
+
local model_slug
|
|
2421
|
+
model_slug="$(printf '%s' "$missing" | tr -c 'A-Za-z0-9._-' '_')"
|
|
2422
|
+
weave_self_refresh ".miss.${model_slug}" 1
|
|
2423
|
+
}
|
|
2424
|
+
weave_refresh_on_price_miss "$requested_norm" "$routed" 2>/dev/null || true
|
|
2425
|
+
|
|
2327
2426
|
# Brand color (#FF6C47) on terminals that grok 24-bit truecolor — that's
|
|
2328
2427
|
# every modern one (iTerm2, Apple Terminal, vscode, ghostty, alacritty,
|
|
2329
2428
|
# wezterm, kitty). Falls back gracefully on any escape-stripping terminal.
|
|
@@ -2439,12 +2538,20 @@ custom_headers="$custom_headers"$'\n'"X-App: claude-code"
|
|
|
2439
2538
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
2440
2539
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
2441
2540
|
env: { ANTHROPIC_BASE_URL: $url, ENABLE_TOOL_SEARCH: "auto" },
|
|
2442
|
-
statusLine: { type: "command", command: $sl }
|
|
2541
|
+
statusLine: { type: "command", command: $sl },
|
|
2542
|
+
attribution: {
|
|
2543
|
+
commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
|
|
2544
|
+
pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
|
|
2545
|
+
}
|
|
2443
2546
|
}' >"$tmp_patch"
|
|
2444
2547
|
else
|
|
2445
2548
|
jq -n --arg url "$base_url" --arg header "$custom_headers" --arg sl "$statusline_path_for_settings" '{
|
|
2446
2549
|
env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header, ENABLE_TOOL_SEARCH: "auto" },
|
|
2447
|
-
statusLine: { type: "command", command: $sl }
|
|
2550
|
+
statusLine: { type: "command", command: $sl },
|
|
2551
|
+
attribution: {
|
|
2552
|
+
commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
|
|
2553
|
+
pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
|
|
2554
|
+
}
|
|
2448
2555
|
}' >"$tmp_patch"
|
|
2449
2556
|
fi
|
|
2450
2557
|
|
|
@@ -2460,6 +2567,7 @@ if [ -f "$settings_file" ]; then
|
|
|
2460
2567
|
| (if (.env | length) == 0 then del(.env) else . end)
|
|
2461
2568
|
| del(.apiKeyHelper)
|
|
2462
2569
|
| (if $b.statusLine then .statusLine = $b.statusLine else . end)
|
|
2570
|
+
| (if $b.attribution then .attribution = $b.attribution else . end)
|
|
2463
2571
|
' "$settings_file" "$tmp_patch")"
|
|
2464
2572
|
printf '%s\n' "$merged" >"$settings_file"
|
|
2465
2573
|
else
|