@workweave/router 0.2.4 → 0.2.6

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
@@ -84,6 +84,18 @@ for the full reference.
84
84
 
85
85
  ## Why npx
86
86
 
87
- `curl -fsSL https://weave.ai/cc/install.sh | sh` still works and is fine.
88
- `npx @workweave/router` adds: Windows support via Git Bash, painless version
89
- pinning, no `curl | sh` aversion, and discoverability via the npm registry.
87
+ `npx @workweave/router` gives Windows support via Git Bash, painless version
88
+ pinning, and discoverability via the npm registry.
89
+
90
+ ## Older npm
91
+
92
+ On npm ≤ 6 the bundled `npx` treats an undeclared `-y` as consuming the next
93
+ token, so `npx -y @workweave/router --claude` silently drops the package name
94
+ and resolves the following argument as the command instead. Either upgrade
95
+ (`npm i -g npm@latest`) or name the binary explicitly:
96
+
97
+ ```bash
98
+ npx --package @workweave/router -y -- weave-router --claude
99
+ ```
100
+
101
+ That form is correct on every npm version.
package/cc-statusline.sh CHANGED
@@ -130,14 +130,19 @@ normalize_model() {
130
130
  prices='{
131
131
  "input": {
132
132
  "claude-fable-5": 0.01,
133
- "claude-haiku-4-5": 0.0008,
133
+ "claude-haiku-4-5": 0.001,
134
+ "claude-opus-4-0": 0.015,
135
+ "claude-opus-4-1": 0.015,
136
+ "claude-opus-4-5": 0.005,
134
137
  "claude-opus-4-6": 0.005,
135
138
  "claude-opus-4-7": 0.005,
136
139
  "claude-opus-4-8": 0.005,
140
+ "claude-opus-5": 0.005,
137
141
  "claude-sonnet-4-5": 0.003,
138
142
  "claude-sonnet-4-6": 0.003,
139
- "deepseek/deepseek-v4-flash": 0.00014,
140
- "deepseek/deepseek-v4-pro": 0.00174,
143
+ "claude-sonnet-5": 0.003,
144
+ "deepseek/deepseek-v4-flash": 0.0001134,
145
+ "deepseek/deepseek-v4-pro": 0.001318,
141
146
  "gemini-2.0-flash": 0.0001,
142
147
  "gemini-2.0-flash-lite": 0.000075,
143
148
  "gemini-2.5-flash": 0.0003,
@@ -148,6 +153,9 @@ prices='{
148
153
  "gemini-3.1-flash-lite-preview": 0.0001,
149
154
  "gemini-3.1-pro-preview": 0.002,
150
155
  "gemini-3.5-flash": 0.0015,
156
+ "gemini-3.5-flash-lite": 0.0003,
157
+ "gemini-3.6-flash": 0.0015,
158
+ "google/gemma-4-26b-a4b-it": 0.00015,
151
159
  "gpt-4.1": 0.002,
152
160
  "gpt-4.1-mini": 0.0004,
153
161
  "gpt-4.1-nano": 0.0001,
@@ -157,43 +165,53 @@ prices='{
157
165
  "gpt-5-chat": 0.0025,
158
166
  "gpt-5-mini": 0.0005,
159
167
  "gpt-5-nano": 0.0001,
160
- "gpt-5.4": 0.003,
161
- "gpt-5.4-mini": 0.0004,
162
- "gpt-5.4-nano": 0.0001,
163
- "gpt-5.4-pro": 0.02,
168
+ "gpt-5.4": 0.0025,
169
+ "gpt-5.4-mini": 0.00075,
170
+ "gpt-5.4-nano": 0.0002,
171
+ "gpt-5.4-pro": 0.03,
164
172
  "gpt-5.5": 0.005,
165
173
  "gpt-5.5-mini": 0.0005,
166
174
  "gpt-5.5-nano": 0.00015,
167
175
  "gpt-5.5-pro": 0.03,
176
+ "gpt-5.6-luna": 0.001,
177
+ "gpt-5.6-sol": 0.005,
178
+ "gpt-5.6-terra": 0.0025,
179
+ "grok-4.5": 0.002,
168
180
  "minimax/minimax-m2.7": 0.0003,
169
181
  "minimax/minimax-m3": 0.0003,
170
182
  "mistralai/mistral-small-2603": 0.0002,
171
183
  "moonshotai/kimi-k2.5": 0.0006,
172
184
  "moonshotai/kimi-k2.6": 0.00095,
173
185
  "moonshotai/kimi-k2.7": 0.00095,
186
+ "moonshotai/kimi-k3": 0.003,
174
187
  "qwen/qwen3-235b-a22b-2507": 0.0002266,
175
188
  "qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
176
189
  "qwen/qwen3-coder": 0.0009,
177
190
  "qwen/qwen3-coder-next": 0.0005,
178
191
  "qwen/qwen3-next-80b-a3b-instruct": 0.00015,
179
192
  "qwen/qwen3.5-flash-02-23": 0.00005,
180
- "qwen/qwen3.6-35b-a3b": 0.00015,
193
+ "qwen/qwen3.6-35b-a3b": 0.000172,
181
194
  "qwen/qwen3.7-plus": 0.0004,
182
195
  "xiaomi/mimo-v2.5-pro": 0.001,
183
- "z-ai/glm-5": 0.0006,
184
- "z-ai/glm-5.1": 0.00105,
196
+ "z-ai/glm-5": 0.001,
197
+ "z-ai/glm-5.1": 0.0014,
185
198
  "z-ai/glm-5.2": 0.0014
186
199
  },
187
200
  "output": {
188
201
  "claude-fable-5": 0.05,
189
- "claude-haiku-4-5": 0.004,
202
+ "claude-haiku-4-5": 0.005,
203
+ "claude-opus-4-0": 0.075,
204
+ "claude-opus-4-1": 0.075,
205
+ "claude-opus-4-5": 0.025,
190
206
  "claude-opus-4-6": 0.025,
191
207
  "claude-opus-4-7": 0.025,
192
208
  "claude-opus-4-8": 0.025,
209
+ "claude-opus-5": 0.025,
193
210
  "claude-sonnet-4-5": 0.015,
194
211
  "claude-sonnet-4-6": 0.015,
195
- "deepseek/deepseek-v4-flash": 0.00028,
196
- "deepseek/deepseek-v4-pro": 0.00348,
212
+ "claude-sonnet-5": 0.015,
213
+ "deepseek/deepseek-v4-flash": 0.0002791,
214
+ "deepseek/deepseek-v4-pro": 0.0026361,
197
215
  "gemini-2.0-flash": 0.0004,
198
216
  "gemini-2.0-flash-lite": 0.0003,
199
217
  "gemini-2.5-flash": 0.0012,
@@ -204,6 +222,9 @@ prices='{
204
222
  "gemini-3.1-flash-lite-preview": 0.0004,
205
223
  "gemini-3.1-pro-preview": 0.008,
206
224
  "gemini-3.5-flash": 0.009,
225
+ "gemini-3.5-flash-lite": 0.0025,
226
+ "gemini-3.6-flash": 0.0075,
227
+ "google/gemma-4-26b-a4b-it": 0.0006,
207
228
  "gpt-4.1": 0.008,
208
229
  "gpt-4.1-mini": 0.0016,
209
230
  "gpt-4.1-nano": 0.0004,
@@ -213,31 +234,36 @@ prices='{
213
234
  "gpt-5-chat": 0.01,
214
235
  "gpt-5-mini": 0.002,
215
236
  "gpt-5-nano": 0.0004,
216
- "gpt-5.4": 0.012,
217
- "gpt-5.4-mini": 0.0016,
218
- "gpt-5.4-nano": 0.0004,
219
- "gpt-5.4-pro": 0.08,
220
- "gpt-5.5": 0.04,
237
+ "gpt-5.4": 0.015,
238
+ "gpt-5.4-mini": 0.0045,
239
+ "gpt-5.4-nano": 0.00125,
240
+ "gpt-5.4-pro": 0.18,
241
+ "gpt-5.5": 0.03,
221
242
  "gpt-5.5-mini": 0.0025,
222
243
  "gpt-5.5-nano": 0.0006,
223
- "gpt-5.5-pro": 0.12,
244
+ "gpt-5.5-pro": 0.18,
245
+ "gpt-5.6-luna": 0.006,
246
+ "gpt-5.6-sol": 0.03,
247
+ "gpt-5.6-terra": 0.015,
248
+ "grok-4.5": 0.006,
224
249
  "minimax/minimax-m2.7": 0.0012,
225
250
  "minimax/minimax-m3": 0.0012,
226
251
  "mistralai/mistral-small-2603": 0.0006,
227
252
  "moonshotai/kimi-k2.5": 0.003,
228
253
  "moonshotai/kimi-k2.6": 0.004,
229
254
  "moonshotai/kimi-k2.7": 0.004,
255
+ "moonshotai/kimi-k3": 0.015,
230
256
  "qwen/qwen3-235b-a22b-2507": 0.0009064,
231
257
  "qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
232
258
  "qwen/qwen3-coder": 0.0027,
233
259
  "qwen/qwen3-coder-next": 0.0012,
234
260
  "qwen/qwen3-next-80b-a3b-instruct": 0.0012,
235
261
  "qwen/qwen3.5-flash-02-23": 0.00015,
236
- "qwen/qwen3.6-35b-a3b": 0.00095,
262
+ "qwen/qwen3.6-35b-a3b": 0.0012002,
237
263
  "qwen/qwen3.7-plus": 0.0016,
238
264
  "xiaomi/mimo-v2.5-pro": 0.003,
239
- "z-ai/glm-5": 0.00208,
240
- "z-ai/glm-5.1": 0.0035,
265
+ "z-ai/glm-5": 0.0032,
266
+ "z-ai/glm-5.1": 0.0044,
241
267
  "z-ai/glm-5.2": 0.0044
242
268
  }
243
269
  }'
