@workweave/router 0.2.7 → 0.2.8
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 +16 -4
- package/cc-statusline.sh +8 -4
- package/codex-skills/disable-routing/SKILL.md +22 -0
- package/install.sh +153 -23
- package/package.json +2 -1
- package/pi-router/README.md +9 -5
- package/pi-router/src/compaction.ts +144 -20
- package/pi-router/src/config.ts +3 -2
- package/pi-router/src/index.ts +3 -3
- package/pi-router/src/pricing.generated.ts +5 -3
- package/pi-router/src/ui.ts +15 -4
- package/uninstall.sh +22 -0
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ npx @workweave/router status --codex # is Codex on the router or direct?
|
|
|
32
32
|
Claude Code reads its router setting at launch, so quit and reopen it after an
|
|
33
33
|
on/off. Codex and opencode pick it up on their next run. Inside Claude Code the
|
|
34
34
|
slash commands `/router-off`, `/router-on`, and `/router-status` do the same.
|
|
35
|
+
Codex installs a `$disable-routing` skill that switches its next session back
|
|
36
|
+
to the normal provider; Codex does not support third-party `/disable-routing`
|
|
37
|
+
slash commands. The shell equivalent is `npx @workweave/router disable-routing`.
|
|
35
38
|
Cursor has no config file we own — toggle its base URL override in **Settings →
|
|
36
39
|
Models** instead.
|
|
37
40
|
|
|
@@ -62,10 +65,19 @@ Four install targets:
|
|
|
62
65
|
api.anthropic.com.
|
|
63
66
|
- **Codex** (`--codex`) — patches `~/.codex/config.toml` (or
|
|
64
67
|
`<repo>/.codex/config.toml`) with a managed `[model_providers.weave]`
|
|
65
|
-
block plus `model_provider = "weave"`.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
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
|
|
72
|
+
`gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` turns use that plan;
|
|
73
|
+
every other selected model uses its WorkWeave deployment or BYOK credential.
|
|
74
|
+
The block lives between begin/end markers
|
|
75
|
+
so re-running the installer rewrites it cleanly and `--uninstall --codex`
|
|
76
|
+
removes it without touching the rest of your config. Codex does not load
|
|
77
|
+
third-party slash-command files; to send a router directive, type it with
|
|
78
|
+
one leading space (for example, ` /force-model gpt-5.6-terra`). Its
|
|
79
|
+
`$disable-routing` skill returns the next Codex session to the default
|
|
80
|
+
provider without logging out or deleting the router configuration.
|
|
69
81
|
- **opencode** (`--opencode`) — merges a `provider.weave` entry (backed by
|
|
70
82
|
opencode's built-in `@ai-sdk/anthropic` provider) into
|
|
71
83
|
`~/.config/opencode/opencode.json` (or `<repo>/opencode.json` with
|
package/cc-statusline.sh
CHANGED
|
@@ -164,7 +164,7 @@ prices='{
|
|
|
164
164
|
"claude-sonnet-4-6": 0.003,
|
|
165
165
|
"claude-sonnet-5": 0.003,
|
|
166
166
|
"deepseek/deepseek-v4-flash": 0.0001134,
|
|
167
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
167
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
168
168
|
"gemini-2.0-flash": 0.0001,
|
|
169
169
|
"gemini-2.0-flash-lite": 0.000075,
|
|
170
170
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -199,6 +199,7 @@ prices='{
|
|
|
199
199
|
"gpt-5.6-sol": 0.005,
|
|
200
200
|
"gpt-5.6-terra": 0.0025,
|
|
201
201
|
"grok-4.5": 0.002,
|
|
202
|
+
"grok-4.6": 0.002,
|
|
202
203
|
"minimax/minimax-m2.7": 0.0003,
|
|
203
204
|
"minimax/minimax-m3": 0.0003,
|
|
204
205
|
"mistralai/mistral-small-2603": 0.0002,
|
|
@@ -212,8 +213,9 @@ prices='{
|
|
|
212
213
|
"qwen/qwen3-coder-next": 0.0005,
|
|
213
214
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
214
215
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
215
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
216
|
+
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
216
217
|
"qwen/qwen3.7-plus": 0.0004,
|
|
218
|
+
"qwen/qwen3.8-max": 0.002,
|
|
217
219
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
218
220
|
"z-ai/glm-5": 0.001,
|
|
219
221
|
"z-ai/glm-5.1": 0.0014,
|
|
@@ -233,7 +235,7 @@ prices='{
|
|
|
233
235
|
"claude-sonnet-4-6": 0.015,
|
|
234
236
|
"claude-sonnet-5": 0.015,
|
|
235
237
|
"deepseek/deepseek-v4-flash": 0.0002791,
|
|
236
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
238
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
237
239
|
"gemini-2.0-flash": 0.0004,
|
|
238
240
|
"gemini-2.0-flash-lite": 0.0003,
|
|
239
241
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -268,6 +270,7 @@ prices='{
|
|
|
268
270
|
"gpt-5.6-sol": 0.03,
|
|
269
271
|
"gpt-5.6-terra": 0.015,
|
|
270
272
|
"grok-4.5": 0.006,
|
|
273
|
+
"grok-4.6": 0.006,
|
|
271
274
|
"minimax/minimax-m2.7": 0.0012,
|
|
272
275
|
"minimax/minimax-m3": 0.0012,
|
|
273
276
|
"mistralai/mistral-small-2603": 0.0006,
|
|
@@ -281,8 +284,9 @@ prices='{
|
|
|
281
284
|
"qwen/qwen3-coder-next": 0.0012,
|
|
282
285
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
283
286
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
284
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
287
|
+
"qwen/qwen3.6-35b-a3b": 0.001,
|
|
285
288
|
"qwen/qwen3.7-plus": 0.0016,
|
|
289
|
+
"qwen/qwen3.8-max": 0.006,
|
|
286
290
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
287
291
|
"z-ai/glm-5": 0.0032,
|
|
288
292
|
"z-ai/glm-5.1": 0.0044,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: disable-routing
|
|
3
|
+
description: "Switch Codex back to its normal provider for future sessions."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- weave-router managed disable-routing skill -->
|
|
7
|
+
|
|
8
|
+
# Disable Weave routing
|
|
9
|
+
|
|
10
|
+
When the user invokes `$disable-routing`, switch the current Codex installation
|
|
11
|
+
off the Weave Router without logging out or deleting its router configuration.
|
|
12
|
+
|
|
13
|
+
1. Explain that the change takes effect on the next `codex` launch and that it
|
|
14
|
+
can later be reversed with `npx --package @workweave/router -y -- weave-router on --codex{{SCOPE}}`.
|
|
15
|
+
2. Run exactly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx --package @workweave/router -y -- weave-router off --codex{{SCOPE}}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
3. Report the command result. Do not uninstall the router or alter any other
|
|
22
|
+
Codex settings.
|
package/install.sh
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
# npx @workweave/router off --claude # route directly to Anthropic again (Claude Code)
|
|
53
53
|
# npx @workweave/router on --codex # route through the Weave Router again (Codex)
|
|
54
54
|
# npx @workweave/router status --opencode # report whether opencode is on the router or direct
|
|
55
|
+
# npx @workweave/router disable-routing # Codex shortcut: use Codex's normal provider again
|
|
55
56
|
# Claude Code reads env at launch, so an off/on takes effect on the next
|
|
56
57
|
# `claude` start; Codex and opencode re-read config every invocation.
|
|
57
58
|
# Cursor's base URL lives in its own settings UI (no file we own), so there's
|
|
@@ -61,8 +62,9 @@ set -euo pipefail
|
|
|
61
62
|
|
|
62
63
|
# ---------- defaults ----------
|
|
63
64
|
|
|
64
|
-
# The hosted Weave Router URL. Override with --base-url for self-hosted.
|
|
65
|
-
|
|
65
|
+
# The public hosted Weave Router URL. Override with --base-url for self-hosted.
|
|
66
|
+
HOSTED_BASE_URL="https://router.workweave.ai"
|
|
67
|
+
DEFAULT_BASE_URL="${WEAVE_ROUTER_URL:-$HOSTED_BASE_URL}"
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
scope="user"
|
|
@@ -88,6 +90,7 @@ target_explicit="false"
|
|
|
88
90
|
# "status" toggle or report an existing install without touching the router
|
|
89
91
|
# key/identity — see the toggle_* helpers and the dispatch block below.
|
|
90
92
|
mode="install"
|
|
93
|
+
disable_routing_alias="false"
|
|
91
94
|
|
|
92
95
|
# ---------- helpers ----------
|
|
93
96
|
|
|
@@ -297,6 +300,7 @@ refuse_if_symlink() {
|
|
|
297
300
|
# --codex) can find and replace the block instead of duplicating it.
|
|
298
301
|
WEAVE_CODEX_BEGIN_MARKER="# >>> weave-router managed (do not edit between markers) >>>"
|
|
299
302
|
WEAVE_CODEX_END_MARKER="# <<< weave-router managed <<<"
|
|
303
|
+
WEAVE_CODEX_SKILL_MARKER="<!-- weave-router managed disable-routing skill -->"
|
|
300
304
|
|
|
301
305
|
# ---------- identity helpers ----------
|
|
302
306
|
#
|
|
@@ -415,8 +419,12 @@ resolve_user_email() {
|
|
|
415
419
|
|
|
416
420
|
# write_codex_config writes a managed [model_providers.weave] block to the
|
|
417
421
|
# Codex CLI's config.toml. Sets `model_provider = "weave"` at the top level so
|
|
418
|
-
# Codex picks the routed provider by default.
|
|
419
|
-
#
|
|
422
|
+
# Codex picks the routed provider by default. The provider requires OpenAI
|
|
423
|
+
# authentication, preserving the user's ChatGPT plan credential while the
|
|
424
|
+
# router key is sent independently. The router applies OAuth only to its
|
|
425
|
+
# native gpt-5.6 Sol/Terra/Luna family; all other routed models use WorkWeave
|
|
426
|
+
# deployment or BYOK credentials. Both settings live inside the managed-block
|
|
427
|
+
# markers so uninstall removes them cleanly. We strip any
|
|
420
428
|
# top-level `model_provider = ...` declaration OUTSIDE the markers before
|
|
421
429
|
# appending so the file doesn't end up with a duplicate key (TOML rejects
|
|
422
430
|
# that). Inline `model_provider` keys inside `[profiles.*]` sections stay
|
|
@@ -462,6 +470,14 @@ write_codex_config() {
|
|
|
462
470
|
# that share the same router key. The router otherwise has to guess from
|
|
463
471
|
# User-Agent.
|
|
464
472
|
headers_parts="${headers_parts}, \"X-App\" = \"codex\""
|
|
473
|
+
# Match the public hosted Claude Code installation: Codex needs this
|
|
474
|
+
# explicit policy selection because that endpoint otherwise uses its
|
|
475
|
+
# cluster default. Self-hosted endpoints may not run the optional HMM
|
|
476
|
+
# sidecar, so every custom URL deliberately keeps the router's own default.
|
|
477
|
+
# Keep this inside the managed block so a re-install can change it safely.
|
|
478
|
+
if [ "$block_url" = "$HOSTED_BASE_URL" ]; then
|
|
479
|
+
headers_parts="${headers_parts}, \"X-Weave-Router-Strategy\" = \"hmm\""
|
|
480
|
+
fi
|
|
465
481
|
local headers_line="http_headers = { ${headers_parts} }"
|
|
466
482
|
|
|
467
483
|
local block
|
|
@@ -476,6 +492,7 @@ model_provider = "weave"
|
|
|
476
492
|
name = "Weave Router"
|
|
477
493
|
base_url = "${esc_url}/v1"
|
|
478
494
|
wire_api = "responses"
|
|
495
|
+
requires_openai_auth = true
|
|
479
496
|
${headers_line}
|
|
480
497
|
${WEAVE_CODEX_END_MARKER}
|
|
481
498
|
TOML
|
|
@@ -774,7 +791,8 @@ write_pi_models_config() {
|
|
|
774
791
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 64000 },
|
|
775
792
|
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 32000 },
|
|
776
793
|
{ 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 }
|
|
794
|
+
{ id: "grok-4.5", name: "Grok 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 500000, maxTokens: 131072 },
|
|
795
|
+
{ id: "grok-4.6", name: "Grok 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 500000, maxTokens: 131072 }
|
|
778
796
|
]
|
|
779
797
|
}
|
|
780
798
|
')"
|
|
@@ -944,6 +962,12 @@ while [ $# -gt 0 ]; do
|
|
|
944
962
|
# npm wrapper forwards argv verbatim so either form reaches us.
|
|
945
963
|
mode="${1#--}"; shift
|
|
946
964
|
;;
|
|
965
|
+
disable-routing|--disable-routing)
|
|
966
|
+
# Convenience alias for the Codex-specific off toggle. Unlike generic
|
|
967
|
+
# `off`, it deliberately resolves the target after all flags are parsed,
|
|
968
|
+
# so `disable-routing --claude` cannot silently change Claude settings.
|
|
969
|
+
mode="off"; disable_routing_alias="true"; shift
|
|
970
|
+
;;
|
|
947
971
|
-h|--help)
|
|
948
972
|
usage 0
|
|
949
973
|
;;
|
|
@@ -953,6 +977,15 @@ while [ $# -gt 0 ]; do
|
|
|
953
977
|
esac
|
|
954
978
|
done
|
|
955
979
|
|
|
980
|
+
if [ "$disable_routing_alias" = "true" ]; then
|
|
981
|
+
if [ "$target_explicit" = "true" ] && [ "$target" != "codex" ]; then
|
|
982
|
+
err "disable-routing only applies to Codex; omit the client flag or use --codex."
|
|
983
|
+
exit 2
|
|
984
|
+
fi
|
|
985
|
+
target="codex"
|
|
986
|
+
target_explicit="true"
|
|
987
|
+
fi
|
|
988
|
+
|
|
956
989
|
# Toggle verbs only flip config install.sh already wrote: no key, no identity,
|
|
957
990
|
# no prompts. Require an explicit client so we never guess which config to
|
|
958
991
|
# touch, and suppress every interactive prompt downstream.
|
|
@@ -1681,20 +1714,22 @@ else
|
|
|
1681
1714
|
info "No identity set — router traffic will be attributed by account UUID only."
|
|
1682
1715
|
fi
|
|
1683
1716
|
|
|
1684
|
-
# ---------- slash command wrappers (
|
|
1717
|
+
# ---------- slash command wrappers (Claude Code and opencode) ----------
|
|
1685
1718
|
#
|
|
1686
|
-
# Claude Code
|
|
1687
|
-
#
|
|
1688
|
-
#
|
|
1689
|
-
# Drop wrapper markdown files into the per-target commands directory so the
|
|
1690
|
-
# slash invocation expands locally into a literal "/force-model …" prompt that
|
|
1691
|
-
# the router's first-line parser picks up.
|
|
1719
|
+
# Claude Code and opencode expand command Markdown locally rather than sending
|
|
1720
|
+
# the literal invocation to the model, so they need wrappers that expand to a
|
|
1721
|
+
# router directive.
|
|
1692
1722
|
#
|
|
1693
1723
|
# Layout:
|
|
1694
1724
|
# Claude: <settings_dir>/commands/{force-model,unforce-model}.md → /force-model
|
|
1695
|
-
# Codex: <codex_dir>/prompts/{force-model,unforce-model}.md → /prompts:force-model
|
|
1696
1725
|
# opencode: <commands_dir>/{force-model,unforce-model}.md → /force-model
|
|
1697
1726
|
#
|
|
1727
|
+
# Codex intentionally is not listed: its slash menu only exposes built-in
|
|
1728
|
+
# commands and does not load these Markdown wrappers. A user can send a router
|
|
1729
|
+
# directive by starting it with one literal space (for example,
|
|
1730
|
+
# ` /force-model gpt-5.6-terra`); Codex submits the trimmed prompt to the
|
|
1731
|
+
# router without interpreting it as a local slash command.
|
|
1732
|
+
#
|
|
1698
1733
|
# Files come from install/commands/ in the repo (or the colocated commands/
|
|
1699
1734
|
# directory the npm package ships alongside install.sh).
|
|
1700
1735
|
install_slash_commands() {
|
|
@@ -1716,11 +1751,11 @@ install_slash_commands() {
|
|
|
1716
1751
|
fi
|
|
1717
1752
|
mkdir -p "$dst_dir"
|
|
1718
1753
|
|
|
1719
|
-
# force-model/unforce-model are router-intercepted prompt expansions
|
|
1720
|
-
#
|
|
1721
|
-
# installer to flip the *local* config, so they're Claude Code-only and
|
|
1722
|
-
# the install scope baked into the command (the .md can't discover it
|
|
1723
|
-
# invocation time). {{SCOPE}} is substituted accordingly.
|
|
1754
|
+
# force-model/unforce-model are router-intercepted prompt expansions for the
|
|
1755
|
+
# command-capable clients. The router-off/on/status wrappers shell out to
|
|
1756
|
+
# this installer to flip the *local* config, so they're Claude Code-only and
|
|
1757
|
+
# need the install scope baked into the command (the .md can't discover it
|
|
1758
|
+
# at invocation time). {{SCOPE}} is substituted accordingly.
|
|
1724
1759
|
installed="force-model, unforce-model, router-feedback, fm, ufm, rf"
|
|
1725
1760
|
cmds="force-model unforce-model router-feedback fm ufm rf"
|
|
1726
1761
|
if [ "$target" = "claude" ]; then
|
|
@@ -1755,12 +1790,103 @@ install_slash_commands() {
|
|
|
1755
1790
|
ok "Slash commands written to $dst_dir ($installed)"
|
|
1756
1791
|
}
|
|
1757
1792
|
|
|
1793
|
+
# Codex builds before custom prompt discovery existed were given wrapper files
|
|
1794
|
+
# under ~/.codex/prompts. They were never invocable as the advertised aliases.
|
|
1795
|
+
# Remove only byte-for-byte copies of our old wrappers; retain any prompt the
|
|
1796
|
+
# user has changed or created independently.
|
|
1797
|
+
remove_obsolete_codex_prompt_wrappers() {
|
|
1798
|
+
local dst_dir="$1"
|
|
1799
|
+
local commands_src_dir=""
|
|
1800
|
+
local candidate cmd src dst
|
|
1801
|
+
|
|
1802
|
+
[ -d "$dst_dir" ] || return 0
|
|
1803
|
+
for candidate in \
|
|
1804
|
+
"$script_dir/commands" \
|
|
1805
|
+
"$script_dir/../commands"
|
|
1806
|
+
do
|
|
1807
|
+
if [ -d "$candidate" ]; then
|
|
1808
|
+
commands_src_dir="$candidate"
|
|
1809
|
+
break
|
|
1810
|
+
fi
|
|
1811
|
+
done
|
|
1812
|
+
[ -n "$commands_src_dir" ] || return 0
|
|
1813
|
+
|
|
1814
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1815
|
+
refuse_if_symlink "$dst_dir"
|
|
1816
|
+
fi
|
|
1817
|
+
|
|
1818
|
+
for cmd in force-model unforce-model router-feedback fm ufm rf; do
|
|
1819
|
+
src="$commands_src_dir/$cmd.md"
|
|
1820
|
+
dst="$dst_dir/$cmd.md"
|
|
1821
|
+
[ -f "$src" ] && [ -f "$dst" ] || continue
|
|
1822
|
+
# A symlink is user-controlled at user scope. Leave it untouched; project
|
|
1823
|
+
# scope rejects it through the same guard used by normal installs.
|
|
1824
|
+
[ -L "$dst" ] && continue
|
|
1825
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1826
|
+
refuse_if_symlink "$dst"
|
|
1827
|
+
fi
|
|
1828
|
+
cmp -s "$src" "$dst" && rm -f "$dst"
|
|
1829
|
+
done
|
|
1830
|
+
rmdir "$dst_dir" 2>/dev/null || true
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
# Codex skills are the supported local extension surface for an action that
|
|
1834
|
+
# must edit Codex's configuration. A literal third-party slash command cannot
|
|
1835
|
+
# do this because Codex reserves `/…` for built-ins. The skill is invoked as
|
|
1836
|
+
# `$disable-routing`, asks Codex to run the existing safe toggle, and leaves
|
|
1837
|
+
# the managed router block in place for a later re-enable.
|
|
1838
|
+
install_codex_disable_routing_skill() {
|
|
1839
|
+
local skill_src=""
|
|
1840
|
+
local candidate dst_dir dst_file scope_args body
|
|
1841
|
+
for candidate in \
|
|
1842
|
+
"$script_dir/codex-skills/disable-routing/SKILL.md" \
|
|
1843
|
+
"$script_dir/../codex-skills/disable-routing/SKILL.md"
|
|
1844
|
+
do
|
|
1845
|
+
if [ -f "$candidate" ]; then
|
|
1846
|
+
skill_src="$candidate"
|
|
1847
|
+
break
|
|
1848
|
+
fi
|
|
1849
|
+
done
|
|
1850
|
+
[ -n "$skill_src" ] || { warn "Codex disable-routing skill template is missing; router configuration is still installed."; return 0; }
|
|
1851
|
+
grep -Fq "$WEAVE_CODEX_SKILL_MARKER" "$skill_src" \
|
|
1852
|
+
|| { warn "Codex disable-routing skill template has no ownership marker; leaving skills unchanged."; return 0; }
|
|
1853
|
+
|
|
1854
|
+
dst_dir="$codex_dir/skills/disable-routing"
|
|
1855
|
+
dst_file="$dst_dir/SKILL.md"
|
|
1856
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1857
|
+
refuse_if_symlink "$codex_dir/skills"
|
|
1858
|
+
refuse_if_symlink "$dst_dir"
|
|
1859
|
+
refuse_if_symlink "$dst_file"
|
|
1860
|
+
elif [ -L "$codex_dir/skills" ] || [ -L "$dst_dir" ] || [ -L "$dst_file" ]; then
|
|
1861
|
+
warn "Codex disable-routing skill path contains a symlink; leaving it untouched."
|
|
1862
|
+
return 0
|
|
1863
|
+
fi
|
|
1864
|
+
if [ -e "$dst_file" ] && { [ ! -f "$dst_file" ] || ! grep -Fq "$WEAVE_CODEX_SKILL_MARKER" "$dst_file"; }; then
|
|
1865
|
+
warn "A user-owned Codex disable-routing skill already exists at $dst_file; leaving it untouched."
|
|
1866
|
+
return 0
|
|
1867
|
+
fi
|
|
1868
|
+
mkdir -p "$dst_dir"
|
|
1869
|
+
|
|
1870
|
+
scope_args=""
|
|
1871
|
+
if [ -n "$install_dir" ]; then
|
|
1872
|
+
scope_args=" --dir $(printf '%q' "$install_dir")"
|
|
1873
|
+
elif [ "$scope" = "project" ]; then
|
|
1874
|
+
scope_args=" --scope project"
|
|
1875
|
+
fi
|
|
1876
|
+
body="$(<"$skill_src")"
|
|
1877
|
+
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
1878
|
+
printf '%s\n' "$body" >"$dst_file"
|
|
1879
|
+
ok "Codex skill installed: \$disable-routing"
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1758
1882
|
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
1759
1883
|
|
|
1760
1884
|
if [ "$target" = "codex" ]; then
|
|
1761
1885
|
write_codex_config "$codex_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
1762
1886
|
ok "Codex config written to $codex_config_file"
|
|
1763
|
-
|
|
1887
|
+
remove_obsolete_codex_prompt_wrappers "$codex_dir/prompts"
|
|
1888
|
+
install_codex_disable_routing_skill
|
|
1889
|
+
info "Codex router directives: begin the message with one space, e.g. ' /force-model gpt-5.6-terra'."
|
|
1764
1890
|
|
|
1765
1891
|
# Project scope: ensure the per-teammate config (which holds the router key)
|
|
1766
1892
|
# is gitignored. The base URL is the same for every teammate, so a
|
|
@@ -2130,7 +2256,7 @@ prices='{
|
|
|
2130
2256
|
"claude-sonnet-4-6": 0.003,
|
|
2131
2257
|
"claude-sonnet-5": 0.003,
|
|
2132
2258
|
"deepseek/deepseek-v4-flash": 0.0001134,
|
|
2133
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
2259
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
2134
2260
|
"gemini-2.0-flash": 0.0001,
|
|
2135
2261
|
"gemini-2.0-flash-lite": 0.000075,
|
|
2136
2262
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -2165,6 +2291,7 @@ prices='{
|
|
|
2165
2291
|
"gpt-5.6-sol": 0.005,
|
|
2166
2292
|
"gpt-5.6-terra": 0.0025,
|
|
2167
2293
|
"grok-4.5": 0.002,
|
|
2294
|
+
"grok-4.6": 0.002,
|
|
2168
2295
|
"minimax/minimax-m2.7": 0.0003,
|
|
2169
2296
|
"minimax/minimax-m3": 0.0003,
|
|
2170
2297
|
"mistralai/mistral-small-2603": 0.0002,
|
|
@@ -2178,8 +2305,9 @@ prices='{
|
|
|
2178
2305
|
"qwen/qwen3-coder-next": 0.0005,
|
|
2179
2306
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
2180
2307
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
2181
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
2308
|
+
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
2182
2309
|
"qwen/qwen3.7-plus": 0.0004,
|
|
2310
|
+
"qwen/qwen3.8-max": 0.002,
|
|
2183
2311
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
2184
2312
|
"z-ai/glm-5": 0.001,
|
|
2185
2313
|
"z-ai/glm-5.1": 0.0014,
|
|
@@ -2199,7 +2327,7 @@ prices='{
|
|
|
2199
2327
|
"claude-sonnet-4-6": 0.015,
|
|
2200
2328
|
"claude-sonnet-5": 0.015,
|
|
2201
2329
|
"deepseek/deepseek-v4-flash": 0.0002791,
|
|
2202
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
2330
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
2203
2331
|
"gemini-2.0-flash": 0.0004,
|
|
2204
2332
|
"gemini-2.0-flash-lite": 0.0003,
|
|
2205
2333
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -2234,6 +2362,7 @@ prices='{
|
|
|
2234
2362
|
"gpt-5.6-sol": 0.03,
|
|
2235
2363
|
"gpt-5.6-terra": 0.015,
|
|
2236
2364
|
"grok-4.5": 0.006,
|
|
2365
|
+
"grok-4.6": 0.006,
|
|
2237
2366
|
"minimax/minimax-m2.7": 0.0012,
|
|
2238
2367
|
"minimax/minimax-m3": 0.0012,
|
|
2239
2368
|
"mistralai/mistral-small-2603": 0.0006,
|
|
@@ -2247,8 +2376,9 @@ prices='{
|
|
|
2247
2376
|
"qwen/qwen3-coder-next": 0.0012,
|
|
2248
2377
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
2249
2378
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
2250
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
2379
|
+
"qwen/qwen3.6-35b-a3b": 0.001,
|
|
2251
2380
|
"qwen/qwen3.7-plus": 0.0016,
|
|
2381
|
+
"qwen/qwen3.8-max": 0.006,
|
|
2252
2382
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
2253
2383
|
"z-ai/glm-5": 0.0032,
|
|
2254
2384
|
"z-ai/glm-5.1": 0.0044,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workweave/router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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
6
|
"weave-router": "bin.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"uninstall.sh",
|
|
18
18
|
"cc-statusline.sh",
|
|
19
19
|
"commands/",
|
|
20
|
+
"codex-skills/",
|
|
20
21
|
"pi-router/",
|
|
21
22
|
"opencode-weave/",
|
|
22
23
|
"README.md",
|
package/pi-router/README.md
CHANGED
|
@@ -31,8 +31,12 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
31
31
|
The persistent status changes to `WEAVE ROUTER — <model> [forced]` after the
|
|
32
32
|
router validates and canonicalizes the requested model.
|
|
33
33
|
- **Per-process routing bias.** Static `x-weave-routing-*` knob headers bias the
|
|
34
|
-
router: quality on the main loop
|
|
35
|
-
|
|
34
|
+
router: quality on the main loop and speed + cheap on subagents.
|
|
35
|
+
- **Long tool-loop compaction.** Pi 0.74 can cross its context threshold inside
|
|
36
|
+
an uninterrupted tool loop before its normal post-run compaction check. The
|
|
37
|
+
extension preserves a usable output budget for the real continuation and
|
|
38
|
+
compacts once the loop settles, while leaving ordinary threshold compaction
|
|
39
|
+
to Pi.
|
|
36
40
|
- **Sticky sessions.** `metadata.user_id = "pi:<sessionId>"` pins the main loop
|
|
37
41
|
to one model for the session; subagents get their own pins.
|
|
38
42
|
- **`dispatch` tool — parallel, context-isolated subagents.** pi has none
|
|
@@ -65,7 +69,7 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
65
69
|
| `WEAVE_PI_ALLOW_SUBAGENT_TOOLS` | unset | `1` lets `dispatch` grant subagents write/exec tools (bash, write, edit); default strips them |
|
|
66
70
|
| `WEAVE_ROUTING_ALPHA` / `…_SPEED_WEIGHT` / `…_OUTPUT_COST_RATIO` / `…_EXPECTED_OUTPUT_TOKENS` | role preset | Override individual routing knobs (main process only — children always use their role preset) |
|
|
67
71
|
| `WEAVE_NO_SAFETY` | unset | `1` disables the catastrophic-bash gate |
|
|
68
|
-
| `
|
|
72
|
+
| `WEAVE_PI_AUTO_COMPACTION` | unset | `0` disables the routed tool-loop compaction safeguard |
|
|
69
73
|
|
|
70
74
|
Internal: `WEAVE_PI_SUBAGENT=1` and `WEAVE_PI_SUBAGENT_ID` are set by `dispatch`
|
|
71
75
|
on child processes; don't set them yourself.
|
|
@@ -83,5 +87,5 @@ version with each response so resumed totals remain auditable.
|
|
|
83
87
|
|
|
84
88
|
## Notes
|
|
85
89
|
|
|
86
|
-
-
|
|
87
|
-
|
|
90
|
+
- Actual SDK/router probes keep their small output budget. Only a probe-sized
|
|
91
|
+
request carrying a real tool-result continuation is repaired.
|
|
@@ -1,26 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Compaction safeguards for Pi's routed provider.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Pi 0.74 checks automatic compaction only after an entire agent loop. A long
|
|
5
|
+
* tool loop can therefore cross the virtual model's context window between
|
|
6
|
+
* turns. Pi then clamps the next request to max_tokens=1, which looks exactly
|
|
7
|
+
* like an SDK quota probe to the router and produces a one-token response.
|
|
8
|
+
*
|
|
9
|
+
* Preserve real probes, but restore a usable budget for a clamped request that
|
|
10
|
+
* contains an actual tool result. Once the agent finishes, compact using the
|
|
11
|
+
* highest context reading seen during the run; routed providers tokenize and
|
|
12
|
+
* report caches differently, so the final response alone is not authoritative.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
AgentEndEvent,
|
|
17
|
+
BeforeProviderRequestEvent,
|
|
18
|
+
ExtensionAPI,
|
|
19
|
+
ExtensionContext,
|
|
20
|
+
SessionCompactEvent,
|
|
21
|
+
TurnEndEvent,
|
|
22
|
+
} from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
24
|
+
|
|
25
|
+
const PROBE_MAX_TOKENS = 4;
|
|
26
|
+
const CONTINUATION_MAX_TOKENS = 16_384;
|
|
27
|
+
const COMPACTION_RESERVE_TOKENS = 16_384;
|
|
28
|
+
const STATUS_KEY = "weave-compaction";
|
|
29
|
+
|
|
30
|
+
interface ProviderPayload {
|
|
31
|
+
max_tokens?: unknown;
|
|
32
|
+
messages?: unknown;
|
|
33
|
+
tools?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ProviderMessage {
|
|
37
|
+
content?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Schedule = (callback: () => void) => unknown;
|
|
41
|
+
|
|
42
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === "object" && value !== null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function containsToolResult(messages: unknown): boolean {
|
|
47
|
+
if (!Array.isArray(messages)) return false;
|
|
48
|
+
return messages.some((message: unknown) => {
|
|
49
|
+
if (!isRecord(message)) return false;
|
|
50
|
+
const content = (message as ProviderMessage).content;
|
|
51
|
+
return Array.isArray(content) && content.some((block: unknown) => isRecord(block) && block.type === "tool_result");
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Repair only the Pi context-exhaustion shape. Genuine SDK probes have no tool
|
|
57
|
+
* result transcript and remain at max_tokens=1..4 for the router to hard-pin.
|
|
12
58
|
*/
|
|
59
|
+
export function repairClampedToolContinuation(payload: unknown): boolean {
|
|
60
|
+
if (!isRecord(payload)) return false;
|
|
61
|
+
const body = payload as ProviderPayload;
|
|
62
|
+
if (typeof body.max_tokens !== "number" || body.max_tokens < 1 || body.max_tokens > PROBE_MAX_TOKENS) return false;
|
|
63
|
+
if (!Array.isArray(body.tools) || body.tools.length === 0) return false;
|
|
64
|
+
if (!containsToolResult(body.messages)) return false;
|
|
65
|
+
body.max_tokens = CONTINUATION_MAX_TOKENS;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function contextTokens(message: AssistantMessage): number {
|
|
70
|
+
const usage = message.usage;
|
|
71
|
+
if (usage.totalTokens > 0) return usage.totalTokens;
|
|
72
|
+
return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function latestCompactionId(ctx: ExtensionContext): string | undefined {
|
|
76
|
+
const branch = ctx.sessionManager.getBranch();
|
|
77
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
78
|
+
const entry = branch[i];
|
|
79
|
+
if (entry?.type === "compaction") return entry.id;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function registerCompaction(pi: ExtensionAPI, schedule: Schedule = (callback) => setTimeout(callback, 0)): void {
|
|
85
|
+
let highWaterTokens = 0;
|
|
86
|
+
let lastTurnTokens = 0;
|
|
87
|
+
let repairedContinuation = false;
|
|
88
|
+
let compactionScheduled = false;
|
|
89
|
+
|
|
90
|
+
const resetRun = () => {
|
|
91
|
+
highWaterTokens = 0;
|
|
92
|
+
lastTurnTokens = 0;
|
|
93
|
+
repairedContinuation = false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const finishCompaction = (ctx: ExtensionContext) => {
|
|
97
|
+
compactionScheduled = false;
|
|
98
|
+
resetRun();
|
|
99
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
pi.on("session_start", resetRun);
|
|
103
|
+
pi.on("agent_start", resetRun);
|
|
104
|
+
pi.on("session_compact", (_event: SessionCompactEvent, ctx: ExtensionContext) => finishCompaction(ctx));
|
|
105
|
+
|
|
106
|
+
pi.on("before_provider_request", (event: BeforeProviderRequestEvent) => {
|
|
107
|
+
if (repairClampedToolContinuation(event.payload)) repairedContinuation = true;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
pi.on("turn_end", (event: TurnEndEvent) => {
|
|
111
|
+
if (event.message.role !== "assistant") return;
|
|
112
|
+
lastTurnTokens = contextTokens(event.message as AssistantMessage);
|
|
113
|
+
highWaterTokens = Math.max(highWaterTokens, lastTurnTokens);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.on("agent_end", (_event: AgentEndEvent, ctx: ExtensionContext) => {
|
|
117
|
+
if (process.env.WEAVE_PI_AUTO_COMPACTION === "0" || compactionScheduled) return;
|
|
118
|
+
const contextWindow = ctx.model?.contextWindow ?? ctx.getContextUsage()?.contextWindow ?? 0;
|
|
119
|
+
if (contextWindow <= COMPACTION_RESERVE_TOKENS) return;
|
|
120
|
+
const threshold = contextWindow - COMPACTION_RESERVE_TOKENS;
|
|
121
|
+
// Pi's built-in check runs immediately after this event and owns the
|
|
122
|
+
// ordinary final-turn threshold case. Starting another compaction while
|
|
123
|
+
// that async summary is in flight would race it.
|
|
124
|
+
if (lastTurnTokens > threshold) return;
|
|
125
|
+
if (!repairedContinuation && highWaterTokens <= threshold) return;
|
|
126
|
+
|
|
127
|
+
compactionScheduled = true;
|
|
128
|
+
const previousCompactionId = latestCompactionId(ctx);
|
|
13
129
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
130
|
+
// Pi is still settling agent_end while extension handlers run. Schedule
|
|
131
|
+
// manual compaction for the next event-loop turn to avoid abort/wait
|
|
132
|
+
// recursion, and let Pi's own auto-compaction win if it already ran.
|
|
133
|
+
schedule(() => {
|
|
134
|
+
if (!compactionScheduled) return;
|
|
135
|
+
const currentCompactionId = latestCompactionId(ctx);
|
|
136
|
+
if (currentCompactionId !== previousCompactionId) {
|
|
137
|
+
finishCompaction(ctx);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, "compacting routed context...");
|
|
141
|
+
ctx.compact({
|
|
142
|
+
onComplete: () => finishCompaction(ctx),
|
|
143
|
+
onError: (error) => {
|
|
144
|
+
compactionScheduled = false;
|
|
145
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, `compaction failed: ${error.message}`);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
25
149
|
});
|
|
26
150
|
}
|
package/pi-router/src/config.ts
CHANGED
|
@@ -225,9 +225,10 @@ export const WEAVE_MODELS: ProviderModelConfig[] = [
|
|
|
225
225
|
model("claude-opus-4-7", "Claude Opus 4.7 (via Weave Router)", 64000),
|
|
226
226
|
model("claude-sonnet-4-6", "Claude Sonnet 4.6 (via Weave Router)", 64000),
|
|
227
227
|
model("claude-haiku-4-5", "Claude Haiku 4.5 (via Weave Router)", 32000),
|
|
228
|
+
model("grok-4.6", "Grok 4.6 (via Weave Router)", 131072, 500000),
|
|
228
229
|
];
|
|
229
230
|
|
|
230
|
-
function model(id: string, name: string, maxTokens: number): ProviderModelConfig {
|
|
231
|
+
function model(id: string, name: string, maxTokens: number, contextWindow: number = 200000): ProviderModelConfig {
|
|
231
232
|
return {
|
|
232
233
|
id,
|
|
233
234
|
name,
|
|
@@ -235,7 +236,7 @@ function model(id: string, name: string, maxTokens: number): ProviderModelConfig
|
|
|
235
236
|
input: ["text", "image"],
|
|
236
237
|
// Real cost is decided by the router per request and is unknown client-side.
|
|
237
238
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
238
|
-
contextWindow
|
|
239
|
+
contextWindow,
|
|
239
240
|
maxTokens,
|
|
240
241
|
};
|
|
241
242
|
}
|
package/pi-router/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* detection.
|
|
10
10
|
* - Loom UI: branded header, Wooly animation, actual route, and saved $.
|
|
11
11
|
* - safety: block catastrophic bash (unless WEAVE_NO_SAFETY=1).
|
|
12
|
-
* - compaction:
|
|
12
|
+
* - compaction: protect long tool loops, then compact routed context.
|
|
13
13
|
* - dispatch: parallel, context-isolated subagents — top-level process
|
|
14
14
|
* only (no grandchildren).
|
|
15
15
|
*
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
22
22
|
import { isSubagent } from "./config.js";
|
|
23
|
-
import {
|
|
23
|
+
import { registerCompaction } from "./compaction.js";
|
|
24
24
|
import { registerDispatch } from "./dispatch.js";
|
|
25
25
|
import { registerForceModelCommands } from "./force-model.js";
|
|
26
26
|
import { registerMetadata } from "./metadata.js";
|
|
@@ -40,9 +40,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
40
40
|
registerMetadata(pi);
|
|
41
41
|
registerForceModelCommands(pi);
|
|
42
42
|
registerRoutedModel(pi);
|
|
43
|
+
registerCompaction(pi);
|
|
43
44
|
|
|
44
45
|
if (process.env.WEAVE_NO_SAFETY !== "1") registerSafety(pi);
|
|
45
|
-
if (process.env.WEAVE_CHEAP_COMPACTION === "1") registerCheapCompaction(pi);
|
|
46
46
|
|
|
47
47
|
// Only the top-level process fans out. Children (WEAVE_PI_SUBAGENT=1) load
|
|
48
48
|
// this same extension but get no dispatch tool, so subagents can't spawn
|
|
@@ -6,7 +6,7 @@ export interface ModelPricing {
|
|
|
6
6
|
outputUsdPerMillion: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export const PRICING_VERSION = "catalog-sha256:
|
|
9
|
+
export const PRICING_VERSION = "catalog-sha256:f2841a252235d113";
|
|
10
10
|
|
|
11
11
|
export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = Object.freeze({
|
|
12
12
|
"claude-fable-5": { inputUsdPerMillion: 10, outputUsdPerMillion: 50 },
|
|
@@ -22,7 +22,7 @@ export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = Object.free
|
|
|
22
22
|
"claude-sonnet-4-6": { inputUsdPerMillion: 3, outputUsdPerMillion: 15 },
|
|
23
23
|
"claude-sonnet-5": { inputUsdPerMillion: 3, outputUsdPerMillion: 15 },
|
|
24
24
|
"deepseek/deepseek-v4-flash": { inputUsdPerMillion: 0.1134, outputUsdPerMillion: 0.2791 },
|
|
25
|
-
"deepseek/deepseek-v4-pro": { inputUsdPerMillion: 1.
|
|
25
|
+
"deepseek/deepseek-v4-pro": { inputUsdPerMillion: 1.74, outputUsdPerMillion: 3.48 },
|
|
26
26
|
"gemini-2.0-flash": { inputUsdPerMillion: 0.1, outputUsdPerMillion: 0.4 },
|
|
27
27
|
"gemini-2.0-flash-lite": { inputUsdPerMillion: 0.075, outputUsdPerMillion: 0.3 },
|
|
28
28
|
"gemini-2.5-flash": { inputUsdPerMillion: 0.3, outputUsdPerMillion: 1.2 },
|
|
@@ -57,6 +57,7 @@ export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = Object.free
|
|
|
57
57
|
"gpt-5.6-sol": { inputUsdPerMillion: 5, outputUsdPerMillion: 30 },
|
|
58
58
|
"gpt-5.6-terra": { inputUsdPerMillion: 2.5, outputUsdPerMillion: 15 },
|
|
59
59
|
"grok-4.5": { inputUsdPerMillion: 2, outputUsdPerMillion: 6 },
|
|
60
|
+
"grok-4.6": { inputUsdPerMillion: 2, outputUsdPerMillion: 6 },
|
|
60
61
|
"minimax/minimax-m2.7": { inputUsdPerMillion: 0.3, outputUsdPerMillion: 1.2 },
|
|
61
62
|
"minimax/minimax-m3": { inputUsdPerMillion: 0.3, outputUsdPerMillion: 1.2 },
|
|
62
63
|
"mistralai/mistral-small-2603": { inputUsdPerMillion: 0.2, outputUsdPerMillion: 0.6 },
|
|
@@ -70,8 +71,9 @@ export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = Object.free
|
|
|
70
71
|
"qwen/qwen3-coder-next": { inputUsdPerMillion: 0.5, outputUsdPerMillion: 1.2 },
|
|
71
72
|
"qwen/qwen3-next-80b-a3b-instruct": { inputUsdPerMillion: 0.15, outputUsdPerMillion: 1.2 },
|
|
72
73
|
"qwen/qwen3.5-flash-02-23": { inputUsdPerMillion: 0.05, outputUsdPerMillion: 0.15 },
|
|
73
|
-
"qwen/qwen3.6-35b-a3b": { inputUsdPerMillion: 0.
|
|
74
|
+
"qwen/qwen3.6-35b-a3b": { inputUsdPerMillion: 0.15, outputUsdPerMillion: 1 },
|
|
74
75
|
"qwen/qwen3.7-plus": { inputUsdPerMillion: 0.4, outputUsdPerMillion: 1.6 },
|
|
76
|
+
"qwen/qwen3.8-max": { inputUsdPerMillion: 2, outputUsdPerMillion: 6 },
|
|
75
77
|
"xiaomi/mimo-v2.5-pro": { inputUsdPerMillion: 1, outputUsdPerMillion: 3 },
|
|
76
78
|
"z-ai/glm-5": { inputUsdPerMillion: 1, outputUsdPerMillion: 3.2 },
|
|
77
79
|
"z-ai/glm-5.1": { inputUsdPerMillion: 1.4, outputUsdPerMillion: 4.4 },
|
package/pi-router/src/ui.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface RouterUiSnapshot {
|
|
|
26
26
|
savings: SavingsAggregate;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
type LegacyCompatibleExtensionContext = ExtensionContext & { mode?: string };
|
|
30
|
+
|
|
31
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
32
|
+
const mode = (ctx as LegacyCompatibleExtensionContext).mode;
|
|
33
|
+
// Pi 0.74.2 supports headers and widgets but predates ctx.mode. In that
|
|
34
|
+
// release hasUI is true only for the interactive TUI, so it is the safe
|
|
35
|
+
// compatibility signal. Newer Pi releases expose mode and take precedence.
|
|
36
|
+
return mode === "tui" || (mode === undefined && ctx.hasUI);
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
function brand(text: string): string {
|
|
30
40
|
return `${BRAND_OPEN}${text}${BRAND_CLOSE}`;
|
|
31
41
|
}
|
|
@@ -45,7 +55,7 @@ function headerLines(theme: Theme, width: number): string[] {
|
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export function installLoomUi(ctx: ExtensionContext): void {
|
|
48
|
-
if (ctx
|
|
58
|
+
if (!isTuiContext(ctx)) return;
|
|
49
59
|
ctx.ui.setTitle("Loom · Weave Router");
|
|
50
60
|
ctx.ui.setHeader((_tui: TUI, theme: Theme) => ({
|
|
51
61
|
invalidate() {},
|
|
@@ -59,7 +69,7 @@ export function installLoomUi(ctx: ExtensionContext): void {
|
|
|
59
69
|
export function clearLoomUi(ctx: ExtensionContext): void {
|
|
60
70
|
if (!ctx.hasUI) return;
|
|
61
71
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
62
|
-
if (ctx
|
|
72
|
+
if (isTuiContext(ctx)) {
|
|
63
73
|
ctx.ui.setWidget(WOOLY_WIDGET_KEY, undefined);
|
|
64
74
|
ctx.ui.setHeader(undefined);
|
|
65
75
|
}
|
|
@@ -74,7 +84,8 @@ export function updateRouterStatus(ctx: ExtensionContext, snapshot: RouterUiSnap
|
|
|
74
84
|
else if (requestedModel) route = `${requestedModel} · awaiting route`;
|
|
75
85
|
else route = "automatic routing";
|
|
76
86
|
|
|
77
|
-
const
|
|
87
|
+
const tui = isTuiContext(ctx);
|
|
88
|
+
const label = tui ? ctx.ui.theme.bold(brand("WEAVE ROUTER")) : "WEAVE ROUTER";
|
|
78
89
|
const detail = forcedModel ? `${forcedModel} [forced]` : `${route} · ${formatSavings(savings)}`;
|
|
79
|
-
ctx.ui.setStatus(STATUS_KEY, `${label} — ${
|
|
90
|
+
ctx.ui.setStatus(STATUS_KEY, `${label} — ${tui ? ctx.ui.theme.fg("dim", detail) : detail}`);
|
|
80
91
|
}
|
package/uninstall.sh
CHANGED
|
@@ -30,6 +30,7 @@ install_dir=""
|
|
|
30
30
|
target="claude"
|
|
31
31
|
|
|
32
32
|
err() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; }
|
|
33
|
+
warn() { printf "\033[33mwarning:\033[0m %s\n" "$*" >&2; }
|
|
33
34
|
info() { printf "\033[36m==>\033[0m %s\n" "$*"; }
|
|
34
35
|
ok() { printf "\033[32m✓\033[0m %s\n" "$*"; }
|
|
35
36
|
|
|
@@ -91,6 +92,7 @@ fi
|
|
|
91
92
|
# Markers must stay in sync with install.sh. Keep verbatim.
|
|
92
93
|
WEAVE_CODEX_BEGIN_MARKER="# >>> weave-router managed (do not edit between markers) >>>"
|
|
93
94
|
WEAVE_CODEX_END_MARKER="# <<< weave-router managed <<<"
|
|
95
|
+
WEAVE_CODEX_SKILL_MARKER="<!-- weave-router managed disable-routing skill -->"
|
|
94
96
|
|
|
95
97
|
# strip_codex_block rewrites config.toml without the managed block and any
|
|
96
98
|
# top-level `model_provider = "weave"` that lived outside the markers (which
|
|
@@ -436,6 +438,26 @@ if [ "$target" = "codex" ]; then
|
|
|
436
438
|
rmdir "$codex_prompts_dir" 2>/dev/null || true
|
|
437
439
|
fi
|
|
438
440
|
|
|
441
|
+
# The installer-owned skill switches Codex back to its normal provider. It
|
|
442
|
+
# is separate from config.toml, so remove it when removing the router too.
|
|
443
|
+
# A same-named skill without our marker belongs to the user and is preserved.
|
|
444
|
+
codex_skill_dir="$codex_dir/skills/disable-routing"
|
|
445
|
+
codex_skill_file="$codex_skill_dir/SKILL.md"
|
|
446
|
+
if [ -d "$codex_skill_dir" ]; then
|
|
447
|
+
refuse_if_symlink "$codex_skill_dir"
|
|
448
|
+
if [ -f "$codex_skill_file" ]; then
|
|
449
|
+
refuse_if_symlink "$codex_skill_file"
|
|
450
|
+
if grep -Fq "$WEAVE_CODEX_SKILL_MARKER" "$codex_skill_file"; then
|
|
451
|
+
rm -f "$codex_skill_file"
|
|
452
|
+
ok "Removed $codex_skill_file"
|
|
453
|
+
else
|
|
454
|
+
warn "Leaving user-owned Codex skill at $codex_skill_file untouched."
|
|
455
|
+
fi
|
|
456
|
+
fi
|
|
457
|
+
rmdir "$codex_skill_dir" 2>/dev/null || true
|
|
458
|
+
rmdir "$codex_dir/skills" 2>/dev/null || true
|
|
459
|
+
fi
|
|
460
|
+
|
|
439
461
|
if [ -n "$install_dir" ]; then
|
|
440
462
|
ok "Weave Router uninstalled from $install_dir (Codex)."
|
|
441
463
|
else
|