@workweave/router 0.2.6 → 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 +29 -9
- package/cc-statusline.sh +68 -6
- package/codex-skills/disable-routing/SKILL.md +22 -0
- package/install.sh +224 -27
- package/package.json +2 -1
- package/pi-router/README.md +30 -9
- package/pi-router/src/compaction.ts +144 -20
- package/pi-router/src/config.ts +5 -2
- package/pi-router/src/force-model.ts +126 -0
- package/pi-router/src/index.ts +6 -4
- package/pi-router/src/pricing.generated.ts +81 -0
- package/pi-router/src/routed-model.ts +151 -17
- package/pi-router/src/savings.ts +191 -0
- package/pi-router/src/ui.ts +91 -0
- package/pi-router/src/wooly.ts +408 -0
- package/uninstall.sh +25 -0
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
|
|
@@ -1999,10 +2125,21 @@ set -euo pipefail
|
|
|
1999
2125
|
# lives in their per-user cache dir, and on no-content-change days we skip
|
|
2000
2126
|
# the mv entirely so the repo working tree stays clean. When upstream does
|
|
2001
2127
|
# change, the first teammate's commit propagates the new version to the rest.
|
|
2128
|
+
# A pricing-table miss also triggers a refresh off-schedule — see
|
|
2129
|
+
# weave_refresh_on_price_miss below.
|
|
2002
2130
|
#
|
|
2003
2131
|
# Opt out entirely with `export WEAVE_STATUSLINE_UPDATE=0`. Override the
|
|
2004
2132
|
# source with `WEAVE_STATUSLINE_URL=...`, e.g. for self-hosters who fork.
|
|
2133
|
+
#
|
|
2134
|
+
# $1 is an optional stamp suffix, so a caller refreshing for a reason other than
|
|
2135
|
+
# "the interval elapsed" rate-limits on its own clock rather than sharing the
|
|
2136
|
+
# periodic check's budget. $2=1 drops the stamp again if the download fails, for
|
|
2137
|
+
# callers recovering from a known-bad state where waiting out the full interval
|
|
2138
|
+
# on a transient network error is worse than retrying next turn.
|
|
2005
2139
|
weave_self_refresh() {
|
|
2140
|
+
local stamp_suffix="${1:-}"
|
|
2141
|
+
local retry_on_fail="${2:-0}"
|
|
2142
|
+
|
|
2006
2143
|
[ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
|
|
2007
2144
|
command -v curl >/dev/null 2>&1 || return 0
|
|
2008
2145
|
|
|
@@ -2019,7 +2156,7 @@ weave_self_refresh() {
|
|
|
2019
2156
|
mkdir -p "$cache_dir" 2>/dev/null || return 0
|
|
2020
2157
|
local script_slug
|
|
2021
2158
|
script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
|
|
2022
|
-
local stamp="$cache_dir/checked-at${script_slug}"
|
|
2159
|
+
local stamp="$cache_dir/checked-at${script_slug}${stamp_suffix}"
|
|
2023
2160
|
|
|
2024
2161
|
local now stamp_mtime
|
|
2025
2162
|
now="$(date +%s 2>/dev/null)" || return 0
|
|
@@ -2043,7 +2180,13 @@ weave_self_refresh() {
|
|
|
2043
2180
|
: > "$stamp" 2>/dev/null || return 0
|
|
2044
2181
|
|
|
2045
2182
|
local url="${WEAVE_STATUSLINE_URL:-https://raw.githubusercontent.com/workweave/router/main/install/cc-statusline.sh}"
|
|
2046
|
-
|
|
2183
|
+
# $$ alone is not unique: two calls can run in one invocation (the periodic
|
|
2184
|
+
# check and a pricing-miss retry both fire on a cold cache) and would then
|
|
2185
|
+
# curl -o into the same path and mv over each other, installing a truncated
|
|
2186
|
+
# script that can never self-heal. The stamp suffix is the right key — it is
|
|
2187
|
+
# what makes two callers mutually exclusive in the first place, so callers
|
|
2188
|
+
# that could overlap necessarily have different suffixes.
|
|
2189
|
+
local tmp="${self}.tmp.$$${stamp_suffix}"
|
|
2047
2190
|
(
|
|
2048
2191
|
# Detach stdin (CC pipes JSON to us) so curl can't accidentally consume
|
|
2049
2192
|
# it, and silence all output so nothing leaks into the statusline.
|
|
@@ -2062,6 +2205,11 @@ weave_self_refresh() {
|
|
|
2062
2205
|
fi
|
|
2063
2206
|
else
|
|
2064
2207
|
rm -f "$tmp"
|
|
2208
|
+
# A download that never landed shouldn't spend the caller's whole
|
|
2209
|
+
# interval; drop the stamp so the next turn can try again.
|
|
2210
|
+
if [ "$retry_on_fail" = "1" ]; then
|
|
2211
|
+
rm -f "$stamp"
|
|
2212
|
+
fi
|
|
2065
2213
|
fi
|
|
2066
2214
|
) >/dev/null 2>&1 &
|
|
2067
2215
|
disown 2>/dev/null || true
|
|
@@ -2108,7 +2256,7 @@ prices='{
|
|
|
2108
2256
|
"claude-sonnet-4-6": 0.003,
|
|
2109
2257
|
"claude-sonnet-5": 0.003,
|
|
2110
2258
|
"deepseek/deepseek-v4-flash": 0.0001134,
|
|
2111
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
2259
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
2112
2260
|
"gemini-2.0-flash": 0.0001,
|
|
2113
2261
|
"gemini-2.0-flash-lite": 0.000075,
|
|
2114
2262
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -2143,6 +2291,7 @@ prices='{
|
|
|
2143
2291
|
"gpt-5.6-sol": 0.005,
|
|
2144
2292
|
"gpt-5.6-terra": 0.0025,
|
|
2145
2293
|
"grok-4.5": 0.002,
|
|
2294
|
+
"grok-4.6": 0.002,
|
|
2146
2295
|
"minimax/minimax-m2.7": 0.0003,
|
|
2147
2296
|
"minimax/minimax-m3": 0.0003,
|
|
2148
2297
|
"mistralai/mistral-small-2603": 0.0002,
|
|
@@ -2156,8 +2305,9 @@ prices='{
|
|
|
2156
2305
|
"qwen/qwen3-coder-next": 0.0005,
|
|
2157
2306
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
2158
2307
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
2159
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
2308
|
+
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
2160
2309
|
"qwen/qwen3.7-plus": 0.0004,
|
|
2310
|
+
"qwen/qwen3.8-max": 0.002,
|
|
2161
2311
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
2162
2312
|
"z-ai/glm-5": 0.001,
|
|
2163
2313
|
"z-ai/glm-5.1": 0.0014,
|
|
@@ -2177,7 +2327,7 @@ prices='{
|
|
|
2177
2327
|
"claude-sonnet-4-6": 0.015,
|
|
2178
2328
|
"claude-sonnet-5": 0.015,
|
|
2179
2329
|
"deepseek/deepseek-v4-flash": 0.0002791,
|
|
2180
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
2330
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
2181
2331
|
"gemini-2.0-flash": 0.0004,
|
|
2182
2332
|
"gemini-2.0-flash-lite": 0.0003,
|
|
2183
2333
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -2212,6 +2362,7 @@ prices='{
|
|
|
2212
2362
|
"gpt-5.6-sol": 0.03,
|
|
2213
2363
|
"gpt-5.6-terra": 0.015,
|
|
2214
2364
|
"grok-4.5": 0.006,
|
|
2365
|
+
"grok-4.6": 0.006,
|
|
2215
2366
|
"minimax/minimax-m2.7": 0.0012,
|
|
2216
2367
|
"minimax/minimax-m3": 0.0012,
|
|
2217
2368
|
"mistralai/mistral-small-2603": 0.0006,
|
|
@@ -2225,8 +2376,9 @@ prices='{
|
|
|
2225
2376
|
"qwen/qwen3-coder-next": 0.0012,
|
|
2226
2377
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
2227
2378
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
2228
|
-
"qwen/qwen3.6-35b-a3b": 0.
|
|
2379
|
+
"qwen/qwen3.6-35b-a3b": 0.001,
|
|
2229
2380
|
"qwen/qwen3.7-plus": 0.0016,
|
|
2381
|
+
"qwen/qwen3.8-max": 0.006,
|
|
2230
2382
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
2231
2383
|
"z-ai/glm-5": 0.0032,
|
|
2232
2384
|
"z-ai/glm-5.1": 0.0044,
|
|
@@ -2365,6 +2517,42 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
2365
2517
|
) || true
|
|
2366
2518
|
fi
|
|
2367
2519
|
|
|
2520
|
+
# ---------- refresh when a model isn't in the pricing table ----------
|
|
2521
|
+
#
|
|
2522
|
+
# A model that shipped after this copy of the script has no price entry, so the
|
|
2523
|
+
# jq guard above zeroes savings on every turn and the line reads "saved $0.00"
|
|
2524
|
+
# — indistinguishable from "the router ran and didn't beat your selection". The
|
|
2525
|
+
# periodic check heals that eventually, but a week late, and a model launch is
|
|
2526
|
+
# exactly when the number gets looked at. Refresh off-schedule instead.
|
|
2527
|
+
#
|
|
2528
|
+
# Keyed on its own stamp per unpriced id: a model we never price (self-hosted,
|
|
2529
|
+
# unrecognized) then costs one download per interval, not one per turn, and
|
|
2530
|
+
# can't starve the periodic check.
|
|
2531
|
+
weave_refresh_on_price_miss() {
|
|
2532
|
+
local candidates="" m
|
|
2533
|
+
for m in "$@"; do
|
|
2534
|
+
case "$m" in
|
|
2535
|
+
"" | "?" | failure | weave-router | "<synthetic>") continue ;;
|
|
2536
|
+
esac
|
|
2537
|
+
candidates="${candidates}${m}"$'\n'
|
|
2538
|
+
done
|
|
2539
|
+
[ -n "$candidates" ] || return 0
|
|
2540
|
+
|
|
2541
|
+
# A malformed $prices emits nothing here, which fails closed (no refresh)
|
|
2542
|
+
# rather than re-downloading every turn.
|
|
2543
|
+
local missing
|
|
2544
|
+
missing="$(printf '%s' "$candidates" \
|
|
2545
|
+
| jq -rR --argjson p "$prices" \
|
|
2546
|
+
'select($p.input[.] == null or $p.output[.] == null)' 2>/dev/null \
|
|
2547
|
+
| head -n 1)" || return 0
|
|
2548
|
+
[ -n "$missing" ] || return 0
|
|
2549
|
+
|
|
2550
|
+
local model_slug
|
|
2551
|
+
model_slug="$(printf '%s' "$missing" | tr -c 'A-Za-z0-9._-' '_')"
|
|
2552
|
+
weave_self_refresh ".miss.${model_slug}" 1
|
|
2553
|
+
}
|
|
2554
|
+
weave_refresh_on_price_miss "$requested_norm" "$routed" 2>/dev/null || true
|
|
2555
|
+
|
|
2368
2556
|
# Brand color (#FF6C47) on terminals that grok 24-bit truecolor — that's
|
|
2369
2557
|
# every modern one (iTerm2, Apple Terminal, vscode, ghostty, alacritty,
|
|
2370
2558
|
# wezterm, kitty). Falls back gracefully on any escape-stripping terminal.
|
|
@@ -2480,12 +2668,20 @@ custom_headers="$custom_headers"$'\n'"X-App: claude-code"
|
|
|
2480
2668
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
2481
2669
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
2482
2670
|
env: { ANTHROPIC_BASE_URL: $url, ENABLE_TOOL_SEARCH: "auto" },
|
|
2483
|
-
statusLine: { type: "command", command: $sl }
|
|
2671
|
+
statusLine: { type: "command", command: $sl },
|
|
2672
|
+
attribution: {
|
|
2673
|
+
commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
|
|
2674
|
+
pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
|
|
2675
|
+
}
|
|
2484
2676
|
}' >"$tmp_patch"
|
|
2485
2677
|
else
|
|
2486
2678
|
jq -n --arg url "$base_url" --arg header "$custom_headers" --arg sl "$statusline_path_for_settings" '{
|
|
2487
2679
|
env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header, ENABLE_TOOL_SEARCH: "auto" },
|
|
2488
|
-
statusLine: { type: "command", command: $sl }
|
|
2680
|
+
statusLine: { type: "command", command: $sl },
|
|
2681
|
+
attribution: {
|
|
2682
|
+
commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
|
|
2683
|
+
pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
|
|
2684
|
+
}
|
|
2489
2685
|
}' >"$tmp_patch"
|
|
2490
2686
|
fi
|
|
2491
2687
|
|
|
@@ -2501,6 +2697,7 @@ if [ -f "$settings_file" ]; then
|
|
|
2501
2697
|
| (if (.env | length) == 0 then del(.env) else . end)
|
|
2502
2698
|
| del(.apiKeyHelper)
|
|
2503
2699
|
| (if $b.statusLine then .statusLine = $b.statusLine else . end)
|
|
2700
|
+
| (if $b.attribution then .attribution = $b.attribution else . end)
|
|
2504
2701
|
' "$settings_file" "$tmp_patch")"
|
|
2505
2702
|
printf '%s\n' "$merged" >"$settings_file"
|
|
2506
2703
|
else
|
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
|
@@ -20,21 +20,37 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
20
20
|
|
|
21
21
|
## What it does
|
|
22
22
|
|
|
23
|
+
- **Loom experience on stock pi.** Replaces pi's startup header through the
|
|
24
|
+
public extension API, adds Wooly's responsive orange terminal animation, and
|
|
25
|
+
keeps pi's own runtime/footer intact. Wooly is visual only: there is no
|
|
26
|
+
dialogue box, narration, coaching request, or separate Loom runtime.
|
|
23
27
|
- **Automatic model selection.** All pi traffic flows through the router, which
|
|
24
28
|
selects the model per request. You don't pick a model — the router does.
|
|
29
|
+
- **Force-model commands.** `/fm <model>` and `/force-model <model>` pin the
|
|
30
|
+
current router session; `/ufm` and `/unforce-model` resume automatic routing.
|
|
31
|
+
The persistent status changes to `WEAVE ROUTER — <model> [forced]` after the
|
|
32
|
+
router validates and canonicalizes the requested model.
|
|
25
33
|
- **Per-process routing bias.** Static `x-weave-routing-*` knob headers bias the
|
|
26
|
-
router: quality on the main loop
|
|
27
|
-
|
|
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.
|
|
28
40
|
- **Sticky sessions.** `metadata.user_id = "pi:<sessionId>"` pins the main loop
|
|
29
41
|
to one model for the session; subagents get their own pins.
|
|
30
42
|
- **`dispatch` tool — parallel, context-isolated subagents.** pi has none
|
|
31
43
|
natively. `dispatch` spawns child `pi` processes (read-only by default), runs
|
|
32
44
|
them concurrently, and returns only each subagent's final answer — intermediate
|
|
33
45
|
tool output stays in the child, so the main context stays small.
|
|
34
|
-
- **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
- **Persistent route + savings display.** Shows
|
|
47
|
+
`WEAVE ROUTER — <routed> ← <selected> · saved $X.XX` below pi's native footer
|
|
48
|
+
data. Savings compare the selected and routed catalog prices against the same
|
|
49
|
+
input/output/cache usage, accumulate across the reachable session branch,
|
|
50
|
+
and survive resume. Unknown catalog prices are labeled `unpriced` instead of
|
|
51
|
+
silently contributing zero; costlier routing is labeled `extra`, not savings.
|
|
52
|
+
- **No duplicate in-band badge.** Sets `X-Weave-Routing-Marker: off` because the
|
|
53
|
+
persistent status already conveys the actual model.
|
|
38
54
|
- **Safety backstop.** Blocks a few catastrophic shell commands (`rm -rf /`,
|
|
39
55
|
`mkfs`, `dd of=/dev/…`, fork bombs, force-push to main). Disable with
|
|
40
56
|
`WEAVE_NO_SAFETY=1`.
|
|
@@ -53,7 +69,7 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
53
69
|
| `WEAVE_PI_ALLOW_SUBAGENT_TOOLS` | unset | `1` lets `dispatch` grant subagents write/exec tools (bash, write, edit); default strips them |
|
|
54
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) |
|
|
55
71
|
| `WEAVE_NO_SAFETY` | unset | `1` disables the catastrophic-bash gate |
|
|
56
|
-
| `
|
|
72
|
+
| `WEAVE_PI_AUTO_COMPACTION` | unset | `0` disables the routed tool-loop compaction safeguard |
|
|
57
73
|
|
|
58
74
|
Internal: `WEAVE_PI_SUBAGENT=1` and `WEAVE_PI_SUBAGENT_ID` are set by `dispatch`
|
|
59
75
|
on child processes; don't set them yourself.
|
|
@@ -64,7 +80,12 @@ Routing through the router switches pi from Claude **subscription OAuth** to
|
|
|
64
80
|
**per-token** billing on the router deployment's key (or your BYOK key). BYOK
|
|
65
81
|
skips cross-provider failover; deployment-key billing is the default.
|
|
66
82
|
|
|
83
|
+
The displayed savings are a client-side estimate from the router's generated
|
|
84
|
+
model-price catalog. Cache writes use 1.25× input price and cache reads use 0.1×
|
|
85
|
+
input price, matching the Claude Code statusline. The ledger stores its catalog
|
|
86
|
+
version with each response so resumed totals remain auditable.
|
|
87
|
+
|
|
67
88
|
## Notes
|
|
68
89
|
|
|
69
|
-
-
|
|
70
|
-
|
|
90
|
+
- Actual SDK/router probes keep their small output budget. Only a probe-sized
|
|
91
|
+
request carrying a real tool-result continuation is repaired.
|