@@ -427,20 +453,23 @@ elif [[ "$routed" == "failure" ]]; then
427
453
  # swap or compute savings against a non-model.
428
454
  printf '%s — %s%s' "$brand" "$routed" "$tokens_clause"
429
455
  elif [[ -n "$routed" ]]; then
430
- # Show the savings clause only when the session is genuinely net-saving.
456
+ # Always show the savings clause, flooring a non-positive total at $0.00.
431
457
  # session_savings is "0.0000" on fresh sessions or sessions where every
432
- # turn routed back to the selected model; it can also go negative when
433
- # sticky routing forces a haiku-tagged side-call up to a cached
434
- # sonnet/opus decision. In both cases the word "saved" would mislead,
435
- # so drop the savings clause but keep the token totals.
436
- has_savings="false"
437
- if [[ -n "$session_savings" ]] \
438
- && awk -v v="$session_savings" 'BEGIN{exit !(v+0 > 0.005)}'; then
439
- has_savings="true"
458
+ # turn routed back to the selected model, and can go negative when routing
459
+ # upgrades a turn to a pricier model (e.g. a sticky/hard-pinned side-call,
460
+ # or a hard turn escalated to opus). "saved -$X" would mislead, so clamp
461
+ # the display to $0.00 rather than dropping the clause — a $0.00 readout
462
+ # tells the user the router ran and simply didn't beat their selection.
463
+ # When the CC selection is unknown ("?" or empty) there's nothing to
464
+ # compare against, so drop the "← selection" arrow and just show routed.
465
+ display_savings="$session_savings"
466
+ if [[ -z "$display_savings" ]] \
467
+ || awk -v v="$display_savings" 'BEGIN{exit !(v+0 < 0)}'; then
468
+ display_savings="0"
440
469
  fi
