@workweave/router 0.2.2 → 0.2.3
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/bin.js +3 -3
- package/cc-statusline.sh +47 -2
- package/commands/fm.md +1 -1
- package/commands/force-model.md +1 -1
- package/commands/rf.md +1 -1
- package/commands/router-feedback.md +1 -1
- package/commands/router-off.md +1 -1
- package/commands/router-on.md +1 -1
- package/commands/router-status.md +2 -2
- package/commands/ufm.md +1 -1
- package/commands/unforce-model.md +1 -1
- package/install.sh +193 -41
- package/opencode-weave/README.md +59 -0
- package/opencode-weave/package.json +9 -0
- package/opencode-weave/src/index.ts +438 -0
- package/package.json +2 -1
- package/pi-router/src/dispatch.ts +2 -2
- package/pi-router/src/index.ts +1 -1
- package/pi-router/src/provider.ts +1 -1
- package/pi-router/src/routed-model.ts +1 -1
- package/uninstall.sh +35 -11
package/install.sh
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
# Usage:
|
|
35
35
|
# npx @workweave/router # interactive picker (Claude Code, Codex, opencode)
|
|
36
36
|
# npx @workweave/router --claude # skip the picker, target Claude Code
|
|
37
|
-
# npx @workweave/router --codex # skip the picker, target
|
|
37
|
+
# npx @workweave/router --codex # skip the picker, target Codex
|
|
38
38
|
# npx @workweave/router --opencode # skip the picker, target opencode
|
|
39
39
|
# npx @workweave/router --pi # skip the picker, target pi
|
|
40
40
|
# npx @workweave/router --scope project # commit-with-team install
|
|
@@ -533,13 +533,20 @@ TOML
|
|
|
533
533
|
chmod 600 "$config_file"
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
# write_opencode_config merges
|
|
537
|
-
# opencode.json
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
536
|
+
# write_opencode_config merges TWO managed providers into opencode's
|
|
537
|
+
# opencode.json plus the bundled Codex-subscription plugin:
|
|
538
|
+
# - provider.weave : Anthropic-shaped (@ai-sdk/anthropic) — the router
|
|
539
|
+
# speaks the Anthropic Messages API natively, so the
|
|
540
|
+
# bundled provider works unmodified. The default.
|
|
541
|
+
# - provider.weave-codex : OpenAI-shaped (@ai-sdk/openai, Responses wire) —
|
|
542
|
+
# lets a caller's own ChatGPT (Codex) subscription
|
|
543
|
+
# pay for their opencode turns. Dormant until the
|
|
544
|
+
# user runs `opencode auth login` → ChatGPT and the
|
|
545
|
+
# weave-codex plugin's loader injects the bearer.
|
|
546
|
+
# The plugin (src bundled at $script_dir/opencode-weave/src/index.ts) is dropped
|
|
547
|
+
# into $opencode_dir/.weave/ and registered via opencode.json's `plugin` array
|
|
548
|
+
# by absolute path — scope-independent (no reliance on an auto-load dir).
|
|
549
|
+
# Re-running rewrites both blocks in-place via jq; uninstall strips them.
|
|
543
550
|
#
|
|
544
551
|
# Usage: write_opencode_config <config_file_path> <base_url> <api_key> [user_email] [user_name]
|
|
545
552
|
write_opencode_config() {
|
|
@@ -601,24 +608,101 @@ write_opencode_config() {
|
|
|
601
608
|
}
|
|
602
609
|
')"
|
|
603
610
|
|
|
611
|
+
# The OpenAI-shaped Codex-subscription provider. Reuses the same X-Weave-*
|
|
612
|
+
# header triplet (router key + identity) — the plugin's loader adds only the
|
|
613
|
+
# dynamic subscription Authorization + ChatGPT-Account-Id on top. baseURL
|
|
614
|
+
# KEEPS /v1: opencode's @ai-sdk/openai provider appends /responses, yielding
|
|
615
|
+
# the router's /v1/responses Codex passthrough. apiKey is the router key as a
|
|
616
|
+
# parse-time placeholder (the loader overrides it once a subscription is
|
|
617
|
+
# linked), mirroring the weave block above.
|
|
618
|
+
local codex_block
|
|
619
|
+
codex_block="$(jq -n \
|
|
620
|
+
--arg url "$block_url/v1" \
|
|
621
|
+
--arg key "$block_key" \
|
|
622
|
+
--argjson headers "$headers_json" '
|
|
623
|
+
{
|
|
624
|
+
npm: "@ai-sdk/openai",
|
|
625
|
+
name: "Weave Router (Codex subscription)",
|
|
626
|
+
options: { apiKey: $key, baseURL: $url, headers: $headers },
|
|
627
|
+
models: {
|
|
628
|
+
"gpt-5.5": { name: "GPT-5.5 — Codex subscription (via Weave Router)" },
|
|
629
|
+
"gpt-5.4": { name: "GPT-5.4 — Codex subscription (via Weave Router)" }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
')"
|
|
633
|
+
|
|
634
|
+
# Drop the Codex-subscription plugin next to the config and capture the
|
|
635
|
+
# absolute path we'll register in opencode.json's `plugin` array (so it loads
|
|
636
|
+
# regardless of scope). $config_file's dir is the already-created
|
|
637
|
+
# opencode_dir, so the `cd … && pwd` canonicalization is safe — uninstall.sh
|
|
638
|
+
# must canonicalize identically so the array entry matches on removal.
|
|
639
|
+
#
|
|
640
|
+
# Only register the path when the bundled source is actually present and
|
|
641
|
+
# copied: registering a path with no file on disk makes opencode fail to load
|
|
642
|
+
# a missing plugin. The plugin holds no secrets (router key lives in the
|
|
643
|
+
# config; ChatGPT tokens live in opencode's own auth store), so 644 is fine.
|
|
644
|
+
# Source is bundled alongside install.sh by the npm prepack
|
|
645
|
+
# (scripts/copy-installer.js), same as commands/ + pi-router/.
|
|
646
|
+
local plugin_dir plugin_spec plugin_src plugin_arg=""
|
|
647
|
+
plugin_dir="$(cd "$(dirname "$config_file")" && pwd)/.weave"
|
|
648
|
+
plugin_spec="$plugin_dir/opencode-weave.ts"
|
|
649
|
+
plugin_src="$script_dir/opencode-weave/src/index.ts"
|
|
650
|
+
if [ -f "$plugin_src" ]; then
|
|
651
|
+
mkdir -p "$plugin_dir"
|
|
652
|
+
cp "$plugin_src" "$plugin_spec"
|
|
653
|
+
chmod 644 "$plugin_spec"
|
|
654
|
+
plugin_arg="$plugin_spec"
|
|
655
|
+
else
|
|
656
|
+
warn "opencode Codex plugin source not found at $plugin_src — skipping the weave-codex plugin registration. (Use a packaged 'npx @workweave/router' install.)"
|
|
657
|
+
fi
|
|
658
|
+
|
|
604
659
|
# Merge into any existing opencode.json. We always overwrite provider.weave
|
|
605
|
-
# so re-install reflects the latest key/identity, but we leave the rest of
|
|
606
|
-
#
|
|
607
|
-
#
|
|
660
|
+
# so re-install reflects the latest key/identity, but we leave the rest of the
|
|
661
|
+
# file (other providers, mcp, agent settings) untouched. Top-level `model` is
|
|
662
|
+
# only set when the user hasn't already picked one.
|
|
663
|
+
#
|
|
664
|
+
# The weave-codex provider AND its plugin entry are written together only when
|
|
665
|
+
# the bundled plugin was present and copied ($plugin non-empty). Codex
|
|
666
|
+
# subscription auth depends on that plugin, so registering the provider
|
|
667
|
+
# without it (e.g. the `curl | sh` path, which carries no plugin source)
|
|
668
|
+
# would leave a non-working provider — instead we omit it (and strip any
|
|
669
|
+
# stale one from a prior install).
|
|
608
670
|
local merged
|
|
609
671
|
if [ -f "$config_file" ]; then
|
|
610
|
-
merged="$(jq
|
|
672
|
+
merged="$(jq \
|
|
673
|
+
--argjson block "$block" \
|
|
674
|
+
--argjson codex "$codex_block" \
|
|
675
|
+
--arg plugin "$plugin_arg" \
|
|
676
|
+
--arg pluginspec "$plugin_spec" '
|
|
611
677
|
.provider = ((.provider // {}) | .weave = $block)
|
|
678
|
+
| (if $plugin != "" then .provider["weave-codex"] = $codex else .provider |= del(."weave-codex") end)
|
|
679
|
+
# Register the managed plugin path when we installed it; otherwise strip a
|
|
680
|
+
# stale entry left by a prior install (the provider was just removed, so a
|
|
681
|
+
# lingering plugin reference would be dead weight).
|
|
682
|
+
| (if $plugin != ""
|
|
683
|
+
then .plugin = ((.plugin // []) | if index($plugin) then . else . + [$plugin] end)
|
|
684
|
+
else (if (.plugin | type) == "array"
|
|
685
|
+
then (.plugin -= [$pluginspec]) | (if (.plugin | length) == 0 then del(.plugin) else . end)
|
|
686
|
+
else . end)
|
|
687
|
+
end)
|
|
612
688
|
| (if (.model // "") == "" then .model = "weave/claude-sonnet-4-6" else . end)
|
|
689
|
+
# If we just stripped weave-codex (plugin-less re-install) but the default
|
|
690
|
+
# model still points at it, reset to the Anthropic weave model so opencode
|
|
691
|
+
# does not boot with a model whose provider no longer exists.
|
|
692
|
+
| (if $plugin == "" and (.model // "" | tostring | startswith("weave-codex/")) then .model = "weave/claude-sonnet-4-6" else . end)
|
|
613
693
|
| (.["$schema"] //= "https://opencode.ai/config.json")
|
|
614
694
|
' "$config_file")"
|
|
615
695
|
else
|
|
616
|
-
merged="$(jq -n
|
|
696
|
+
merged="$(jq -n \
|
|
697
|
+
--argjson block "$block" \
|
|
698
|
+
--argjson codex "$codex_block" \
|
|
699
|
+
--arg plugin "$plugin_arg" '
|
|
617
700
|
{
|
|
618
701
|
"$schema": "https://opencode.ai/config.json",
|
|
619
702
|
model: "weave/claude-sonnet-4-6",
|
|
620
703
|
provider: { weave: $block }
|
|
621
704
|
}
|
|
705
|
+
| (if $plugin != "" then .provider["weave-codex"] = $codex | .plugin = [$plugin] else . end)
|
|
622
706
|
')"
|
|
623
707
|
fi
|
|
624
708
|
printf '%s\n' "$merged" >"$config_file"
|
|
@@ -800,11 +884,11 @@ for arg in "$@"; do
|
|
|
800
884
|
# also gives us a chance to fail closed on 404 HTML pages before
|
|
801
885
|
# handing the content to bash.
|
|
802
886
|
if ! uninstall_body="$(curl -fsSL --max-time 30 "$url" 2>/dev/null)"; then
|
|
803
|
-
err "
|
|
887
|
+
err "Failed to fetch uninstall.sh from $url."
|
|
804
888
|
exit 1
|
|
805
889
|
fi
|
|
806
890
|
if [ -z "$uninstall_body" ] || [ "${uninstall_body:0:2}" != "#!" ]; then
|
|
807
|
-
err "
|
|
891
|
+
err "Fetched content from $url doesn't look like a bash script."
|
|
808
892
|
exit 1
|
|
809
893
|
fi
|
|
810
894
|
exec bash -c "$uninstall_body" weave-uninstall "${cleaned_args[@]+"${cleaned_args[@]}"}"
|
|
@@ -817,12 +901,12 @@ while [ $# -gt 0 ]; do
|
|
|
817
901
|
case "$1" in
|
|
818
902
|
--scope)
|
|
819
903
|
scope="${2:-}"; shift 2
|
|
820
|
-
[ "$scope" = "user" ] || [ "$scope" = "project" ] || { err "--scope must be 'user' or 'project'"; exit 2; }
|
|
904
|
+
[ "$scope" = "user" ] || [ "$scope" = "project" ] || { err "--scope must be 'user' or 'project'."; exit 2; }
|
|
821
905
|
scope_explicit="true"
|
|
822
906
|
;;
|
|
823
907
|
--base-url)
|
|
824
908
|
base_url="${2:-}"; shift 2
|
|
825
|
-
[ -n "$base_url" ] || { err "--base-url requires a value"; exit 2; }
|
|
909
|
+
[ -n "$base_url" ] || { err "--base-url requires a value."; exit 2; }
|
|
826
910
|
;;
|
|
827
911
|
--local)
|
|
828
912
|
# Shorthand for local dev: localhost:8080 (matches `wv mr` / `make dev` default PORT).
|
|
@@ -837,7 +921,7 @@ while [ $# -gt 0 ]; do
|
|
|
837
921
|
;;
|
|
838
922
|
--dir)
|
|
839
923
|
install_dir="${2:-}"; shift 2
|
|
840
|
-
[ -n "$install_dir" ] || { err "--dir requires a path"; exit 2; }
|
|
924
|
+
[ -n "$install_dir" ] || { err "--dir requires a path."; exit 2; }
|
|
841
925
|
;;
|
|
842
926
|
--codex)
|
|
843
927
|
target="codex"; target_explicit="true"; shift
|
|
@@ -863,7 +947,7 @@ while [ $# -gt 0 ]; do
|
|
|
863
947
|
usage 0
|
|
864
948
|
;;
|
|
865
949
|
*)
|
|
866
|
-
err "
|
|
950
|
+
err "Unknown flag: $1."; usage 2
|
|
867
951
|
;;
|
|
868
952
|
esac
|
|
869
953
|
done
|
|
@@ -874,7 +958,7 @@ done
|
|
|
874
958
|
if [ "$mode" != "install" ]; then
|
|
875
959
|
non_interactive="true"
|
|
876
960
|
if [ "$target_explicit" != "true" ]; then
|
|
877
|
-
err "'$mode' requires an explicit client: --claude, --codex, or --opencode"
|
|
961
|
+
err "'$mode' requires an explicit client: --claude, --codex, or --opencode."
|
|
878
962
|
exit 2
|
|
879
963
|
fi
|
|
880
964
|
fi
|
|
@@ -883,7 +967,7 @@ fi
|
|
|
883
967
|
# structural models.json/settings.json merge, reversed by the uninstaller
|
|
884
968
|
# rather than a single env/key line we can park and restore.
|
|
885
969
|
if [ "$mode" != "install" ] && [ "$target" = "pi" ]; then
|
|
886
|
-
err "
|
|
970
|
+
err "Toggle verbs (off/on/status) aren't supported for --pi. Use 'npx @workweave/router --uninstall --pi' to remove, or re-run the installer to refresh."
|
|
887
971
|
exit 2
|
|
888
972
|
fi
|
|
889
973
|
|
|
@@ -914,7 +998,7 @@ if [ "$target_explicit" = "false" ] && [ "$non_interactive" = "false" ] && [ -r
|
|
|
914
998
|
2|codex|x|X) target="codex" ;;
|
|
915
999
|
3|opencode|o|O) target="opencode" ;;
|
|
916
1000
|
4|pi|p|P) target="pi" ;;
|
|
917
|
-
*) err "
|
|
1001
|
+
*) err "Invalid choice: $target_choice."; exit 2 ;;
|
|
918
1002
|
esac
|
|
919
1003
|
fi
|
|
920
1004
|
|
|
@@ -970,7 +1054,7 @@ if [ -z "$install_dir" ] && [ "$scope_explicit" = "false" ] && [ "$non_interacti
|
|
|
970
1054
|
case "${scope_choice:-1}" in
|
|
971
1055
|
1|""|user|u|U) scope="user" ;;
|
|
972
1056
|
2|project|p|P) scope="project" ;;
|
|
973
|
-
*) err "
|
|
1057
|
+
*) err "Invalid choice: $scope_choice."; exit 2 ;;
|
|
974
1058
|
esac
|
|
975
1059
|
|
|
976
1060
|
# For project scope, ask which directory rather than silently assuming CWD.
|
|
@@ -987,7 +1071,7 @@ if [ -z "$install_dir" ] && [ "$scope_explicit" = "false" ] && [ "$non_interacti
|
|
|
987
1071
|
"~/"*) project_dir="$HOME/${project_dir#~/}" ;;
|
|
988
1072
|
esac
|
|
989
1073
|
if [ ! -d "$project_dir" ]; then
|
|
990
|
-
err "
|
|
1074
|
+
err "Directory does not exist: $project_dir."
|
|
991
1075
|
exit 1
|
|
992
1076
|
fi
|
|
993
1077
|
project_dir="$(cd "$project_dir" && pwd)"
|
|
@@ -1324,7 +1408,7 @@ toggle_claude() {
|
|
|
1324
1408
|
| (if (.env // {} | length) == 0 then del(.env) else . end)' "$settings_file")"
|
|
1325
1409
|
printf '%s\n' "$merged" >"$settings_file"
|
|
1326
1410
|
fi
|
|
1327
|
-
ok "Claude Code is
|
|
1411
|
+
ok "Claude Code is ${C_BOLD}off${C_RESET} (direct to Anthropic). Restart Claude Code for it to take effect."
|
|
1328
1412
|
;;
|
|
1329
1413
|
on)
|
|
1330
1414
|
if [ "$parked_present" != "true" ]; then
|
|
@@ -1348,7 +1432,7 @@ toggle_claude() {
|
|
|
1348
1432
|
| (if (.env // {} | length) == 0 then del(.env) else . end)' "$local_settings_file")"
|
|
1349
1433
|
printf '%s\n' "$merged" >"$local_settings_file"
|
|
1350
1434
|
chmod 600 "$local_settings_file"
|
|
1351
|
-
ok "Claude Code is
|
|
1435
|
+
ok "Claude Code is ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
|
|
1352
1436
|
else
|
|
1353
1437
|
warn "Claude Code is off and the parked router key is missing (its sidecar was deleted). Re-run the installer to restore the router key — leaving the current direct-to-Anthropic setup in place so requests don't fail auth."
|
|
1354
1438
|
fi
|
|
@@ -1387,7 +1471,7 @@ toggle_claude() {
|
|
|
1387
1471
|
printf '%s\n' "$merged" >"$active"
|
|
1388
1472
|
[ "$proj" = "true" ] && chmod 600 "$active"
|
|
1389
1473
|
rm -f "$parked"
|
|
1390
|
-
ok "Claude Code is
|
|
1474
|
+
ok "Claude Code is ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
|
|
1391
1475
|
;;
|
|
1392
1476
|
esac
|
|
1393
1477
|
}
|
|
@@ -1425,7 +1509,7 @@ toggle_codex() {
|
|
|
1425
1509
|
{print}
|
|
1426
1510
|
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1427
1511
|
chmod 600 "$f"
|
|
1428
|
-
ok "Codex is
|
|
1512
|
+
ok "Codex is ${C_BOLD}off${C_RESET} (default provider). Takes effect on your next 'codex' run."
|
|
1429
1513
|
;;
|
|
1430
1514
|
on)
|
|
1431
1515
|
if [ "$state" = "absent" ]; then warn "No managed Weave block in $f. Run the installer to set up Codex."; return 0; fi
|
|
@@ -1440,7 +1524,7 @@ toggle_codex() {
|
|
|
1440
1524
|
{print}
|
|
1441
1525
|
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1442
1526
|
chmod 600 "$f"
|
|
1443
|
-
ok "Codex is
|
|
1527
|
+
ok "Codex is ${C_BOLD}on${C_RESET} (routing through the Weave Router). Takes effect on your next 'codex' run."
|
|
1444
1528
|
;;
|
|
1445
1529
|
esac
|
|
1446
1530
|
}
|
|
@@ -1459,7 +1543,9 @@ toggle_opencode() {
|
|
|
1459
1543
|
model="$(jq -r '.model // empty' "$f" 2>/dev/null || true)"
|
|
1460
1544
|
if [ "$(jq -r '((.provider // {}) | has("weave"))' "$f" 2>/dev/null || true)" = "true" ]; then has_weave="true"; fi
|
|
1461
1545
|
fi
|
|
1462
|
-
|
|
1546
|
+
# Either managed provider counts as router-on: the Anthropic-shaped `weave/…`
|
|
1547
|
+
# default or a caller-selected `weave-codex/…` Codex-subscription model.
|
|
1548
|
+
case "$model" in weave/* | weave-codex/*) on="true" ;; esac
|
|
1463
1549
|
|
|
1464
1550
|
case "$mode" in
|
|
1465
1551
|
status)
|
|
@@ -1482,7 +1568,7 @@ toggle_opencode() {
|
|
|
1482
1568
|
printf '%s\n' "$merged" >"$f"
|
|
1483
1569
|
chmod 600 "$f"
|
|
1484
1570
|
gitignore_add ".weave-parked.json"
|
|
1485
|
-
ok "opencode is
|
|
1571
|
+
ok "opencode is ${C_BOLD}off${C_RESET} — pick a non-Weave model with /models. Takes effect on your next opencode run."
|
|
1486
1572
|
;;
|
|
1487
1573
|
on)
|
|
1488
1574
|
if [ "$on" = "true" ]; then ok "opencode is already on — nothing to do."; return 0; fi
|
|
@@ -1502,11 +1588,22 @@ toggle_opencode() {
|
|
|
1502
1588
|
| "weave/" + .
|
|
1503
1589
|
' "$f" 2>/dev/null || echo "weave/claude-sonnet-4-6")"
|
|
1504
1590
|
fi
|
|
1591
|
+
# Never restore a weave-codex model whose provider is no longer
|
|
1592
|
+
# registered (e.g. a plugin-less re-install stripped it after `off`
|
|
1593
|
+
# parked a weave-codex/… model). Fall back to the Anthropic weave default
|
|
1594
|
+
# so `on` can't leave opencode pointing at a missing provider.
|
|
1595
|
+
case "$restore_model" in
|
|
1596
|
+
weave-codex/*)
|
|
1597
|
+
if [ "$(jq -r '((.provider // {}) | has("weave-codex"))' "$f" 2>/dev/null || true)" != "true" ]; then
|
|
1598
|
+
restore_model="weave/claude-sonnet-4-6"
|
|
1599
|
+
fi
|
|
1600
|
+
;;
|
|
1601
|
+
esac
|
|
1505
1602
|
merged="$(jq --arg m "$restore_model" '.model = $m' "$f")"
|
|
1506
1603
|
printf '%s\n' "$merged" >"$f"
|
|
1507
1604
|
chmod 600 "$f"
|
|
1508
1605
|
rm -f "$parked"
|
|
1509
|
-
ok "opencode is
|
|
1606
|
+
ok "opencode is ${C_BOLD}on${C_RESET} (default model $restore_model via the Weave Router). Takes effect on your next opencode run."
|
|
1510
1607
|
;;
|
|
1511
1608
|
esac
|
|
1512
1609
|
}
|
|
@@ -1535,19 +1632,19 @@ if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
|
1535
1632
|
# `set -e` would abort on read returning 1). If /dev/tty isn't available
|
|
1536
1633
|
# (e.g. CI without a controlling terminal) the user must use --non-interactive.
|
|
1537
1634
|
if [ ! -r /dev/tty ]; then
|
|
1538
|
-
err "
|
|
1635
|
+
err "No controlling terminal — set WEAVE_ROUTER_KEY and re-run with --non-interactive."
|
|
1539
1636
|
exit 1
|
|
1540
1637
|
fi
|
|
1541
1638
|
# _spin_cleanup (installed globally above) already restores stty echo on
|
|
1542
1639
|
# any exit path, so we don't need a separate trap here — that would
|
|
1543
1640
|
# overwrite the spinner cleanup and leak the cursor / child PID on Ctrl-C.
|
|
1544
1641
|
printf "%sGet your Weave Router API key at %s%s\n" "$C_BRAND" "$base_url" "$C_RESET"
|
|
1545
|
-
printf "%sPaste your key here (rk_
|
|
1642
|
+
printf "%sPaste your key here (rk_…):%s " "$C_DIM" "$C_RESET"
|
|
1546
1643
|
stty -echo </dev/tty 2>/dev/null || true
|
|
1547
1644
|
read -r api_key </dev/tty
|
|
1548
1645
|
stty echo </dev/tty 2>/dev/null || true
|
|
1549
1646
|
printf "\n"
|
|
1550
|
-
[ -n "$api_key" ] || { err "
|
|
1647
|
+
[ -n "$api_key" ] || { err "No key provided."; exit 1; }
|
|
1551
1648
|
fi
|
|
1552
1649
|
|
|
1553
1650
|
# ---------- identity (user email + name) ----------
|
|
@@ -1716,18 +1813,20 @@ if [ "$target" = "opencode" ]; then
|
|
|
1716
1813
|
|
|
1717
1814
|
# Project scope: the per-teammate config carries the router key, so it
|
|
1718
1815
|
# stays out of git. Same reasoning as the Codex path — base URL is shared,
|
|
1719
|
-
# but the key is per-person.
|
|
1816
|
+
# but the key is per-person. The .weave/ plugin dir is per-person too (the
|
|
1817
|
+
# config references it by absolute path), so ignore it alongside.
|
|
1720
1818
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ]; then
|
|
1721
1819
|
gitignore="$git_root/.gitignore"
|
|
1722
1820
|
refuse_if_symlink "$gitignore"
|
|
1723
1821
|
for entry in \
|
|
1724
|
-
"opencode.json"
|
|
1822
|
+
"opencode.json" \
|
|
1823
|
+
".weave/"
|
|
1725
1824
|
do
|
|
1726
1825
|
if [ ! -f "$gitignore" ] || ! grep -qxF "$entry" "$gitignore"; then
|
|
1727
1826
|
printf '%s\n' "$entry" >>"$gitignore"
|
|
1728
1827
|
fi
|
|
1729
1828
|
done
|
|
1730
|
-
ok "Updated $gitignore (ignored opencode.json)"
|
|
1829
|
+
ok "Updated $gitignore (ignored opencode.json, .weave/)"
|
|
1731
1830
|
fi
|
|
1732
1831
|
|
|
1733
1832
|
# Post-install verification: same probes the Claude/Codex paths run.
|
|
@@ -1750,6 +1849,14 @@ if [ "$target" = "opencode" ]; then
|
|
|
1750
1849
|
printf "\n"
|
|
1751
1850
|
printf "%s✓%s %s%sWeave Router installed for opencode.%s\n" \
|
|
1752
1851
|
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
1852
|
+
# Surface the optional Codex-subscription path only when this run actually
|
|
1853
|
+
# registered the weave-codex provider. Gate on the provider's presence in the
|
|
1854
|
+
# written config (authoritative) rather than a plugin file on disk — a
|
|
1855
|
+
# leftover plugin from a prior install can outlive a plugin-less re-install
|
|
1856
|
+
# that stripped the provider, which would make these instructions misleading.
|
|
1857
|
+
if jq -e '(.provider // {}) | has("weave-codex")' "$opencode_config_file" >/dev/null 2>&1; then
|
|
1858
|
+
info "To pay for opencode turns with your own ChatGPT (Codex) subscription: run ${C_BOLD}opencode auth login${C_RESET} → ${C_BOLD}ChatGPT Pro/Plus${C_RESET}, then pick a ${C_BOLD}weave-codex${C_RESET} model."
|
|
1859
|
+
fi
|
|
1753
1860
|
if [ -n "$install_dir" ]; then
|
|
1754
1861
|
# --dir installs land outside opencode's discovery roots, so the caller
|
|
1755
1862
|
# has to point opencode at the file explicitly.
|
|
@@ -1819,7 +1926,7 @@ if [ "$target" = "pi" ]; then
|
|
|
1819
1926
|
# Billing note: pi normally draws on a Claude subscription (OAuth); routing
|
|
1820
1927
|
# through the router switches to per-token billing on the router deployment
|
|
1821
1928
|
# key (or BYOK). Surface it at install so the change isn't a surprise.
|
|
1822
|
-
info "pi
|
|
1929
|
+
info "pi bills per token on the Weave Router key, not your Claude subscription."
|
|
1823
1930
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1824
1931
|
info "Run pi with PI_CODING_AGENT_DIR=$pi_dir pi so it picks up this config."
|
|
1825
1932
|
fi
|
|
@@ -1832,7 +1939,7 @@ fi
|
|
|
1832
1939
|
cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
1833
1940
|
#!/usr/bin/env bash
|
|
1834
1941
|
#
|
|
1835
|
-
# Claude Code statusline for the Weave
|
|
1942
|
+
# Claude Code statusline for the Weave Router. CC pipes a JSON blob on stdin
|
|
1836
1943
|
# whose `transcript_path` points at the JSONL log of the current session and
|
|
1837
1944
|
# whose `model.display_name` is the user's CC-side model selection. The
|
|
1838
1945
|
# router rewrites each request's `model` field before forwarding, so
|
|
@@ -1998,9 +2105,11 @@ prices='{
|
|
|
1998
2105
|
"gpt-5.5-nano": 0.00015,
|
|
1999
2106
|
"gpt-5.5-pro": 0.03,
|
|
2000
2107
|
"minimax/minimax-m2.7": 0.0003,
|
|
2108
|
+
"minimax/minimax-m3": 0.0003,
|
|
2001
2109
|
"mistralai/mistral-small-2603": 0.0002,
|
|
2002
2110
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
2003
2111
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
2112
|
+
"moonshotai/kimi-k2.7": 0.00095,
|
|
2004
2113
|
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
2005
2114
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
2006
2115
|
"qwen/qwen3-coder": 0.0009,
|
|
@@ -2008,6 +2117,7 @@ prices='{
|
|
|
2008
2117
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
2009
2118
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
2010
2119
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
2120
|
+
"qwen/qwen3.7-plus": 0.0004,
|
|
2011
2121
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
2012
2122
|
"z-ai/glm-5": 0.0006,
|
|
2013
2123
|
"z-ai/glm-5.1": 0.00105
|
|
@@ -2050,9 +2160,11 @@ prices='{
|
|
|
2050
2160
|
"gpt-5.5-nano": 0.0006,
|
|
2051
2161
|
"gpt-5.5-pro": 0.12,
|
|
2052
2162
|
"minimax/minimax-m2.7": 0.0012,
|
|
2163
|
+
"minimax/minimax-m3": 0.0012,
|
|
2053
2164
|
"mistralai/mistral-small-2603": 0.0006,
|
|
2054
2165
|
"moonshotai/kimi-k2.5": 0.003,
|
|
2055
2166
|
"moonshotai/kimi-k2.6": 0.004,
|
|
2167
|
+
"moonshotai/kimi-k2.7": 0.004,
|
|
2056
2168
|
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
2057
2169
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
2058
2170
|
"qwen/qwen3-coder": 0.0027,
|
|
@@ -2060,6 +2172,7 @@ prices='{
|
|
|
2060
2172
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
2061
2173
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
2062
2174
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
2175
|
+
"qwen/qwen3.7-plus": 0.0016,
|
|
2063
2176
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
2064
2177
|
"z-ai/glm-5": 0.00208,
|
|
2065
2178
|
"z-ai/glm-5.1": 0.0035
|
|
@@ -2068,6 +2181,8 @@ prices='{
|
|
|
2068
2181
|
# END_GENERATED_PRICES
|
|
2069
2182
|
|
|
2070
2183
|
routed=""
|
|
2184
|
+
forced="false"
|
|
2185
|
+
forced_model=""
|
|
2071
2186
|
session_savings=""
|
|
2072
2187
|
tot_in=0
|
|
2073
2188
|
tot_out=0
|
|
@@ -2105,6 +2220,36 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
2105
2220
|
routed="$(normalize_model "$routed")"
|
|
2106
2221
|
fi
|
|
2107
2222
|
|
|
2223
|
+
# Detect an active /force-model pin from the router's synthetic ack turns —
|
|
2224
|
+
# the only turns stamped message.model == "weave-router". The router emits
|
|
2225
|
+
# one whenever a pin changes state:
|
|
2226
|
+
# * /force-model → "force-model applied: <model> (<provider>) …"
|
|
2227
|
+
# * /unforce-model → "force-model cleared …"
|
|
2228
|
+
# * loop / no-progress break → "… clearing the session pin …" (expires the
|
|
2229
|
+
# pin, including a user-forced one)
|
|
2230
|
+
# * unrecognized model → "… isn't a recognized model · keeping
|
|
2231
|
+
# automatic routing" — a NO-OP: the prior
|
|
2232
|
+
# pin, if any, is left untouched
|
|
2233
|
+
# These persist on disk (the ingress stripper only scrubs them from upstream
|
|
2234
|
+
# requests). Classify each weave-router turn newest-first, skip the no-op
|
|
2235
|
+
# "rejected" acks, and let the latest real state change decide: an "applied"
|
|
2236
|
+
# marker means the session is pinned (and names the model); anything else
|
|
2237
|
+
# (cleared / loop-break / no-progress) means automatic routing has resumed.
|
|
2238
|
+
# Restricting to weave-router turns keeps a normal reply that merely quotes
|
|
2239
|
+
# these phrases from flipping the tag. (A silent server-side TTL expiry emits
|
|
2240
|
+
# no turn and so can't be reflected here — the pin TTL outlives a session.)
|
|
2241
|
+
force_state="$("${reverse[@]}" "$transcript_path" 2>/dev/null \
|
|
2242
|
+
| jq -r 'select(.type=="assistant" and .message.model=="weave-router")
|
|
2243
|
+
| ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
|
|
2244
|
+
| if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
|
|
2245
|
+
elif ($t | test("isn.t a recognized model")) then "REJECTED"
|
|
2246
|
+
else "CLEARED" end' 2>/dev/null \
|
|
2247
|
+
| grep -m1 -v '^REJECTED$' || true)"
|
|
2248
|
+
if [[ "$force_state" == APPLIED\ * ]]; then
|
|
2249
|
+
forced="true"
|
|
2250
|
+
forced_model="${force_state#APPLIED }"
|
|
2251
|
+
fi
|
|
2252
|
+
|
|
2108
2253
|
# Compute a session running total: savings across every assistant turn
|
|
2109
2254
|
# whose marker reports a requested ≠ routed swap, plus cumulative token
|
|
2110
2255
|
# counts across every assistant turn (rerouted or not — total work the
|
|
@@ -2207,7 +2352,14 @@ if [[ "$tot_in" -gt 0 || "$tot_out" -gt 0 || "$tot_cache_read" -gt 0 || "$tot_ca
|
|
|
2207
2352
|
fi
|
|
2208
2353
|
fi
|
|
2209
2354
|
|
|
2210
|
-
if [[ "$
|
|
2355
|
+
if [[ "$forced" == "true" ]]; then
|
|
2356
|
+
# Session is pinned via /force-model. The "← selection · saved $X" clause
|
|
2357
|
+
# describes automatic routing and would be misleading on a manual pin, so
|
|
2358
|
+
# show the pinned model with a [forced] tag instead. forced_model comes from
|
|
2359
|
+
# the marker; fall back to the routed/selected id if parsing came up empty.
|
|
2360
|
+
forced_display="${forced_model:-${routed:-$selected_display}}"
|
|
2361
|
+
printf '%s — %s [forced]%s' "$brand" "$forced_display" "$tokens_clause"
|
|
2362
|
+
elif [[ "$routed" == "failure" ]]; then
|
|
2211
2363
|
# Latest turn was a CC-synthesized error stub — don't claim a routing
|
|
2212
2364
|
# swap or compute savings against a non-model.
|
|
2213
2365
|
printf '%s — %s%s' "$brand" "$routed" "$tokens_clause"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# opencode Weave Codex plugin
|
|
2
|
+
|
|
3
|
+
Lets a caller's own **ChatGPT (Codex) subscription** pay for their **opencode**
|
|
4
|
+
turns, routed through the Weave Router. Bundled into `@workweave/router`; the
|
|
5
|
+
installer (`--codex` / `--opencode`) drops `src/index.ts` into the user's
|
|
6
|
+
opencode plugins dir and writes a matching `weave-codex` provider block into
|
|
7
|
+
`opencode.json`.
|
|
8
|
+
|
|
9
|
+
## Why a plugin (config alone can't do it)
|
|
10
|
+
|
|
11
|
+
opencode removed built-in subscription auth in 1.3.0 and binds OAuth to its own
|
|
12
|
+
first-party providers — its bundled `openai/codex.ts` plugin hardcodes the
|
|
13
|
+
upstream to `chatgpt.com` and binds provider id `openai`, so a custom router
|
|
14
|
+
provider can't reuse it. And a subscription needs the caller's OAuth token in
|
|
15
|
+
`Authorization`, which expires hourly — a static `options.headers` string can't
|
|
16
|
+
refresh it.
|
|
17
|
+
|
|
18
|
+
This plugin re-implements the same ChatGPT OAuth + refresh against a **custom**
|
|
19
|
+
provider id (`weave-codex`) and, crucially, **leaves the request URL on the
|
|
20
|
+
Weave Router** instead of rewriting it to `chatgpt.com`.
|
|
21
|
+
|
|
22
|
+
## Wire shape it produces
|
|
23
|
+
|
|
24
|
+
Matches the router's `/v1/responses` Codex passthrough:
|
|
25
|
+
|
|
26
|
+
| | |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `POST {router}/v1/responses` | Responses wire format (opencode's default for an `@ai-sdk/openai` provider) |
|
|
29
|
+
| `Authorization: Bearer <ChatGPT JWT>` | the caller's subscription, refreshed on expiry |
|
|
30
|
+
| `ChatGPT-Account-Id: <id>` | paired account id (required by the Codex backend) |
|
|
31
|
+
| `X-Weave-Router-Key: rk_…` | from `opencode.json` `options.headers` — the router authenticates off this, leaving `Authorization` free for the JWT |
|
|
32
|
+
| `originator`, `session-id` | from the plugin's `chat.headers` hook (Codex backend session continuity) |
|
|
33
|
+
|
|
34
|
+
The router detects the inbound Codex bearer and serves the turn on the caller's
|
|
35
|
+
own plan at the subscription fee.
|
|
36
|
+
|
|
37
|
+
## Division of responsibility
|
|
38
|
+
|
|
39
|
+
- **Installer** writes the router key + identity headers (`X-Weave-Router-Key`,
|
|
40
|
+
`X-App`, `X-Weave-User-Email`) into `opencode.json` `options.headers`.
|
|
41
|
+
- **This plugin** manages only the dynamic, secret, refreshable subscription
|
|
42
|
+
credential (the `Authorization` bearer + `ChatGPT-Account-Id`) via the auth
|
|
43
|
+
`loader`, and the login flow via auth `methods` (browser PKCE + headless
|
|
44
|
+
device code). Tokens are stored in opencode's own auth store under the
|
|
45
|
+
`weave-codex` provider id.
|
|
46
|
+
|
|
47
|
+
## Env overrides
|
|
48
|
+
|
|
49
|
+
- `WEAVE_CODEX_OAUTH_ISSUER` — override the OpenAI auth issuer (self-hosted
|
|
50
|
+
OpenAI auth proxies; also used by the capture tests).
|
|
51
|
+
|
|
52
|
+
## Verification
|
|
53
|
+
|
|
54
|
+
Capture-tested end-to-end against a local server standing in for the router
|
|
55
|
+
(opencode 1.17.9, real ChatGPT login): the inject path forwards the real JWT +
|
|
56
|
+
account-id in Responses format to `/v1/responses`, and the refresh path detects
|
|
57
|
+
an expired token, refreshes against the issuer, rotates + persists the tokens,
|
|
58
|
+
and injects the refreshed bearer. Typechecks under `strict` against
|
|
59
|
+
`@opencode-ai/plugin`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workweave/router-opencode-extension",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The opencode Codex-subscription auth plugin bundled into @workweave/router. NOT published separately: src/ is copied into the @workweave/router package at prepack and dropped into the user's opencode plugins dir by install.sh (--codex/--opencode). Lets a caller's own ChatGPT (Codex) subscription pay for their opencode turns, routed through the Weave Router. This manifest just marks the sources as ESM and declares the opencode peer dep for local typecheck.",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"@opencode-ai/plugin": "*"
|
|
8
|
+
}
|
|
9
|
+
}
|