@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/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
- DEFAULT_BASE_URL="${WEAVE_ROUTER_URL:-https://router.workweave.ai}"
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. Both lines live inside the
419
- # managed-block markers so uninstall removes them cleanly. We strip any
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 (shared by both targets) ----------
1717
+ # ---------- slash command wrappers (Claude Code and opencode) ----------
1685
1718
  #
1686
- # Claude Code intercepts any prompt starting with "/" as a local slash command,
1687
- # so a typed /force-model would resolve to "Unknown command" and never reach
1688
- # the router. Codex CLI does the same (its built-in / menu has its own set).
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 and
1720
- # apply to every target. The router-off/on/status wrappers shell out to this
1721
- # installer to flip the *local* config, so they're Claude Code-only and need
1722
- # the install scope baked into the command (the .md can't discover it at
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
- install_slash_commands "$codex_dir/prompts"
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
- local tmp="${self}.tmp.$$"
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.001318,
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.000172,
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.0026361,
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.0012002,
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.6",
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",
@@ -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, speed + cheap on subagents, cheapest on
27
- compaction.
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
- - **Routed-model display.** Shows the model the router actually chose
35
- (`x-router-model`) in the status bar, and opts the request out of the router's
36
- in-band routing badge (`X-Weave-Routing-Marker: off`) pi can't render that
37
- separate marker text block inline, and the status bar already conveys the model.
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
- | `WEAVE_CHEAP_COMPACTION` | unset | `1` enables the (experimental) cheap-compaction path |
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
- - Cheap compaction is currently a reserved flag the handler defers to pi's
70
- built-in compaction until the router-routed path is validated end-to-end.
90
+ - Actual SDK/router probes keep their small output budget. Only a probe-sized
91
+ request carrying a real tool-result continuation is repaired.