441
- if [[ "$has_savings" == "true" ]]; then
470
+ if [[ -n "$selected_display" && "$selected_display" != "?" ]]; then
442
471
  printf '%s — %s ← %s · saved %s%s' \
443
- "$brand" "$routed" "$selected_display" "$(fmt_money "$session_savings")" "$tokens_clause"
472
+ "$brand" "$routed" "$selected_display" "$(fmt_money "$display_savings")" "$tokens_clause"
444
473
  else
445
474
  printf '%s — %s%s' "$brand" "$routed" "$tokens_clause"
446
475
  fi
@@ -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
- local cmd="npx -y @workweave/router --uninstall"
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
- # Headline models we surface in opencode's picker. The router re-routes
581
- # each request anyway, so this list is mostly UX — what shows up when the
582
- # user runs /models inside opencode. We list a mix of GPT + Claude so the
583
- # picker reflects that the router spans families; whichever model serves the
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,11 +603,7 @@ write_opencode_config() {
601
603
  name: "Weave Router",
602
604
  options: { apiKey: $key, baseURL: $url, headers: $headers },
603
605
  models: {
604
- "claude-sonnet-4-6": { name: "Claude Sonnet 4.6 (via Weave Router)" },
605
- "claude-opus-4-8": { name: "Claude Opus 4.8 (via Weave Router)" },
606
- "claude-haiku-4-5": { name: "Claude Haiku 4.5 (via Weave Router)" },
607
- "gpt-5.5": { name: "GPT-5.5 (via Weave Router)" },
608
- "gpt-5.4": { name: "GPT-5.4 (via Weave Router)" }
606
+ auto: { name: "Auto" }
609
607
  }
610
608
  }
611
609
  ')"
@@ -655,8 +653,9 @@ write_opencode_config() {
655
653
 
656
654
  # Merge into any existing opencode.json. We always overwrite provider.weave
657
655
  # so re-install reflects the latest key/identity, but we leave the rest of the
658
- # file (other providers, mcp, agent settings) untouched. Top-level `model` is
659
- # only set when the user hasn't already picked one.
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.
660
659
  #
661
660
  # The weave-claude login provider AND the plugin entry are written together
662
661
  # only when the bundled plugin was present and copied ($plugin non-empty): the
@@ -684,12 +683,12 @@ write_opencode_config() {
684
683
  then (.plugin -= [$pluginspec]) | (if (.plugin | length) == 0 then del(.plugin) else . end)
685
684
  else . end)
686
685
  end)
