@workweave/router 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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"`. OpenAI plan credentials flow
66
- through to api.openai.com. The block lives between begin/end markers so
67
- re-running the installer rewrites it cleanly and `--uninstall --codex`
68
- removes it without touching the rest of your config.
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.001318,
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.000172,
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.0026361,
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.0012002,
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
- 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
@@ -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.001318,
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.000172,
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.0026361,
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.0012002,
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,
@@ -2540,7 +2670,7 @@ if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
2540
2670
  env: { ANTHROPIC_BASE_URL: $url, ENABLE_TOOL_SEARCH: "auto" },
2541
2671
  statusLine: { type: "command", command: $sl },
2542
2672
  attribution: {
2543
- commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
2673
+ commit: "Co-Authored-By: Weave Router <router@workweave.ai>",
2544
2674
  pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
2545
2675
  }
2546
2676
  }' >"$tmp_patch"
@@ -2549,7 +2679,7 @@ else
2549
2679
  env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header, ENABLE_TOOL_SEARCH: "auto" },
2550
2680
  statusLine: { type: "command", command: $sl },
2551
2681
  attribution: {
2552
- commit: "Co-Authored-By: Weave Router <noreply@workweave.ai>",
2682
+ commit: "Co-Authored-By: Weave Router <router@workweave.ai>",
2553
2683
  pr: "🤖 Generated with [Weave Router](https://router.workweave.ai)"
2554
2684
  }
2555
2685
  }' >"$tmp_patch"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -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, speed + cheap on subagents, cheapest on
35
- 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.
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
- | `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 |
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
- - Cheap compaction is currently a reserved flag the handler defers to pi's
87
- 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.
@@ -1,26 +1,150 @@
1
1
  /**
2
- * EXPERIMENTAL, off by default (enable with WEAVE_CHEAP_COMPACTION=1).
2
+ * Compaction safeguards for Pi's routed provider.
3
3
  *
4
- * Intent: run context compaction through the cheapest routing knobs. The
5
- * catch is that compaction bypasses provider hooks and reuses the session's
6
- * static provider headers (the main loop's quality knobs), and
7
- * `session_before_compact` is the only per-turn lever. Producing a fully
8
- * router-routed cheap CompactionResult here is not yet validated, so this
9
- * handler currently defers to pi's built-in compaction (returns no override)
10
- * and only reserves the flag + surfaces that it's active. Promote to a real
11
- * cheap path once validated end-to-end.
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
- import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent } from "@mariozechner/pi-coding-agent";
15
-
16
- export function registerCheapCompaction(pi: ExtensionAPI): void {
17
- let announced = false;
18
- pi.on("session_before_compact", (_event: SessionBeforeCompactEvent, ctx: ExtensionContext) => {
19
- if (ctx.hasUI && !announced) {
20
- announced = true;
21
- ctx.ui.setStatus("weave-compaction", "cheap compaction: experimental (using built-in)");
22
- }
23
- // Defer to built-in compaction until the router-routed cheap path is validated.
24
- return undefined;
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
  }
@@ -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: 200000,
239
+ contextWindow,
239
240
  maxTokens,
240
241
  };
241
242
  }
@@ -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: experimental cheap path (only when WEAVE_CHEAP_COMPACTION=1).
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 { registerCheapCompaction } from "./compaction.js";
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:a13e743aa9694647";
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.318, outputUsdPerMillion: 2.6361 },
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.172, outputUsdPerMillion: 1.2002 },
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 },
@@ -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.mode !== "tui") return;
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.mode === "tui") {
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 label = ctx.mode === "tui" ? ctx.ui.theme.bold(brand("WEAVE ROUTER")) : "WEAVE ROUTER";
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} — ${ctx.mode === "tui" ? ctx.ui.theme.fg("dim", detail) : detail}`);
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
@@ -523,7 +545,8 @@ if [ -f "$settings_file" ]; then
523
545
  then del(.statusLine) else . end)
524
546
  | (if (.apiKeyHelper // "" | tostring | endswith("weave-key.sh"))
525
547
  then del(.apiKeyHelper) else . end)
526
- | (if (.attribution.commit == "Co-Authored-By: Weave Router <noreply@workweave.ai>"
548
+ | (if ((.attribution.commit == "Co-Authored-By: Weave Router <router@workweave.ai>"
549
+ or .attribution.commit == "Co-Authored-By: Weave Router <noreply@workweave.ai>")
527
550
  and .attribution.pr == "🤖 Generated with [Weave Router](https://router.workweave.ai)")
528
551
  then del(.attribution) else . end)
529
552
  ' "$settings_file")"