687
- | (if (.model // "") == "" then .model = "weave/claude-sonnet-4-6" else . end)
688
- # Reset the default model if it points at a provider we just removed: the
689
- # retired weave-codex, or weave-claude on a plugin-less re-install (it has
690
- # no models anyway). Otherwise opencode boots with a dangling model.
691
- | (if (.model // "" | tostring | startswith("weave-codex/")) then .model = "weave/claude-sonnet-4-6" else . end)
692
- | (if $plugin == "" and (.model // "" | tostring | startswith("weave-claude/")) then .model = "weave/claude-sonnet-4-6" else . end)
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)
693
692
  | (.["$schema"] //= "https://opencode.ai/config.json")
694
693
  ' "$config_file")"
695
694
  else
@@ -699,7 +698,7 @@ write_opencode_config() {
699
698
  --arg plugin "$plugin_arg" '
700
699
  {
701
700
  "$schema": "https://opencode.ai/config.json",
702
- model: "weave/claude-sonnet-4-6",
701
+ model: "weave/auto",
703
702
  provider: { weave: $block }
704
703
  }
705
704
  | (if $plugin != "" then .provider["weave-claude"] = $claude | .plugin = [$plugin] else . end)
@@ -770,10 +769,12 @@ write_pi_models_config() {
770
769
  headers: $headers,
771
770
  models: [
772
771
  { id: "claude-fable-5", name: "Claude Fable 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
773
- { id: "claude-opus-4-8", name: "Claude Opus 4.8 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
772
+ { id: "claude-opus-5", name: "Claude Opus 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
774
773
  { id: "claude-opus-4-7", name: "Claude Opus 4.7 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
775
774
  { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
776
- { 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 }
777
778
  ]
778
779
  }
779
780
  ')"
@@ -1402,9 +1403,9 @@ toggle_claude() {
1402
1403
  else
1403
1404
  # Park just the router-owned env keys, then strip them so Claude Code
1404
1405
  # falls back to its own Anthropic login.
1405
- jq '{env: ((.env // {}) | {ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS} | with_entries(select(.value != null)))}' "$settings_file" >"$parked"
1406
+ jq '{env: ((.env // {}) | {ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS, ENABLE_TOOL_SEARCH} | with_entries(select(.value != null)))}' "$settings_file" >"$parked"
1406
1407
  chmod 600 "$parked"
1407
- merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL, .ANTHROPIC_CUSTOM_HEADERS)
1408
+ merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL, .ANTHROPIC_CUSTOM_HEADERS, .ENABLE_TOOL_SEARCH)
1408
1409
  | (if (.env // {} | length) == 0 then del(.env) else . end)' "$settings_file")"
1409
1410
  printf '%s\n' "$merged" >"$settings_file"
1410
1411
  fi
@@ -1572,30 +1573,34 @@ toggle_opencode() {
1572
1573
  ;;
1573
1574
  on)
1574
1575
  if [ "$on" = "true" ]; then ok "opencode is already on — nothing to do."; return 0; fi
1575
- restore_model="weave/claude-sonnet-4-6"
1576
+ restore_model="weave/auto"
1576
1577
  if [ "$parked_present" = "true" ]; then
1577
- restore_model="$(jq -r '.model // "weave/claude-sonnet-4-6"' "$parked")"
1578
+ restore_model="$(jq -r '.model // "weave/auto"' "$parked")"
1578
1579
  elif [ "$has_weave" != "true" ]; then
1579
1580
  warn "opencode isn't configured for the router. Run the installer first."; return 0
1580
1581
  else
1581
1582
  # No parked model (sidecar deleted by hand). Derive the default from the
1582
1583
  # installed provider.weave.models block rather than a hardcoded literal
1583
- # that silently diverges when the installer's default changes — prefer a
1584
- # sonnet entry, else the first model the installer registered.
1584
+ # that silently diverges when the installer's default changes — prefer
1585
+ # the auto-routing entry, else the first model the installer registered.
1585
1586
  restore_model="$(jq -r '
1586
1587
  (.provider.weave.models // {} | keys) as $k
1587
- | (([$k[] | select(test("sonnet"))] | first) // $k[0] // "claude-sonnet-4-6")
1588
+ | (([$k[] | select(. == "auto")] | first) // $k[0] // "auto")
1588
1589
  | "weave/" + .
1589
- ' "$f" 2>/dev/null || echo "weave/claude-sonnet-4-6")"
1590
+ ' "$f" 2>/dev/null || echo "weave/auto")"
1590
1591
  fi
1591
- # Never restore a legacy weave-codex model whose provider is no longer
1592
- # registered (every current install strips weave-codex). Fall back to the
1593
- # `weave` default so `on` can't leave opencode pointing at a missing
1594
- # 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`.
1595
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
+ ;;
1596
1601
  weave-codex/*)
1597
1602
  if [ "$(jq -r '((.provider // {}) | has("weave-codex"))' "$f" 2>/dev/null || true)" != "true" ]; then
1598
- restore_model="weave/claude-sonnet-4-6"
1603
+ restore_model="weave/auto"
1599
1604
  fi
1600
1605
  ;;
1601
1606
  esac
@@ -1688,6 +1693,7 @@ fi
1688
1693
  # Layout:
1689
1694
  # Claude: <settings_dir>/commands/{force-model,unforce-model}.md → /force-model
1690
1695
  # Codex: <codex_dir>/prompts/{force-model,unforce-model}.md → /prompts:force-model
1696
+ # opencode: <commands_dir>/{force-model,unforce-model}.md → /force-model
1691
1697
  #
1692
1698
  # Files come from install/commands/ in the repo (or the colocated commands/
1693
1699
  # directory the npm package ships alongside install.sh).
@@ -1718,8 +1724,8 @@ install_slash_commands() {
1718
1724
  installed="force-model, unforce-model, router-feedback, fm, ufm, rf"
1719
1725
  cmds="force-model unforce-model router-feedback fm ufm rf"
1720
1726
  if [ "$target" = "claude" ]; then
1721
- cmds="$cmds router-off router-on router-status"
1722
- 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"
1723
1729
  fi
1724
1730
 
1725
1731
  # Bake the same scope selector the toggle needs to find this install: --dir
@@ -1829,6 +1835,26 @@ if [ "$target" = "opencode" ]; then
1829
1835
  ok "Updated $gitignore (ignored opencode.json, .weave/)"
1830
1836
  fi
1831
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
+
1832
1858
  # Post-install verification: same probes the Claude/Codex paths run.
1833
1859
  if [ "$quiet" != "true" ]; then
1834
1860
  if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
@@ -2070,14 +2096,19 @@ normalize_model() {
2070
2096
  prices='{
2071
2097
  "input": {
2072
2098
  "claude-fable-5": 0.01,
2073
- "claude-haiku-4-5": 0.0008,
2099
+ "claude-haiku-4-5": 0.001,
2100
+ "claude-opus-4-0": 0.015,
2101
+ "claude-opus-4-1": 0.015,
2102
+ "claude-opus-4-5": 0.005,
2074
2103
  "claude-opus-4-6": 0.005,
2075
2104
  "claude-opus-4-7": 0.005,
2076
2105
  "claude-opus-4-8": 0.005,
2106
+ "claude-opus-5": 0.005,
2077
2107
  "claude-sonnet-4-5": 0.003,
2078
2108
  "claude-sonnet-4-6": 0.003,
2079
- "deepseek/deepseek-v4-flash": 0.00014,
2080
- "deepseek/deepseek-v4-pro": 0.00174,
2109
+ "claude-sonnet-5": 0.003,
2110
+ "deepseek/deepseek-v4-flash": 0.0001134,
2111
+ "deepseek/deepseek-v4-pro": 0.001318,
2081
2112
  "gemini-2.0-flash": 0.0001,
2082
2113
  "gemini-2.0-flash-lite": 0.000075,
2083
2114
  "gemini-2.5-flash": 0.0003,
@@ -2088,6 +2119,9 @@ prices='{
2088
2119
  "gemini-3.1-flash-lite-preview": 0.0001,
2089
2120
  "gemini-3.1-pro-preview": 0.002,
2090
2121
  "gemini-3.5-flash": 0.0015,
2122
+ "gemini-3.5-flash-lite": 0.0003,
2123
+ "gemini-3.6-flash": 0.0015,
2124
+ "google/gemma-4-26b-a4b-it": 0.00015,
2091
2125
  "gpt-4.1": 0.002,
2092
2126
  "gpt-4.1-mini": 0.0004,
2093
2127
  "gpt-4.1-nano": 0.0001,
@@ -2097,43 +2131,53 @@ prices='{
2097
2131
  "gpt-5-chat": 0.0025,
2098
2132
  "gpt-5-mini": 0.0005,
2099
2133
  "gpt-5-nano": 0.0001,
2100
- "gpt-5.4": 0.003,
2101
- "gpt-5.4-mini": 0.0004,
2102
- "gpt-5.4-nano": 0.0001,
2103
- "gpt-5.4-pro": 0.02,
2134
+ "gpt-5.4": 0.0025,
2135
+ "gpt-5.4-mini": 0.00075,
2136
+ "gpt-5.4-nano": 0.0002,
2137
+ "gpt-5.4-pro": 0.03,
2104
2138
  "gpt-5.5": 0.005,
2105
2139
  "gpt-5.5-mini": 0.0005,
2106
2140
  "gpt-5.5-nano": 0.00015,
2107
2141
  "gpt-5.5-pro": 0.03,
2142
+ "gpt-5.6-luna": 0.001,
2143
+ "gpt-5.6-sol": 0.005,
2144
+ "gpt-5.6-terra": 0.0025,
2145
+ "grok-4.5": 0.002,
2108
2146
  "minimax/minimax-m2.7": 0.0003,
2109
2147
  "minimax/minimax-m3": 0.0003,
2110
2148
  "mistralai/mistral-small-2603": 0.0002,
2111
2149
  "moonshotai/kimi-k2.5": 0.0006,
2112
2150
  "moonshotai/kimi-k2.6": 0.00095,
2113
2151
  "moonshotai/kimi-k2.7": 0.00095,
2152
+ "moonshotai/kimi-k3": 0.003,
2114
2153
  "qwen/qwen3-235b-a22b-2507": 0.0002266,
2115
2154
  "qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
2116
2155
  "qwen/qwen3-coder": 0.0009,
2117
2156
  "qwen/qwen3-coder-next": 0.0005,
2118
2157
  "qwen/qwen3-next-80b-a3b-instruct": 0.00015,
2119
2158
  "qwen/qwen3.5-flash-02-23": 0.00005,
2120
- "qwen/qwen3.6-35b-a3b": 0.00015,
2159
+ "qwen/qwen3.6-35b-a3b": 0.000172,
2121
2160
  "qwen/qwen3.7-plus": 0.0004,
2122
2161
  "xiaomi/mimo-v2.5-pro": 0.001,
2123
- "z-ai/glm-5": 0.0006,
2124
- "z-ai/glm-5.1": 0.00105,
2162
+ "z-ai/glm-5": 0.001,
2163
+ "z-ai/glm-5.1": 0.0014,
2125
2164
  "z-ai/glm-5.2": 0.0014
2126
2165
  },
2127
2166
  "output": {
2128
2167
  "claude-fable-5": 0.05,
2129
- "claude-haiku-4-5": 0.004,
2168
+ "claude-haiku-4-5": 0.005,
2169
+ "claude-opus-4-0": 0.075,
2170
+ "claude-opus-4-1": 0.075,
2171
+ "claude-opus-4-5": 0.025,
2130
2172
  "claude-opus-4-6": 0.025,
2131
2173
  "claude-opus-4-7": 0.025,
2132
2174
  "claude-opus-4-8": 0.025,
2175
+ "claude-opus-5": 0.025,
2133
2176
  "claude-sonnet-4-5": 0.015,
2134
2177
  "claude-sonnet-4-6": 0.015,
2135
- "deepseek/deepseek-v4-flash": 0.00028,
2136
- "deepseek/deepseek-v4-pro": 0.00348,
2178
+ "claude-sonnet-5": 0.015,
2179
+ "deepseek/deepseek-v4-flash": 0.0002791,
2180
+ "deepseek/deepseek-v4-pro": 0.0026361,
2137
2181
  "gemini-2.0-flash": 0.0004,
2138
2182
  "gemini-2.0-flash-lite": 0.0003,
2139
2183
  "gemini-2.5-flash": 0.0012,
@@ -2144,6 +2188,9 @@ prices='{
2144
2188
  "gemini-3.1-flash-lite-preview": 0.0004,
2145
2189
  "gemini-3.1-pro-preview": 0.008,
2146
2190
  "gemini-3.5-flash": 0.009,
2191
+ "gemini-3.5-flash-lite": 0.0025,
2192
+ "gemini-3.6-flash": 0.0075,
2193
+ "google/gemma-4-26b-a4b-it": 0.0006,
2147
2194
  "gpt-4.1": 0.008,
2148
2195
  "gpt-4.1-mini": 0.0016,
2149
2196
  "gpt-4.1-nano": 0.0004,
@@ -2153,31 +2200,36 @@ prices='{
2153
2200
  "gpt-5-chat": 0.01,
2154
2201
  "gpt-5-mini": 0.002,
2155
2202
  "gpt-5-nano": 0.0004,
2156
- "gpt-5.4": 0.012,
2157
- "gpt-5.4-mini": 0.0016,
2158
- "gpt-5.4-nano": 0.0004,
2159
- "gpt-5.4-pro": 0.08,
2160
- "gpt-5.5": 0.04,
2203
+ "gpt-5.4": 0.015,
2204
+ "gpt-5.4-mini": 0.0045,
2205
+ "gpt-5.4-nano": 0.00125,
2206
+ "gpt-5.4-pro": 0.18,
2207
+ "gpt-5.5": 0.03,
2161
2208
  "gpt-5.5-mini": 0.0025,
2162
2209
  "gpt-5.5-nano": 0.0006,
2163
- "gpt-5.5-pro": 0.12,
2210
+ "gpt-5.5-pro": 0.18,
2211
+ "gpt-5.6-luna": 0.006,
2212
+ "gpt-5.6-sol": 0.03,
2213
+ "gpt-5.6-terra": 0.015,
2214
+ "grok-4.5": 0.006,
2164
2215
  "minimax/minimax-m2.7": 0.0012,
2165
2216
  "minimax/minimax-m3": 0.0012,
2166
2217
  "mistralai/mistral-small-2603": 0.0006,
2167
2218
  "moonshotai/kimi-k2.5": 0.003,
2168
2219
  "moonshotai/kimi-k2.6": 0.004,
2169
2220
  "moonshotai/kimi-k2.7": 0.004,
2221
+ "moonshotai/kimi-k3": 0.015,
2170
2222
  "qwen/qwen3-235b-a22b-2507": 0.0009064,
2171
2223
  "qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
2172
2224
  "qwen/qwen3-coder": 0.0027,
2173
2225
  "qwen/qwen3-coder-next": 0.0012,
2174
2226
  "qwen/qwen3-next-80b-a3b-instruct": 0.0012,
2175
2227
  "qwen/qwen3.5-flash-02-23": 0.00015,
2176
- "qwen/qwen3.6-35b-a3b": 0.00095,
2228
+ "qwen/qwen3.6-35b-a3b": 0.0012002,
2177
2229
  "qwen/qwen3.7-plus": 0.0016,
2178
2230
  "xiaomi/mimo-v2.5-pro": 0.003,
2179
- "z-ai/glm-5": 0.00208,
2180
- "z-ai/glm-5.1": 0.0035,
2231
+ "z-ai/glm-5": 0.0032,
2232
+ "z-ai/glm-5.1": 0.0044,
2181
2233
  "z-ai/glm-5.2": 0.0044
2182
2234
  }
2183
2235
  }'
@@ -2367,20 +2419,23 @@ elif [[ "$routed" == "failure" ]]; then
2367
2419
  # swap or compute savings against a non-model.
2368
2420
  printf '%s — %s%s' "$brand" "$routed" "$tokens_clause"
2369
2421
  elif [[ -n "$routed" ]]; then
2370
- # Show the savings clause only when the session is genuinely net-saving.
2422
+ # Always show the savings clause, flooring a non-positive total at $0.00.
2371
2423
  # session_savings is "0.0000" on fresh sessions or sessions where every
2372
- # turn routed back to the selected model; it can also go negative when
2373
- # sticky routing forces a haiku-tagged side-call up to a cached
2374
- # sonnet/opus decision. In both cases the word "saved" would mislead,
2375
- # so drop the savings clause but keep the token totals.
2376
- has_savings="false"
2377
- if [[ -n "$session_savings" ]] \
2378
- && awk -v v="$session_savings" 'BEGIN{exit !(v+0 > 0.005)}'; then
2379
- has_savings="true"
2380
- fi
2381
- if [[ "$has_savings" == "true" ]]; then
2424
+ # turn routed back to the selected model, and can go negative when routing
2425
+ # upgrades a turn to a pricier model (e.g. a sticky/hard-pinned side-call,
2426
+ # or a hard turn escalated to opus). "saved -$X" would mislead, so clamp
2427
+ # the display to $0.00 rather than dropping the clause — a $0.00 readout
2428
+ # tells the user the router ran and simply didn't beat their selection.
2429
+ # When the CC selection is unknown ("?" or empty) there's nothing to
2430
+ # compare against, so drop the "← selection" arrow and just show routed.
2431
+ display_savings="$session_savings"
2432
+ if [[ -z "$display_savings" ]] \
2433
+ || awk -v v="$display_savings" 'BEGIN{exit !(v+0 < 0)}'; then
2434
+ display_savings="0"
2435
+ fi
2436
+ if [[ -n "$selected_display" && "$selected_display" != "?" ]]; then
2382
2437
  printf '%s — %s ← %s · saved %s%s' \
2383
- "$brand" "$routed" "$selected_display" "$(fmt_money "$session_savings")" "$tokens_clause"
2438
+ "$brand" "$routed" "$selected_display" "$(fmt_money "$display_savings")" "$tokens_clause"
2384
2439
  else
2385
2440
  printf '%s — %s%s' "$brand" "$routed" "$tokens_clause"
2386
2441
  fi
@@ -2417,14 +2472,19 @@ if [ -n "$user_name" ]; then
2417
2472
  fi
2418
2473
  custom_headers="$custom_headers"$'\n'"X-App: claude-code"
2419
2474
 
2475
+ # Setting ANTHROPIC_BASE_URL makes Claude Code treat us as non-first-party and
2476
+ # disable MCP tool-search deferral, inlining every tool schema into every request
2477
+ # — a large uncompactable prefix that can push a session into an autocompact
2478
+ # thrash loop. ENABLE_TOOL_SEARCH=auto restores on-demand loading (Claude Code's
2479
+ # own first-party default).
2420
2480
  if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
2421
2481
  jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
2422
- env: { ANTHROPIC_BASE_URL: $url },
2482
+ env: { ANTHROPIC_BASE_URL: $url, ENABLE_TOOL_SEARCH: "auto" },
2423
2483
  statusLine: { type: "command", command: $sl }
2424
2484
  }' >"$tmp_patch"
2425
2485
  else
2426
2486
  jq -n --arg url "$base_url" --arg header "$custom_headers" --arg sl "$statusline_path_for_settings" '{
2427
- env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header },
2487
+ env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header, ENABLE_TOOL_SEARCH: "auto" },
2428
2488
  statusLine: { type: "command", command: $sl }
2429
2489
  }' >"$tmp_patch"
2430
2490
  fi
@@ -3,7 +3,7 @@
3
3
  Lets a caller's own **AI subscriptions** pay for their **opencode** turns, routed
4
4
  through the Weave Router. A subscription is a **credential scoped to the model
5
5
  family it can pay for**, not a provider you pick: you connect your ChatGPT
6
- (Codex) and/or Claude (Pro/Max) plan once, the router routes every turn to the
6
+ (Codex) and/or Claude (Pro/Max) plan once, the router routes every request to the
7
7
  best model, and bills the plan that matches the model it served — ChatGPT pays
8
8
  for GPT/Codex turns, Claude pays for Claude turns, your Weave key pays for
9
9
  everything else.
@@ -34,9 +34,9 @@ router's dedicated headers:
34
34
  | `X-Weave-Anthropic-Subscription: <sk-ant-oat token>` | pays Claude turns, refreshed on expiry |
35
35
  | `X-Weave-Router-Key: rk_…` | from `opencode.json` `options.headers` — the router authenticates off this |
36
36
 
37
- The router routes the turn across every model the caller's subs + key can pay
37
+ The router routes each request across every model the caller's subs + key can pay
38
38
  for and resolves the subscription matching the chosen provider, so a sub is
39
- never billed for a turn outside its family.
39
+ never billed for a request outside its family.
40
40
 
41
41
  ## Two storage slots, one request provider
42
42
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "One-command installer that points Claude Code, Codex, opencode, or pi at the Weave Router. For pi it also ships the routing extension, loaded via pi.extensions.",
5
5
  "bin": {
6
- "weave-router": "bin.js"
6
+ "weave-router": "bin.js",
7
+ "router": "bin.js"
7
8
  },
8
9
  "pi": {
9
10
  "extensions": [
@@ -9,8 +9,8 @@ LLM proxy that picks the most cost-efficient model that still solves each task.
9
9
  Installed automatically by the Weave Router installer:
10
10
 
11
11
  ```bash
12
- WEAVE_ROUTER_KEY=rk_… npx -y @workweave/router --pi # user scope
13
- WEAVE_ROUTER_KEY=rk_… npx -y @workweave/router --pi --local # local router (http://localhost:8080)
12
+ WEAVE_ROUTER_KEY=rk_… npx --package @workweave/router -y -- weave-router --pi
13
+ WEAVE_ROUTER_KEY=rk_… npx --package @workweave/router -y -- weave-router --pi --local # local router
14
14
  ```
15
15
 
16
16
  That writes `~/.pi/agent/models.json` (the `weave` provider), adds
package/uninstall.sh CHANGED
@@ -213,6 +213,41 @@ if [ "$target" = "opencode" ]; then
213
213
  ok "Removed $opencode_parked"
214
214
  fi
215
215
 
216
+ # Remove slash command wrapper files this installer owns. Install mirrors
217
+ # this split: project scope uses <repo>/.opencode/commands/, while user
218
+ # scope—including a user-style --dir install—uses the global XDG commands
219
+ # directory so opencode discovers the wrappers from any working directory.
220
+ remove_opencode_command_dir() {
221
+ local opencode_cmds_dir="$1" cmd cmd_file
222
+ if [ -d "$opencode_cmds_dir" ]; then
223
+ refuse_if_symlink "$opencode_cmds_dir"
224
+ for cmd in force-model unforce-model router-feedback fm ufm rf; do
225
+ cmd_file="$opencode_cmds_dir/$cmd.md"
226
+ if [ -f "$cmd_file" ]; then
227
+ refuse_if_symlink "$cmd_file"
228
+ rm -f "$cmd_file"
229
+ ok "Removed $cmd_file"
230
+ fi
231
+ done
232
+ rmdir "$opencode_cmds_dir" 2>/dev/null || true
233
+ fi
234
+ }
235
+
236
+ if [ "$scope" = "project" ]; then
237
+ opencode_commands_dir="$opencode_dir/.opencode/commands"
238
+ else
239
+ opencode_commands_dir="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/commands"
240
+ fi
241
+ remove_opencode_command_dir "$opencode_commands_dir"
242
+
243
+ # Also clean the two paths used by earlier installer revisions, but avoid
244
+ # visiting the canonical path twice. This keeps uninstall idempotent for
245
+ # existing project and --dir installations.
246
+ for legacy_dir in "$opencode_dir/commands" "$opencode_dir/.opencode/commands"; do
247
+ [ "$legacy_dir" = "$opencode_commands_dir" ] && continue
248
+ remove_opencode_command_dir "$legacy_dir"
249
+ done
250
+
216
251
  if [ -n "$install_dir" ]; then
217
252
  ok "Weave Router uninstalled from $install_dir (opencode)."
218
253
  else
@@ -475,13 +510,13 @@ else
475
510
  fi
476
511
 
477
512
  if [ -f "$settings_file" ]; then
478
- # Only remove keys we actually installed: scrub our two env vars, and only
513
+ # Only remove keys we actually installed: scrub our env vars, and only
479
514
  # delete `statusLine` / `apiKeyHelper` when they point at scripts this
480
515
  # installer used in older versions. Otherwise an unrelated user-configured
481
516
  # statusLine or apiKeyHelper would be silently clobbered.
482
517
  cleaned="$(jq '
483
518
  if .env then
484
- .env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
519
+ .env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS, .ENABLE_TOOL_SEARCH))
485
520
  | (if (.env | length) == 0 then del(.env) else . end)
486
521
  else . end
487
522
  | (if (.statusLine.command // "" | tostring | endswith("cc-statusline.sh"))
@@ -501,7 +536,7 @@ if [ -n "$local_settings_file" ] && [ -f "$local_settings_file" ]; then
501
536
  # uninstall fully reverts a toggled-off install.
502
537
  cleaned="$(jq '
503
538
  if .env then
504
- .env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
539
+ .env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS, .ENABLE_TOOL_SEARCH))
505
540
  | (if (.env | length) == 0 then del(.env) else . end)
506
541
  else . end
507
542
  | del(.apiKeyHelper)
@@ -531,7 +566,7 @@ done
531
566
  commands_dir="$(dirname "$settings_file")/commands"
532
567
  if [ -d "$commands_dir" ]; then
533
568
  refuse_if_symlink "$commands_dir"
534
- for cmd in force-model unforce-model router-feedback fm ufm rf router-off router-on router-status; do
569
+ for cmd in force-model unforce-model router-feedback fm ufm rf router-off router-on router-status router-session; do
535
570
  cmd_file="$commands_dir/$cmd.md"
536
571
  if [ -f "$cmd_file" ]; then
537
572
  refuse_if_symlink "$cmd_file"