@workweave/router 0.2.10 → 0.2.11
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 +15 -3
- package/cc-statusline.sh +179 -7
- package/codex-skills/force-model/SKILL.md +14 -0
- package/codex-skills/router-feedback/SKILL.md +14 -0
- package/codex-skills/unforce-model/SKILL.md +14 -0
- package/commands/models.md +46 -0
- package/commands/router-models.md +46 -0
- package/directives.tsv +12 -0
- package/install.sh +1036 -78
- package/package.json +6 -1
- package/pi-router/README.md +36 -7
- package/pi-router/skills/install-lsps/SKILL.md +75 -0
- package/pi-router/skills/lsp-guide/SKILL.md +63 -0
- package/pi-router/src/compaction.ts +46 -8
- package/pi-router/src/config.ts +34 -5
- package/pi-router/src/context-window.ts +12 -0
- package/pi-router/src/dispatch.ts +35 -2
- package/pi-router/src/index.ts +10 -1
- package/pi-router/src/lsp-broker.ts +255 -0
- package/pi-router/src/lsp-client.ts +435 -0
- package/pi-router/src/lsp-format.ts +230 -0
- package/pi-router/src/lsp-install.ts +215 -0
- package/pi-router/src/lsp-protocol.ts +128 -0
- package/pi-router/src/lsp-servers.ts +361 -0
- package/pi-router/src/lsp.ts +529 -0
- package/pi-router/src/pricing.generated.ts +6 -1
- package/pi-router/src/provider.ts +15 -2
- package/pi-router/src/routed-model.ts +17 -0
- package/registry.sh +100 -0
- package/uninstall.sh +173 -30
package/install.sh
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
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
|
+
# npx @workweave/router --pi --lsp go,typescript # also install language servers for pi's lsp tool
|
|
41
|
+
# (go/typescript/python/rust; needs that language's toolchain)
|
|
40
42
|
# npx @workweave/router --scope project # commit-with-team install
|
|
41
43
|
# npx @workweave/router --dir /tmp/my-sandbox # isolated throwaway install
|
|
42
44
|
# npx @workweave/router --local # local router on localhost:8080
|
|
@@ -64,6 +66,28 @@
|
|
|
64
66
|
# `claude` start; Codex and opencode re-read config every invocation.
|
|
65
67
|
# Cursor's base URL lives in its own settings UI (no file we own), so there's
|
|
66
68
|
# nothing to toggle here — flip "Override OpenAI Base URL" in Cursor settings.
|
|
69
|
+
#
|
|
70
|
+
# Which router directives each client gets is declared once in
|
|
71
|
+
# install/directives.tsv and installed from there: Claude Code and opencode get
|
|
72
|
+
# Markdown slash commands, Codex gets native `$name` skills (it reserves `/…`
|
|
73
|
+
# for built-ins), pi registers /fm and /ufm in its extension, and Cursor is
|
|
74
|
+
# manual. See install/README.md for the full matrix.
|
|
75
|
+
#
|
|
76
|
+
# Inspect and edit which models this installation lets the router pick from —
|
|
77
|
+
# the same lists the router dashboard's settings page renders. The endpoint and
|
|
78
|
+
# router key both come from the install already on disk, so nothing is prompted
|
|
79
|
+
# and no key is ever passed on the command line:
|
|
80
|
+
# npx @workweave/router models --claude # every model, with its on/off state
|
|
81
|
+
# npx @workweave/router models disable gpt-5.6 --claude # take a model out of rotation
|
|
82
|
+
# npx @workweave/router models enable gpt-5.6 --claude # put it back
|
|
83
|
+
# npx @workweave/router models providers --claude # same, one row per provider
|
|
84
|
+
# npx @workweave/router models providers disable openai --claude # drop a whole provider
|
|
85
|
+
# npx @workweave/router models prefer claude-opus-5 --claude # set the preferred-model ranking
|
|
86
|
+
# npx @workweave/router models list --json --claude # machine-readable, for scripts
|
|
87
|
+
# Editing needs a router that mounts the model-selection API (self-hosted and
|
|
88
|
+
# local routers do). The Weave-hosted router keeps model selection with the
|
|
89
|
+
# organization, in the Weave dashboard — there `models` lists and points you at
|
|
90
|
+
# it rather than editing.
|
|
67
91
|
|
|
68
92
|
set -euo pipefail
|
|
69
93
|
|
|
@@ -98,9 +122,22 @@ target_explicit="false"
|
|
|
98
122
|
# never-prompting install that reuses the key already on disk. "off"/"on"/
|
|
99
123
|
# "status" toggle or report an existing install without touching the router
|
|
100
124
|
# key/identity — see the toggle_* helpers and the dispatch block below.
|
|
125
|
+
# "models" reads and edits the installation's model selection over the router's
|
|
126
|
+
# admin API; it touches no local config at all.
|
|
101
127
|
mode="install"
|
|
102
128
|
disable_routing_alias="false"
|
|
103
129
|
|
|
130
|
+
# `models` sub-verb plus its operands (model / provider ids), newline-delimited
|
|
131
|
+
# in argument order. Catalog ids and provider names never contain whitespace,
|
|
132
|
+
# so a newline-delimited string stays readable where a bash 3.2 array under
|
|
133
|
+
# `set -u` would not. --json switches the output to the raw API payload.
|
|
134
|
+
models_args=""
|
|
135
|
+
models_json="false"
|
|
136
|
+
|
|
137
|
+
# --lsp <langs>: comma-separated languages whose language servers to install
|
|
138
|
+
# for the pi extension's `lsp` tool (pi target only). Empty = don't install any.
|
|
139
|
+
lsp_langs=""
|
|
140
|
+
|
|
104
141
|
# --rotate-key forces the interactive key prompt even when a usable key is
|
|
105
142
|
# already installed, so a rotated key can replace it.
|
|
106
143
|
rotate_key="false"
|
|
@@ -486,14 +523,9 @@ write_codex_config() {
|
|
|
486
523
|
# that share the same router key. The router otherwise has to guess from
|
|
487
524
|
# User-Agent.
|
|
488
525
|
headers_parts="${headers_parts}, \"X-App\" = \"codex\""
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
# sidecar, so every custom URL deliberately keeps the router's own default.
|
|
493
|
-
# Keep this inside the managed block so a re-install can change it safely.
|
|
494
|
-
if [ "$block_url" = "$HOSTED_BASE_URL" ]; then
|
|
495
|
-
headers_parts="${headers_parts}, \"X-Weave-Router-Strategy\" = \"hmm\""
|
|
496
|
-
fi
|
|
526
|
+
# No strategy header: pinning one here freezes installed clients on whatever
|
|
527
|
+
# policy was current at install time, so a deployment-default change never
|
|
528
|
+
# reaches them. Every endpoint, hosted or self-hosted, uses its own default.
|
|
497
529
|
local headers_line="http_headers = { ${headers_parts} }"
|
|
498
530
|
|
|
499
531
|
local block
|
|
@@ -803,8 +835,8 @@ write_pi_models_config() {
|
|
|
803
835
|
models: [
|
|
804
836
|
{ id: "claude-fable-5", name: "Claude Fable 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
|
|
805
837
|
{ id: "claude-opus-5", name: "Claude Opus 5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 128000 },
|
|
806
|
-
{ id: "claude-opus-4-7", name: "Claude Opus 4.7 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow:
|
|
807
|
-
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow:
|
|
838
|
+
{ id: "claude-opus-4-7", name: "Claude Opus 4.7 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 64000 },
|
|
839
|
+
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1000000, maxTokens: 64000 },
|
|
808
840
|
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 200000, maxTokens: 32000 },
|
|
809
841
|
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 1050000, maxTokens: 128000 },
|
|
810
842
|
{ id: "grok-4.5", name: "Grok 4.5 (via Weave Router)", reasoning: true, input: ["text","image"], contextWindow: 500000, maxTokens: 131072 },
|
|
@@ -859,6 +891,69 @@ write_pi_settings_config() {
|
|
|
859
891
|
printf '%s\n' "$merged" >"$settings_file"
|
|
860
892
|
}
|
|
861
893
|
|
|
894
|
+
# Language-server installs for the pi extension's `lsp` tool (--lsp go,ts,...).
|
|
895
|
+
# The alias -> server -> toolchain matrix mirrors LSP_SERVERS in
|
|
896
|
+
# install/pi-router/src/lsp-servers.ts — keep the two in lockstep. Every
|
|
897
|
+
# failure is a warn-and-continue: a missing toolchain must not fail the router
|
|
898
|
+
# install, and the extension re-offers conversationally at session start.
|
|
899
|
+
install_lsp_servers() {
|
|
900
|
+
local langs="$1" lang
|
|
901
|
+
local seen=" "
|
|
902
|
+
for lang in $(printf '%s' "$langs" | tr ',' ' '); do
|
|
903
|
+
local id="" bin="" alt_bin="" toolchain="" fallback_dir="" gopath=""
|
|
904
|
+
local cmd=""
|
|
905
|
+
case "$(printf '%s' "$lang" | tr '[:upper:]' '[:lower:]')" in
|
|
906
|
+
go|golang)
|
|
907
|
+
id="go"; bin="gopls"; toolchain="go"
|
|
908
|
+
# go install honors $GOBIN, else <first GOPATH element>/bin (default ~/go/bin).
|
|
909
|
+
gopath="${GOPATH:-$HOME/go}"
|
|
910
|
+
fallback_dir="${GOBIN:-${gopath%%:*}/bin}"
|
|
911
|
+
cmd="go install golang.org/x/tools/gopls@latest"
|
|
912
|
+
;;
|
|
913
|
+
ts|typescript|js|javascript)
|
|
914
|
+
id="typescript"; bin="typescript-language-server"; toolchain="npm"
|
|
915
|
+
cmd="npm i -g typescript-language-server typescript"
|
|
916
|
+
;;
|
|
917
|
+
py|python)
|
|
918
|
+
id="python"; bin="pyright-langserver"; alt_bin="basedpyright-langserver"; toolchain="npm"
|
|
919
|
+
cmd="npm i -g pyright"
|
|
920
|
+
;;
|
|
921
|
+
rs|rust)
|
|
922
|
+
id="rust"; bin="rust-analyzer"; toolchain="rustup"
|
|
923
|
+
# rustup honors $CARGO_HOME (default ~/.cargo).
|
|
924
|
+
fallback_dir="${CARGO_HOME:-$HOME/.cargo}/bin"
|
|
925
|
+
cmd="rustup component add rust-analyzer"
|
|
926
|
+
;;
|
|
927
|
+
*)
|
|
928
|
+
warn "--lsp: unknown language '$lang' (valid: go, typescript, python, rust). Skipping."
|
|
929
|
+
continue
|
|
930
|
+
;;
|
|
931
|
+
esac
|
|
932
|
+
case "$seen" in *" $id "*) continue ;; esac
|
|
933
|
+
seen="$seen$id "
|
|
934
|
+
|
|
935
|
+
if command -v "$bin" >/dev/null 2>&1 \
|
|
936
|
+
|| { [ -n "$alt_bin" ] && command -v "$alt_bin" >/dev/null 2>&1; } \
|
|
937
|
+
|| { [ -n "$fallback_dir" ] && [ -x "$fallback_dir/$bin" ]; }; then
|
|
938
|
+
ok "$id language server already installed ($bin)"
|
|
939
|
+
continue
|
|
940
|
+
fi
|
|
941
|
+
if ! command -v "$toolchain" >/dev/null 2>&1; then
|
|
942
|
+
warn "--lsp $id: needs '$toolchain' on PATH (install command: $cmd). Skipping — re-run after installing the $toolchain toolchain."
|
|
943
|
+
continue
|
|
944
|
+
fi
|
|
945
|
+
# shellcheck disable=SC2086 — $cmd is a fixed argv from the case above, never user input.
|
|
946
|
+
if spin "Installing $id language server" $cmd; then
|
|
947
|
+
ok "$id language server installed ($cmd)"
|
|
948
|
+
if [ -n "$fallback_dir" ] && ! command -v "$bin" >/dev/null 2>&1; then
|
|
949
|
+
info "$bin landed in $fallback_dir (not on PATH). The pi lsp tool finds it there; other tools may need a PATH entry."
|
|
950
|
+
fi
|
|
951
|
+
else
|
|
952
|
+
warn "--lsp $id: '$cmd' failed. Run it manually, or ask pi to enable $id LSP support later."
|
|
953
|
+
fi
|
|
954
|
+
done
|
|
955
|
+
}
|
|
956
|
+
|
|
862
957
|
# resolve_user_name mirrors resolve_user_email but for display name. Priority:
|
|
863
958
|
# WEAVE_USER_NAME env override → git config user.name → empty. We don't
|
|
864
959
|
# prompt for name independently: if email prompting yielded nothing, name
|
|
@@ -972,6 +1067,10 @@ while [ $# -gt 0 ]; do
|
|
|
972
1067
|
--pi)
|
|
973
1068
|
target="pi"; target_explicit="true"; shift
|
|
974
1069
|
;;
|
|
1070
|
+
--lsp)
|
|
1071
|
+
lsp_langs="${2:-}"; shift 2
|
|
1072
|
+
[ -n "$lsp_langs" ] || { err "--lsp requires a comma-separated language list (go,typescript,python,rust)."; exit 2; }
|
|
1073
|
+
;;
|
|
975
1074
|
--claude)
|
|
976
1075
|
# No-op selector for symmetry with --codex / --opencode. Useful in
|
|
977
1076
|
# pipelines that want to skip the interactive picker without depending
|
|
@@ -994,15 +1093,44 @@ while [ $# -gt 0 ]; do
|
|
|
994
1093
|
# so `disable-routing --claude` cannot silently change Claude settings.
|
|
995
1094
|
mode="off"; disable_routing_alias="true"; shift
|
|
996
1095
|
;;
|
|
1096
|
+
models|--models)
|
|
1097
|
+
# Model selection. Sub-verb and operand collection happens in the
|
|
1098
|
+
# catch-all arm below (guarded on mode="models"), not here — that lets
|
|
1099
|
+
# `--claude`/`--json`/etc. appear before, after, or between operands
|
|
1100
|
+
# instead of only after every operand has been consumed.
|
|
1101
|
+
mode="models"; shift
|
|
1102
|
+
;;
|
|
1103
|
+
--json)
|
|
1104
|
+
models_json="true"; shift
|
|
1105
|
+
;;
|
|
997
1106
|
-h|--help)
|
|
998
1107
|
usage 0
|
|
999
1108
|
;;
|
|
1000
1109
|
*)
|
|
1001
|
-
|
|
1110
|
+
# In `models` mode, a bare (non-dashed) word is a sub-verb or operand —
|
|
1111
|
+
# collect it and keep parsing, so a flag can appear before, after, or
|
|
1112
|
+
# between them (`models --claude enable x` and `models enable x --claude`
|
|
1113
|
+
# both work). A dashed token nothing above matched is still an error,
|
|
1114
|
+
# models mode or not.
|
|
1115
|
+
if [ "$mode" = "models" ] && [ "${1#-}" = "$1" ]; then
|
|
1116
|
+
models_args="${models_args}${models_args:+$'\n'}$1"; shift
|
|
1117
|
+
else
|
|
1118
|
+
err "Unknown flag: $1."; usage 2
|
|
1119
|
+
fi
|
|
1002
1120
|
;;
|
|
1003
1121
|
esac
|
|
1004
1122
|
done
|
|
1005
1123
|
|
|
1124
|
+
# --lsp is a pi-extension feature, so it implies the pi target; combining it
|
|
1125
|
+
# with another explicit target is a contradiction, not a preference.
|
|
1126
|
+
if [ -n "$lsp_langs" ]; then
|
|
1127
|
+
if [ "$target_explicit" = "true" ] && [ "$target" != "pi" ]; then
|
|
1128
|
+
err "--lsp only applies to the pi target (the lsp tool ships in the pi extension). Drop --lsp or use --pi."
|
|
1129
|
+
exit 2
|
|
1130
|
+
fi
|
|
1131
|
+
target="pi"; target_explicit="true"
|
|
1132
|
+
fi
|
|
1133
|
+
|
|
1006
1134
|
if [ "$disable_routing_alias" = "true" ]; then
|
|
1007
1135
|
if [ "$target_explicit" = "true" ] && [ "$target" != "codex" ]; then
|
|
1008
1136
|
err "disable-routing only applies to Codex; omit the client flag or use --codex."
|
|
@@ -1014,15 +1142,39 @@ fi
|
|
|
1014
1142
|
|
|
1015
1143
|
# Toggle verbs only flip config install.sh already wrote: no key, no identity,
|
|
1016
1144
|
# no prompts. Require an explicit client so we never guess which config to
|
|
1017
|
-
# touch, and suppress every interactive prompt downstream.
|
|
1145
|
+
# touch, and suppress every interactive prompt downstream. `models` edits no
|
|
1146
|
+
# local config, but it still reads the endpoint and key out of one install, so
|
|
1147
|
+
# it needs the same explicit choice.
|
|
1018
1148
|
if [ "$mode" != "install" ] && [ "$mode" != "update" ]; then
|
|
1019
1149
|
non_interactive="true"
|
|
1020
1150
|
if [ "$target_explicit" != "true" ]; then
|
|
1021
|
-
|
|
1151
|
+
if [ "$mode" = "models" ]; then
|
|
1152
|
+
err "'models' requires an explicit client: --claude."
|
|
1153
|
+
else
|
|
1154
|
+
err "'$mode' requires an explicit client: --claude, --codex, or --opencode."
|
|
1155
|
+
fi
|
|
1022
1156
|
exit 2
|
|
1023
1157
|
fi
|
|
1024
1158
|
fi
|
|
1025
1159
|
|
|
1160
|
+
# Only Claude Code's settings are read back for a key today (read_installed_key),
|
|
1161
|
+
# so `models` can't authenticate as any other client. Fail fast here rather than
|
|
1162
|
+
# falling through to the toggle dispatch, which would flip config nobody asked
|
|
1163
|
+
# to change.
|
|
1164
|
+
if [ "$mode" = "models" ] && [ "$target" != "claude" ]; then
|
|
1165
|
+
err "'models' currently supports --claude only (it reads the router key from Claude Code's settings)."
|
|
1166
|
+
exit 2
|
|
1167
|
+
fi
|
|
1168
|
+
|
|
1169
|
+
# --json prints the API payload verbatim, so nothing else may reach stdout.
|
|
1170
|
+
if [ "$models_json" = "true" ]; then
|
|
1171
|
+
if [ "$mode" != "models" ]; then
|
|
1172
|
+
err "--json only applies to 'models'."
|
|
1173
|
+
exit 2
|
|
1174
|
+
fi
|
|
1175
|
+
quiet="true"
|
|
1176
|
+
fi
|
|
1177
|
+
|
|
1026
1178
|
# `update` is an install that never prompts: it refreshes the managed config
|
|
1027
1179
|
# and assets using the key already on disk (or in the environment). Suppressing
|
|
1028
1180
|
# prompts here also settles the target/scope pickers below on their defaults,
|
|
@@ -1156,7 +1308,9 @@ fi
|
|
|
1156
1308
|
|
|
1157
1309
|
# ---------- pre-flight ----------
|
|
1158
1310
|
|
|
1159
|
-
|
|
1311
|
+
# `models` skips this line: it prints its own header carrying the endpoint it
|
|
1312
|
+
# resolved, which is the part a reader actually needs.
|
|
1313
|
+
if [ "$mode" != "install" ] && [ "$mode" != "update" ] && [ "$mode" != "models" ]; then
|
|
1160
1314
|
[ "$quiet" = "true" ] || info "mode=${C_BOLD}${mode}${C_RESET} scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET}"
|
|
1161
1315
|
fi
|
|
1162
1316
|
|
|
@@ -1167,12 +1321,17 @@ fi
|
|
|
1167
1321
|
if [ "$target" = "claude" ] || [ "$target" = "opencode" ] || [ "$target" = "pi" ]; then
|
|
1168
1322
|
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
1169
1323
|
fi
|
|
1170
|
-
# curl is
|
|
1171
|
-
# toggles never hit the network.
|
|
1172
|
-
if [ "$mode" = "install" ] || [ "$mode" = "update" ]; then
|
|
1324
|
+
# curl is used by the install/update paths' health/validate probes and by every
|
|
1325
|
+
# `models` call; the on/off/status toggles never hit the network.
|
|
1326
|
+
if [ "$mode" = "install" ] || [ "$mode" = "update" ] || [ "$mode" = "models" ]; then
|
|
1173
1327
|
require_cmd curl "macOS/Linux: usually preinstalled — check your package manager"
|
|
1174
1328
|
fi
|
|
1175
1329
|
|
|
1330
|
+
# Every warning below says the client's config will be written anyway, which is
|
|
1331
|
+
# true for install/update/toggles and false for `models` — it only reads that
|
|
1332
|
+
# config to find the router. Whether the client itself is installed has no
|
|
1333
|
+
# bearing on a model-selection call, so skip the check there entirely.
|
|
1334
|
+
if [ "$mode" != "models" ]; then
|
|
1176
1335
|
case "$target" in
|
|
1177
1336
|
claude)
|
|
1178
1337
|
if ! command -v claude >/dev/null 2>&1; then
|
|
@@ -1199,9 +1358,118 @@ case "$target" in
|
|
|
1199
1358
|
fi
|
|
1200
1359
|
;;
|
|
1201
1360
|
esac
|
|
1361
|
+
fi
|
|
1202
1362
|
|
|
1203
1363
|
script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
1204
1364
|
|
|
1365
|
+
# ---------- directive registry (embedded) ----------
|
|
1366
|
+
#
|
|
1367
|
+
# install.sh is served standalone (WorkWeave serves it for `curl | sh`), so it
|
|
1368
|
+
# cannot source install/registry.sh or read install/directives.tsv. Both are
|
|
1369
|
+
# embedded here verbatim; install/tests/registry_test.sh asserts they never
|
|
1370
|
+
# drift from the canonical files.
|
|
1371
|
+
WEAVE_REGISTRY_DATA=$(cat <<'WEAVE_REGISTRY_EOF'
|
|
1372
|
+
# Weave Router directive registry
|
|
1373
|
+
# canonical|aliases|capability|claude|codex|opencode|pi|cursor|adapter
|
|
1374
|
+
# aliases are comma-separated; client columns are yes/no. adapter is the native asset kind.
|
|
1375
|
+
force-model|fm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
1376
|
+
unforce-model|ufm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
1377
|
+
router-feedback|rf|prompt|yes|yes|yes|no|manual|command,skill
|
|
1378
|
+
router-off||local-toggle|yes|no|no|no|manual|command
|
|
1379
|
+
router-on||local-toggle|yes|no|no|no|manual|command
|
|
1380
|
+
router-status||local-toggle|yes|no|no|no|manual|command
|
|
1381
|
+
router-session||prompt|yes|no|no|no|manual|command
|
|
1382
|
+
router-models|models|local-toggle|yes|no|no|no|manual|command
|
|
1383
|
+
disable-routing||local-toggle|no|yes|no|no|manual|skill
|
|
1384
|
+
WEAVE_REGISTRY_EOF
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
# >>> weave-router registry lib >>>
|
|
1388
|
+
weave_registry_rows() {
|
|
1389
|
+
if [ -n "${WEAVE_REGISTRY_DATA:-}" ]; then
|
|
1390
|
+
printf '%s\n' "$WEAVE_REGISTRY_DATA" | awk -F '|' '!/^([[:space:]]*#|[[:space:]]*$)/ { print }'
|
|
1391
|
+
return 0
|
|
1392
|
+
fi
|
|
1393
|
+
local registry="${WEAVE_REGISTRY_FILE:-}"
|
|
1394
|
+
if [ -z "$registry" ]; then
|
|
1395
|
+
local dir
|
|
1396
|
+
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd -P)" || return 1
|
|
1397
|
+
registry="$dir/directives.tsv"
|
|
1398
|
+
fi
|
|
1399
|
+
[ -f "$registry" ] || return 1
|
|
1400
|
+
awk -F '|' '!/^([[:space:]]*#|[[:space:]]*$)/ { print }' "$registry"
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
# weave_registry_names lists every canonical name and alias a client installs.
|
|
1404
|
+
weave_registry_names() {
|
|
1405
|
+
local target="$1" canonical aliases capability claude codex opencode pi cursor adapter
|
|
1406
|
+
while IFS='|' read -r canonical aliases capability claude codex opencode pi cursor adapter; do
|
|
1407
|
+
case "$target" in
|
|
1408
|
+
claude) [ "$claude" = yes ] || continue ;; codex) [ "$codex" = yes ] || continue ;;
|
|
1409
|
+
opencode) [ "$opencode" = yes ] || continue ;; pi) [ "$pi" = yes ] || continue ;;
|
|
1410
|
+
cursor) [ "$cursor" = yes ] || continue ;;
|
|
1411
|
+
esac
|
|
1412
|
+
printf '%s\n' "$canonical"
|
|
1413
|
+
[ -n "$aliases" ] && printf '%s\n' "$aliases" | tr ',' '\n'
|
|
1414
|
+
done <<EOF
|
|
1415
|
+
$(weave_registry_rows)
|
|
1416
|
+
EOF
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
# weave_registry_skill_names lists the prompt directives a client exposes as a
|
|
1420
|
+
# native skill. Local-config toggles are excluded: they mutate config this
|
|
1421
|
+
# installer owns and are handled per target, not as a generic prompt.
|
|
1422
|
+
weave_registry_skill_names() {
|
|
1423
|
+
local target="$1" canonical aliases capability claude codex opencode pi cursor adapter
|
|
1424
|
+
while IFS='|' read -r canonical aliases capability claude codex opencode pi cursor adapter; do
|
|
1425
|
+
[ "$capability" = prompt ] || continue
|
|
1426
|
+
case "$target" in
|
|
1427
|
+
claude) [ "$claude" = yes ] || continue ;; codex) [ "$codex" = yes ] || continue ;;
|
|
1428
|
+
opencode) [ "$opencode" = yes ] || continue ;; pi) [ "$pi" = yes ] || continue ;;
|
|
1429
|
+
cursor) [ "$cursor" = yes ] || continue ;;
|
|
1430
|
+
esac
|
|
1431
|
+
printf '%s\n' "$canonical"
|
|
1432
|
+
done <<EOF
|
|
1433
|
+
$(weave_registry_rows)
|
|
1434
|
+
EOF
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
# weave_registry_skill_assets lists every directive a client ships as a skill
|
|
1438
|
+
# file, including local-config toggles such as Codex's disable-routing. Install
|
|
1439
|
+
# and uninstall use this for file management; weave_registry_skill_names is the
|
|
1440
|
+
# narrower prompt-only set used when generating prompt adapters.
|
|
1441
|
+
weave_registry_skill_assets() {
|
|
1442
|
+
local target="$1" canonical aliases capability claude codex opencode pi cursor adapter
|
|
1443
|
+
while IFS='|' read -r canonical aliases capability claude codex opencode pi cursor adapter; do
|
|
1444
|
+
case ",$adapter," in *,skill,*) ;; *) continue ;; esac
|
|
1445
|
+
case "$target" in
|
|
1446
|
+
claude) [ "$claude" = yes ] || continue ;; codex) [ "$codex" = yes ] || continue ;;
|
|
1447
|
+
opencode) [ "$opencode" = yes ] || continue ;; pi) [ "$pi" = yes ] || continue ;;
|
|
1448
|
+
cursor) [ "$cursor" = yes ] || continue ;;
|
|
1449
|
+
esac
|
|
1450
|
+
printf '%s\n' "$canonical"
|
|
1451
|
+
done <<EOF
|
|
1452
|
+
$(weave_registry_rows)
|
|
1453
|
+
EOF
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
# weave_registry_canonical_for resolves a name or alias to its canonical
|
|
1457
|
+
# directive, and fails when the name is not in the registry at all.
|
|
1458
|
+
weave_registry_canonical_for() {
|
|
1459
|
+
local wanted="$1" canonical aliases capability claude codex opencode pi cursor adapter alias
|
|
1460
|
+
while IFS='|' read -r canonical aliases capability claude codex opencode pi cursor adapter; do
|
|
1461
|
+
[ "$wanted" = "$canonical" ] && { printf '%s' "$canonical"; return 0; }
|
|
1462
|
+
IFS=',' read -ra _aliases <<< "$aliases"
|
|
1463
|
+
for alias in ${_aliases[@]+"${_aliases[@]}"}; do
|
|
1464
|
+
[ "$wanted" = "$alias" ] && { printf '%s' "$canonical"; return 0; }
|
|
1465
|
+
done
|
|
1466
|
+
done <<EOF
|
|
1467
|
+
$(weave_registry_rows)
|
|
1468
|
+
EOF
|
|
1469
|
+
return 1
|
|
1470
|
+
}
|
|
1471
|
+
# <<< weave-router registry lib <<<
|
|
1472
|
+
|
|
1205
1473
|
# Resolve the base directory. User scope always uses $HOME. Project scope uses
|
|
1206
1474
|
# --dir if given, otherwise the CWD's git root. --dir alone (no --scope) is a
|
|
1207
1475
|
# throwaway user-style install.
|
|
@@ -1396,6 +1664,105 @@ claude_key_present() {
|
|
|
1396
1664
|
[ -n "$(read_claude_key "$1")" ]
|
|
1397
1665
|
}
|
|
1398
1666
|
|
|
1667
|
+
# read_installed_key prints the router key this install already has on disk, or
|
|
1668
|
+
# nothing. Only Claude Code is supported today; the other targets still prompt.
|
|
1669
|
+
# Defined up here rather than with the rest of token handling because `models`
|
|
1670
|
+
# dispatches (and needs a key) before that section runs.
|
|
1671
|
+
#
|
|
1672
|
+
# models_key_file_order echoes the precedence this uses, so a caller that needs
|
|
1673
|
+
# to know *which* file the key came from can walk the same list (a command
|
|
1674
|
+
# substitution around this function would discard any global it set).
|
|
1675
|
+
models_key_file_order() {
|
|
1676
|
+
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1677
|
+
printf '%s\n%s\n%s\n' "$local_settings_file" "$settings_file" "$settings_dir/.weave-parked.json"
|
|
1678
|
+
else
|
|
1679
|
+
printf '%s\n%s\n%s\n' "$settings_file" "$local_settings_file" "$settings_dir/.weave-parked.json"
|
|
1680
|
+
fi
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
read_installed_key() {
|
|
1684
|
+
[ "$target" = "claude" ] || return 0
|
|
1685
|
+
local key="" candidate
|
|
1686
|
+
# Mirror where the install path writes the key: project scope (no --dir) puts
|
|
1687
|
+
# it in the gitignored settings.local.json, everything else inlines it into
|
|
1688
|
+
# settings.json. Check the other file too — a scope was possibly changed, or a
|
|
1689
|
+
# project checkout may carry a committed header from an older install.
|
|
1690
|
+
# The parked sidecar is last: `off` moves the key header out of the settings
|
|
1691
|
+
# files and into it, so a run while toggled off finds nothing above even
|
|
1692
|
+
# though the key is still on disk. Same {"env":{…}} shape, so read_claude_key
|
|
1693
|
+
# reads it directly.
|
|
1694
|
+
while IFS= read -r candidate; do
|
|
1695
|
+
[ -n "$candidate" ] || continue
|
|
1696
|
+
key="$(read_claude_key "$candidate")"
|
|
1697
|
+
[ -n "$key" ] && break
|
|
1698
|
+
done <<<"$(models_key_file_order)"
|
|
1699
|
+
printf '%s' "$key"
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
# resolve_installed_base_url prints the router endpoint this Claude Code install
|
|
1703
|
+
# already points at, or nothing when none of its files carry a router-shaped
|
|
1704
|
+
# one. The parked sidecar comes first: `off` moves the router URL there and
|
|
1705
|
+
# leaves api.anthropic.com in the live file, so reading the live file while
|
|
1706
|
+
# toggled off would report Anthropic as the router.
|
|
1707
|
+
#
|
|
1708
|
+
# models_base_file_order echoes that same precedence for callers that need to
|
|
1709
|
+
# know which file supplied the endpoint (see models_key_file_order).
|
|
1710
|
+
models_base_file_order() {
|
|
1711
|
+
printf '%s\n%s\n%s\n' "$settings_dir/.weave-parked.json" "$settings_file" "$local_settings_file"
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
resolve_installed_base_url() {
|
|
1715
|
+
local candidate found
|
|
1716
|
+
while IFS= read -r candidate; do
|
|
1717
|
+
[ -n "$candidate" ] || continue
|
|
1718
|
+
found="$(json_get "$candidate" '.env.ANTHROPIC_BASE_URL')"
|
|
1719
|
+
if router_shaped_url "$found"; then
|
|
1720
|
+
printf '%s' "${found%/}"
|
|
1721
|
+
return 0
|
|
1722
|
+
fi
|
|
1723
|
+
done <<<"$(models_base_file_order)"
|
|
1724
|
+
printf ''
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
# models_endpoint_is_trusted returns 0 when it is safe to send the router key
|
|
1728
|
+
# resolved from $2 to the endpoint resolved from $1.
|
|
1729
|
+
#
|
|
1730
|
+
# Project scope deliberately splits the two: the endpoint lives in the
|
|
1731
|
+
# committed settings.json (teammates share it) while each teammate's key lives
|
|
1732
|
+
# in the gitignored settings.local.json. That split is fine on its own, but it
|
|
1733
|
+
# means a *hostile* repo can commit a settings.json naming an endpoint it
|
|
1734
|
+
# controls and have this command mail the developer's key to it. Endpoint and
|
|
1735
|
+
# credential from the same file are self-consistent and always fine. When they
|
|
1736
|
+
# differ, the endpoint must be one the user vouched for out-of-band — the
|
|
1737
|
+
# hosted default, or an explicit --base-url — rather than whatever the checkout
|
|
1738
|
+
# happened to contain.
|
|
1739
|
+
models_endpoint_is_trusted() {
|
|
1740
|
+
local url="$1" base_src="$2" key_src="$3"
|
|
1741
|
+
[ "$base_url_explicit" = "true" ] && return 0
|
|
1742
|
+
[ "$url" = "$HOSTED_BASE_URL" ] && return 0
|
|
1743
|
+
[ -n "$base_src" ] && [ "$base_src" = "$key_src" ] && return 0
|
|
1744
|
+
# Project-scoped self-hosted installs intentionally split the endpoint and
|
|
1745
|
+
# key. The installer writes a gitignored marker beside the key; require that
|
|
1746
|
+
# marker to match before trusting the split. A tracked/symlinked local file is
|
|
1747
|
+
# not a teammate's private configuration and cannot vouch for an endpoint.
|
|
1748
|
+
if [ "$key_src" = "$local_settings_file" ] && [ ! -L "$key_src" ]; then
|
|
1749
|
+
local marked_url
|
|
1750
|
+
marked_url="$(json_get "$key_src" '.env.WEAVE_ROUTER_BASE_URL')"
|
|
1751
|
+
if [ "${marked_url%/}" = "${url%/}" ]; then
|
|
1752
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
1753
|
+
git -C "$(dirname "$key_src")" ls-files --error-unmatch -- "$key_src" >/dev/null 2>&1 && return 1
|
|
1754
|
+
return 0
|
|
1755
|
+
fi
|
|
1756
|
+
fi
|
|
1757
|
+
# A repo can only pre-plant a file git tracks; an untracked local file is the
|
|
1758
|
+
# user's own. If the endpoint's file isn't tracked, it wasn't planted. Checked
|
|
1759
|
+
# inline rather than via weave_command_tracked_by_git, which is defined further
|
|
1760
|
+
# down (inside the statusline heredoc's neighborhood) and isn't in scope here.
|
|
1761
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
1762
|
+
git -C "$(dirname "$base_src")" ls-files --error-unmatch -- "$base_src" >/dev/null 2>&1 || return 0
|
|
1763
|
+
return 1
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1399
1766
|
# gitignore_add appends an entry to the repo .gitignore in project scope so a
|
|
1400
1767
|
# parked sidecar (which may carry the router key header) never gets committed.
|
|
1401
1768
|
# No-op for user scope and --dir, matching how install handles its own ignores.
|
|
@@ -1698,6 +2065,393 @@ toggle_opencode() {
|
|
|
1698
2065
|
esac
|
|
1699
2066
|
}
|
|
1700
2067
|
|
|
2068
|
+
# ---------- model selection ----------
|
|
2069
|
+
#
|
|
2070
|
+
# `models` reads and edits the installation's model/provider selection through
|
|
2071
|
+
# the router's /admin/v1 API — the same lists the router dashboard's settings
|
|
2072
|
+
# page renders, backed by the same columns. Nothing local is written: the
|
|
2073
|
+
# endpoint and router key are read out of the install already on disk so a
|
|
2074
|
+
# self-hosted install talks to its own router with its own key.
|
|
2075
|
+
#
|
|
2076
|
+
# The Weave-hosted router runs in `managed` mode and mounts no /admin/v1 at
|
|
2077
|
+
# all — there model selection belongs to the organization and is edited in the
|
|
2078
|
+
# Weave dashboard. That surfaces as a 404, which reads (for a list) as "fall
|
|
2079
|
+
# back to the public catalog and say where to edit" and (for an edit) as a
|
|
2080
|
+
# refusal naming the dashboard.
|
|
2081
|
+
|
|
2082
|
+
# Public, unauthed catalog of everything the router can route to. Mounted in
|
|
2083
|
+
# both deployment modes, so it is the one listing that always works.
|
|
2084
|
+
MODELS_CATALOG_PATH="/v1/router/models"
|
|
2085
|
+
MODELS_DASHBOARD_URL="https://router.workweave.ai/dashboard/settings"
|
|
2086
|
+
|
|
2087
|
+
# Set by models_api on every call.
|
|
2088
|
+
models_http_status=""
|
|
2089
|
+
models_http_body=""
|
|
2090
|
+
|
|
2091
|
+
# models_api METHOD PATH [JSON_BODY] — call the router and capture status+body.
|
|
2092
|
+
# The key rides in on stdin (curl --header @-) rather than a -H argument so it
|
|
2093
|
+
# never appears in the process arg list, which any other local user can read.
|
|
2094
|
+
models_api() {
|
|
2095
|
+
local method="$1" path="$2" body="${3:-}"
|
|
2096
|
+
local out status=""
|
|
2097
|
+
out="$(mktemp)" || { err "Could not create a temp file."; exit 1; }
|
|
2098
|
+
if [ -n "$body" ]; then
|
|
2099
|
+
status="$(printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
2100
|
+
| curl -sS --max-time 20 -X "$method" \
|
|
2101
|
+
-H 'Content-Type: application/json' --data-binary "$body" \
|
|
2102
|
+
--header @- -o "$out" -w '%{http_code}' "$base_url$path" 2>/dev/null)" || status=""
|
|
2103
|
+
else
|
|
2104
|
+
status="$(printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
2105
|
+
| curl -sS --max-time 20 -X "$method" \
|
|
2106
|
+
--header @- -o "$out" -w '%{http_code}' "$base_url$path" 2>/dev/null)" || status=""
|
|
2107
|
+
fi
|
|
2108
|
+
models_http_status="$status"
|
|
2109
|
+
models_http_body="$(cat "$out" 2>/dev/null || true)"
|
|
2110
|
+
rm -f "$out"
|
|
2111
|
+
case "$models_http_status" in
|
|
2112
|
+
2*) return 0 ;;
|
|
2113
|
+
*) return 1 ;;
|
|
2114
|
+
esac
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
# models_api_error prints the router's own `error` field, when it sent one.
|
|
2118
|
+
models_api_error() {
|
|
2119
|
+
printf '%s' "$models_http_body" | jq -r '.error // empty' 2>/dev/null || true
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
# models_fail turns the status left by models_api into a message that says what
|
|
2123
|
+
# to do next, then exits. $1 names the attempted operation.
|
|
2124
|
+
models_fail() {
|
|
2125
|
+
local what="$1" detail
|
|
2126
|
+
detail="$(models_api_error)"
|
|
2127
|
+
case "$models_http_status" in
|
|
2128
|
+
""|000)
|
|
2129
|
+
err "Could not reach the router at $base_url. Is it running?"
|
|
2130
|
+
;;
|
|
2131
|
+
401|403)
|
|
2132
|
+
# 403 with a message is the ROUTER_EXCLUDED_MODELS/PROVIDERS env pin,
|
|
2133
|
+
# which is actionable on its own; a bare 401/403 is a key problem.
|
|
2134
|
+
if [ -n "$detail" ]; then
|
|
2135
|
+
err "$detail"
|
|
2136
|
+
else
|
|
2137
|
+
err "The router rejected this installation's key. Re-run 'npx @workweave/router --claude --rotate-key' to install a current one."
|
|
2138
|
+
fi
|
|
2139
|
+
;;
|
|
2140
|
+
404)
|
|
2141
|
+
err "This router does not expose the model-selection API, so $what is not available here."
|
|
2142
|
+
printf " %sWeave-hosted routers keep model selection with the organization — edit it at %s%s\n" \
|
|
2143
|
+
"$C_DIM" "$MODELS_DASHBOARD_URL" "$C_RESET" >&2
|
|
2144
|
+
printf " %sSelf-hosted? Update the router to a build that serves /admin/v1/models.%s\n" \
|
|
2145
|
+
"$C_DIM" "$C_RESET" >&2
|
|
2146
|
+
;;
|
|
2147
|
+
*)
|
|
2148
|
+
if [ -n "$detail" ]; then
|
|
2149
|
+
err "$what failed (HTTP $models_http_status): $detail"
|
|
2150
|
+
else
|
|
2151
|
+
err "$what failed (HTTP $models_http_status)."
|
|
2152
|
+
fi
|
|
2153
|
+
;;
|
|
2154
|
+
esac
|
|
2155
|
+
exit 1
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
# models_render_list prints the [{model,provider,enabled}] payload as a
|
|
2159
|
+
# provider-grouped checklist. The API sorts by provider then model, which is
|
|
2160
|
+
# what group_by needs, and what keeps two runs comparable.
|
|
2161
|
+
models_render_list() {
|
|
2162
|
+
local payload="$1" total enabled
|
|
2163
|
+
total="$(printf '%s' "$payload" | jq 'length')"
|
|
2164
|
+
enabled="$(printf '%s' "$payload" | jq '[.[] | select(.enabled)] | length')"
|
|
2165
|
+
printf "%s%sWeave Router models%s %s· %s%s\n" "$C_BOLD" "$C_BRAND" "$C_RESET" "$C_DIM" "$base_url" "$C_RESET"
|
|
2166
|
+
printf "%s%s of %s enabled%s\n\n" "$C_DIM" "$enabled" "$total" "$C_RESET"
|
|
2167
|
+
printf '%s' "$payload" | jq -r --arg on "$C_GREEN" --arg off "$C_DIM" --arg reset "$C_RESET" --arg bold "$C_BOLD" '
|
|
2168
|
+
group_by(.provider)[]
|
|
2169
|
+
| ($bold + .[0].provider + $reset),
|
|
2170
|
+
(.[] | if .enabled
|
|
2171
|
+
then " " + $on + "[x]" + $reset + " " + .model
|
|
2172
|
+
else " " + $off + "[ ] " + .model + $reset
|
|
2173
|
+
end)
|
|
2174
|
+
'
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
# models_render_providers prints the [{provider,enabled}] payload.
|
|
2178
|
+
models_render_providers() {
|
|
2179
|
+
local payload="$1"
|
|
2180
|
+
printf "%s%sWeave Router providers%s %s· %s%s\n\n" "$C_BOLD" "$C_BRAND" "$C_RESET" "$C_DIM" "$base_url" "$C_RESET"
|
|
2181
|
+
printf '%s' "$payload" | jq -r --arg on "$C_GREEN" --arg off "$C_DIM" --arg reset "$C_RESET" '
|
|
2182
|
+
.[] | if .enabled
|
|
2183
|
+
then " " + $on + "[x]" + $reset + " " + .provider
|
|
2184
|
+
else " " + $off + "[ ] " + .provider + $reset
|
|
2185
|
+
end
|
|
2186
|
+
'
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
# models_print_preferred appends the priority ranking when one is set. Absent
|
|
2190
|
+
# is the norm (the router picks per turn), so silence is the right output.
|
|
2191
|
+
models_print_preferred() {
|
|
2192
|
+
models_api GET "/admin/v1/preferred-models" || return 0
|
|
2193
|
+
local list
|
|
2194
|
+
list="$(printf '%s' "$models_http_body" | jq -r '(.preferred // []) | join(" > ")' 2>/dev/null || true)"
|
|
2195
|
+
[ -n "$list" ] || return 0
|
|
2196
|
+
printf "\n%sPreferred order:%s %s\n" "$C_DIM" "$C_RESET" "$list"
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
# models_list_catalog is the read-only fallback for a router with no
|
|
2200
|
+
# model-selection API: list what it can route to and say where to change it.
|
|
2201
|
+
# Deliberately not rendered as a checklist — this endpoint reports the deployed
|
|
2202
|
+
# catalog, not the installation's selection, so marking every row [x] would
|
|
2203
|
+
# claim models are enabled that the dashboard may well have excluded.
|
|
2204
|
+
#
|
|
2205
|
+
# $1 selects what to print from the same catalog payload: "models" (the
|
|
2206
|
+
# default) or "providers" — `models providers` hits this same 404 and must
|
|
2207
|
+
# degrade the same way `models list` does rather than hard-failing, since both
|
|
2208
|
+
# are read-only listing commands.
|
|
2209
|
+
models_list_catalog() {
|
|
2210
|
+
local what="${1:-models}"
|
|
2211
|
+
models_api GET "$MODELS_CATALOG_PATH" || models_fail "listing $what"
|
|
2212
|
+
if [ "$models_json" = "true" ]; then
|
|
2213
|
+
if [ "$what" = "providers" ]; then
|
|
2214
|
+
printf '%s' "$models_http_body" | jq -c '[.models[].provider] | unique'
|
|
2215
|
+
else
|
|
2216
|
+
printf '%s\n' "$models_http_body"
|
|
2217
|
+
fi
|
|
2218
|
+
return 0
|
|
2219
|
+
fi
|
|
2220
|
+
printf "%s%sWeave Router %s%s %s· %s%s\n" "$C_BOLD" "$C_BRAND" "$what" "$C_RESET" "$C_DIM" "$base_url" "$C_RESET"
|
|
2221
|
+
printf "%severything this router can route to%s\n\n" "$C_DIM" "$C_RESET"
|
|
2222
|
+
if [ "$what" = "providers" ]; then
|
|
2223
|
+
printf '%s' "$models_http_body" | jq -r '[.models[].provider] | unique[] | " " + .'
|
|
2224
|
+
else
|
|
2225
|
+
printf '%s' "$models_http_body" | jq -r --arg reset "$C_RESET" --arg bold "$C_BOLD" '
|
|
2226
|
+
.models | group_by(.provider)[]
|
|
2227
|
+
| ($bold + .[0].provider + $reset),
|
|
2228
|
+
(.[] | " " + .model)
|
|
2229
|
+
'
|
|
2230
|
+
fi
|
|
2231
|
+
printf "\n%sThis router does not report which of them your installation has enabled:%s\n" "$C_DIM" "$C_RESET"
|
|
2232
|
+
printf "%sselection is an organization-wide setting here. See it, and change it, at%s\n" "$C_DIM" "$C_RESET"
|
|
2233
|
+
printf "%s%s%s\n" "$C_DIM" "$MODELS_DASHBOARD_URL" "$C_RESET"
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
models_list() {
|
|
2237
|
+
if ! models_api GET "/admin/v1/models"; then
|
|
2238
|
+
# Only a missing API is worth degrading for; anything else is a real error.
|
|
2239
|
+
[ "$models_http_status" = "404" ] || models_fail "listing models"
|
|
2240
|
+
models_list_catalog models
|
|
2241
|
+
return 0
|
|
2242
|
+
fi
|
|
2243
|
+
if [ "$models_json" = "true" ]; then
|
|
2244
|
+
printf '%s\n' "$models_http_body"
|
|
2245
|
+
return 0
|
|
2246
|
+
fi
|
|
2247
|
+
models_render_list "$models_http_body"
|
|
2248
|
+
models_print_preferred
|
|
2249
|
+
printf "\n%sEnable a model:%s npx @workweave/router models enable <id> --claude\n" "$C_DIM" "$C_RESET"
|
|
2250
|
+
printf "%sDisable a model:%s npx @workweave/router models disable <id> --claude\n" "$C_DIM" "$C_RESET"
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
models_providers_list() {
|
|
2254
|
+
if ! models_api GET "/admin/v1/providers"; then
|
|
2255
|
+
# Same 404 degrade as models_list: a router with no model-selection API
|
|
2256
|
+
# still answers the unauthed catalog, and this is a read-only listing
|
|
2257
|
+
# command same as `models list` — no reason to hard-fail one and not
|
|
2258
|
+
# the other.
|
|
2259
|
+
[ "$models_http_status" = "404" ] || models_fail "listing providers"
|
|
2260
|
+
models_list_catalog providers
|
|
2261
|
+
return 0
|
|
2262
|
+
fi
|
|
2263
|
+
if [ "$models_json" = "true" ]; then
|
|
2264
|
+
printf '%s\n' "$models_http_body"
|
|
2265
|
+
return 0
|
|
2266
|
+
fi
|
|
2267
|
+
models_render_providers "$models_http_body"
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
# models_toggle KIND ACTION IDS — flip one or more models/providers on or off.
|
|
2271
|
+
# Each id is its own request so a typo in the third id can't roll back the two
|
|
2272
|
+
# that already applied, and so the report names exactly what changed.
|
|
2273
|
+
models_toggle() {
|
|
2274
|
+
local kind="$1" action="$2" ids="$3"
|
|
2275
|
+
local path body id label doing
|
|
2276
|
+
case "$kind" in
|
|
2277
|
+
model) path="/admin/v1/excluded-models" ; label="model" ;;
|
|
2278
|
+
provider) path="/admin/v1/excluded-providers" ; label="provider" ;;
|
|
2279
|
+
esac
|
|
2280
|
+
# Enabling means dropping the id from the exclusion list, disabling means
|
|
2281
|
+
# adding it — the API's remove/add endpoints, inverted here so the CLI reads
|
|
2282
|
+
# the way the dashboard's checkboxes do.
|
|
2283
|
+
doing="disabling"
|
|
2284
|
+
if [ "$action" = "enable" ]; then
|
|
2285
|
+
doing="enabling"
|
|
2286
|
+
path="$path/remove"
|
|
2287
|
+
fi
|
|
2288
|
+
|
|
2289
|
+
# Fed by a here-string, not a pipe: a pipe would run the loop in a subshell
|
|
2290
|
+
# where models_fail's exit only kills that subshell.
|
|
2291
|
+
while IFS= read -r id; do
|
|
2292
|
+
[ -n "$id" ] || continue
|
|
2293
|
+
body="$(jq -nc --arg v "$id" --arg k "$label" '{($k): $v}')"
|
|
2294
|
+
if ! models_api POST "$path" "$body"; then
|
|
2295
|
+
models_fail "$doing $label '$id'"
|
|
2296
|
+
fi
|
|
2297
|
+
# Under --json the router's own response body is the output: a scripted
|
|
2298
|
+
# caller parses stdout, and printing prose there makes it report a parse
|
|
2299
|
+
# failure for a mutation that already succeeded.
|
|
2300
|
+
if [ "$models_json" = "true" ]; then
|
|
2301
|
+
printf '%s\n' "$models_http_body"
|
|
2302
|
+
elif [ "$action" = "enable" ]; then
|
|
2303
|
+
printf "%s✓%s %s %s%s%s is enabled\n" "$C_GREEN" "$C_RESET" "$label" "$C_BOLD" "$id" "$C_RESET"
|
|
2304
|
+
else
|
|
2305
|
+
printf "%s✓%s %s %s%s%s is disabled — the router will not pick it\n" "$C_GREEN" "$C_RESET" "$label" "$C_BOLD" "$id" "$C_RESET"
|
|
2306
|
+
fi
|
|
2307
|
+
done <<<"$ids"
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
# models_prefer replaces the priority ranking outright (the API's PUT), because
|
|
2311
|
+
# a ranking is an ordering: appending one id at a time can't express "this one
|
|
2312
|
+
# first". `clear` drops it and returns the installation to plain routing.
|
|
2313
|
+
models_prefer() {
|
|
2314
|
+
local ids="$1" body stored
|
|
2315
|
+
if [ "$ids" = "clear" ] || [ "$ids" = "none" ]; then
|
|
2316
|
+
body='{"preferred":[]}'
|
|
2317
|
+
else
|
|
2318
|
+
body="$(printf '%s' "$ids" | jq -c -R -s '{preferred: (split("\n") | map(select(length > 0)))}')"
|
|
2319
|
+
fi
|
|
2320
|
+
models_api PUT "/admin/v1/preferred-models" "$body" || models_fail "setting the preferred-model ranking"
|
|
2321
|
+
if [ "$models_json" = "true" ]; then
|
|
2322
|
+
printf '%s\n' "$models_http_body"
|
|
2323
|
+
return 0
|
|
2324
|
+
fi
|
|
2325
|
+
stored="$(printf '%s' "$models_http_body" | jq -r '(.preferred // []) | join(" > ")')"
|
|
2326
|
+
if [ -n "$stored" ]; then
|
|
2327
|
+
printf "%s✓%s Preferred order: %s\n" "$C_GREEN" "$C_RESET" "$stored"
|
|
2328
|
+
else
|
|
2329
|
+
printf "%s✓%s Preferred-model ranking cleared.\n" "$C_GREEN" "$C_RESET"
|
|
2330
|
+
fi
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
models_usage() {
|
|
2334
|
+
err "$1"
|
|
2335
|
+
printf '%s\n' \
|
|
2336
|
+
" npx @workweave/router models --claude # list models" \
|
|
2337
|
+
" npx @workweave/router models enable <id> [<id>…] --claude" \
|
|
2338
|
+
" npx @workweave/router models disable <id> [<id>…] --claude" \
|
|
2339
|
+
" npx @workweave/router models providers --claude # list providers" \
|
|
2340
|
+
" npx @workweave/router models providers disable <name> --claude" \
|
|
2341
|
+
" npx @workweave/router models prefer <id> [<id>…] --claude # ranking ('clear' to drop)" >&2
|
|
2342
|
+
exit 2
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
run_models() {
|
|
2346
|
+
local verb operands
|
|
2347
|
+
verb="$(printf '%s' "$models_args" | head -n 1)"
|
|
2348
|
+
operands="$(printf '%s' "$models_args" | tail -n +2)"
|
|
2349
|
+
|
|
2350
|
+
# `models providers …` shifts one more word: the sub-verb is the second word.
|
|
2351
|
+
case "$verb" in
|
|
2352
|
+
""|list)
|
|
2353
|
+
[ -z "$operands" ] || models_usage "'models $verb' takes no arguments."
|
|
2354
|
+
models_list
|
|
2355
|
+
;;
|
|
2356
|
+
enable|disable)
|
|
2357
|
+
[ -n "$operands" ] || models_usage "'models $verb' needs at least one model id."
|
|
2358
|
+
models_toggle model "$verb" "$operands"
|
|
2359
|
+
;;
|
|
2360
|
+
prefer)
|
|
2361
|
+
[ -n "$operands" ] || models_usage "'models prefer' needs model ids, or 'clear'."
|
|
2362
|
+
models_prefer "$operands"
|
|
2363
|
+
;;
|
|
2364
|
+
providers)
|
|
2365
|
+
local sub rest
|
|
2366
|
+
sub="$(printf '%s' "$operands" | head -n 1)"
|
|
2367
|
+
rest="$(printf '%s' "$operands" | tail -n +2)"
|
|
2368
|
+
case "$sub" in
|
|
2369
|
+
""|list)
|
|
2370
|
+
models_providers_list
|
|
2371
|
+
;;
|
|
2372
|
+
enable|disable)
|
|
2373
|
+
[ -n "$rest" ] || models_usage "'models providers $sub' needs at least one provider name."
|
|
2374
|
+
models_toggle provider "$sub" "$rest"
|
|
2375
|
+
;;
|
|
2376
|
+
*)
|
|
2377
|
+
models_usage "Unknown providers sub-command: '$sub'."
|
|
2378
|
+
;;
|
|
2379
|
+
esac
|
|
2380
|
+
;;
|
|
2381
|
+
*)
|
|
2382
|
+
models_usage "Unknown models sub-command: '$verb'."
|
|
2383
|
+
;;
|
|
2384
|
+
esac
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if [ "$mode" = "models" ]; then
|
|
2388
|
+
# Which file supplied the endpoint / the key. Empty means "not from a settings
|
|
2389
|
+
# file" — an explicit --base-url, or WEAVE_ROUTER_KEY. Initialized before the
|
|
2390
|
+
# branches below so `set -u` holds on every path.
|
|
2391
|
+
models_base_source=""
|
|
2392
|
+
models_key_source=""
|
|
2393
|
+
# Editing model selection needs the endpoint and key of one specific install,
|
|
2394
|
+
# never the hosted defaults: a self-hosted user pointing at their own router
|
|
2395
|
+
# would otherwise silently edit the hosted one's installation.
|
|
2396
|
+
if [ "$base_url_explicit" != "true" ]; then
|
|
2397
|
+
models_base="$(resolve_installed_base_url)"
|
|
2398
|
+
if [ -z "$models_base" ]; then
|
|
2399
|
+
err "No Weave Router install found for Claude Code in this scope. Run 'npx @workweave/router --claude' first, or pass --base-url."
|
|
2400
|
+
exit 1
|
|
2401
|
+
fi
|
|
2402
|
+
base_url="$models_base"
|
|
2403
|
+
# resolve_installed_base_url ran in a command substitution, so any global it
|
|
2404
|
+
# set died with that subshell. Recover the source by walking the same
|
|
2405
|
+
# precedence to find which file holds the endpoint we just adopted.
|
|
2406
|
+
while IFS= read -r models_src_candidate; do
|
|
2407
|
+
[ -n "$models_src_candidate" ] || continue
|
|
2408
|
+
models_src_url="$(json_get "$models_src_candidate" '.env.ANTHROPIC_BASE_URL')"
|
|
2409
|
+
if [ "${models_src_url%/}" = "$models_base" ]; then
|
|
2410
|
+
models_base_source="$models_src_candidate"
|
|
2411
|
+
break
|
|
2412
|
+
fi
|
|
2413
|
+
done <<<"$(models_base_file_order)"
|
|
2414
|
+
fi
|
|
2415
|
+
# WEAVE_ROUTER_KEY is the user's own choice, so it pairs with any endpoint.
|
|
2416
|
+
if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
2417
|
+
api_key="$WEAVE_ROUTER_KEY"
|
|
2418
|
+
models_key_source="env:WEAVE_ROUTER_KEY"
|
|
2419
|
+
else
|
|
2420
|
+
api_key="$(read_installed_key)"
|
|
2421
|
+
# Same subshell caveat as the endpoint above: recover which file the key
|
|
2422
|
+
# came from by re-reading them in read_installed_key's own precedence.
|
|
2423
|
+
models_key_source=""
|
|
2424
|
+
while IFS= read -r models_src_candidate; do
|
|
2425
|
+
[ -n "$models_src_candidate" ] || continue
|
|
2426
|
+
if [ -n "$(read_claude_key "$models_src_candidate")" ]; then
|
|
2427
|
+
models_key_source="$models_src_candidate"
|
|
2428
|
+
break
|
|
2429
|
+
fi
|
|
2430
|
+
done <<<"$(models_key_file_order)"
|
|
2431
|
+
fi
|
|
2432
|
+
if [ -z "$api_key" ]; then
|
|
2433
|
+
err "No router key found for Claude Code in this scope. Re-run 'npx @workweave/router --claude', or export WEAVE_ROUTER_KEY."
|
|
2434
|
+
exit 1
|
|
2435
|
+
fi
|
|
2436
|
+
# Never send a key to an endpoint the checkout supplied. See
|
|
2437
|
+
# models_endpoint_is_trusted: a hostile repo can commit a settings.json naming
|
|
2438
|
+
# its own router, and pairing that with the teammate key from the gitignored
|
|
2439
|
+
# settings.local.json would hand the key to whoever wrote the repo.
|
|
2440
|
+
if [ "$models_key_source" != "env:WEAVE_ROUTER_KEY" ] \
|
|
2441
|
+
&& ! models_endpoint_is_trusted "$base_url" "$models_base_source" "$models_key_source"; then
|
|
2442
|
+
err "Refusing to send this installation's router key to $base_url."
|
|
2443
|
+
printf " %sThat endpoint comes from %s, which git tracks — a checked-out repo can set it —%s\n" \
|
|
2444
|
+
"$C_DIM" "${models_base_source##*/}" "$C_RESET" >&2
|
|
2445
|
+
printf " %swhile the key comes from %s. Pass --base-url <url> to confirm the endpoint,%s\n" \
|
|
2446
|
+
"$C_DIM" "${models_key_source##*/}" "$C_RESET" >&2
|
|
2447
|
+
printf " %sor re-run 'npx @workweave/router --claude' to install against the one you want.%s\n" \
|
|
2448
|
+
"$C_DIM" "$C_RESET" >&2
|
|
2449
|
+
exit 1
|
|
2450
|
+
fi
|
|
2451
|
+
run_models
|
|
2452
|
+
exit 0
|
|
2453
|
+
fi
|
|
2454
|
+
|
|
1701
2455
|
if [ "$mode" != "install" ] && [ "$mode" != "update" ]; then
|
|
1702
2456
|
case "$target" in
|
|
1703
2457
|
claude) toggle_claude ;;
|
|
@@ -1719,16 +2473,7 @@ fi
|
|
|
1719
2473
|
# sidecar is the authority while toggled off — reading the live file there would
|
|
1720
2474
|
# pin the install to api.anthropic.com.
|
|
1721
2475
|
if [ "$mode" = "update" ] && [ "$base_url_explicit" != "true" ] && [ "$target" = "claude" ]; then
|
|
1722
|
-
installed_base=""
|
|
1723
|
-
for candidate in \
|
|
1724
|
-
"$settings_dir/.weave-parked.json" \
|
|
1725
|
-
"$settings_file" \
|
|
1726
|
-
"$local_settings_file"
|
|
1727
|
-
do
|
|
1728
|
-
installed_base="$(json_get "$candidate" '.env.ANTHROPIC_BASE_URL')"
|
|
1729
|
-
router_shaped_url "$installed_base" && break
|
|
1730
|
-
installed_base=""
|
|
1731
|
-
done
|
|
2476
|
+
installed_base="$(resolve_installed_base_url)"
|
|
1732
2477
|
if [ -n "$installed_base" ]; then
|
|
1733
2478
|
base_url="${installed_base%/}"
|
|
1734
2479
|
fi
|
|
@@ -1747,30 +2492,7 @@ fi
|
|
|
1747
2492
|
# a re-run painless — the installer refreshes assets and config shape often
|
|
1748
2493
|
# enough that users re-run it routinely, and re-pasting a key every time is pure
|
|
1749
2494
|
# friction. --rotate-key skips the read-back so a new key can replace the old.
|
|
1750
|
-
|
|
1751
|
-
# read_installed_key prints the router key this install already has on disk, or
|
|
1752
|
-
# nothing. Only Claude Code is supported today; the other targets still prompt.
|
|
1753
|
-
read_installed_key() {
|
|
1754
|
-
[ "$target" = "claude" ] || return 0
|
|
1755
|
-
local key=""
|
|
1756
|
-
# Mirror where the install path writes the key: project scope (no --dir) puts
|
|
1757
|
-
# it in the gitignored settings.local.json, everything else inlines it into
|
|
1758
|
-
# settings.json. Check the other file too — a scope was possibly changed, or a
|
|
1759
|
-
# project checkout may carry a committed header from an older install.
|
|
1760
|
-
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1761
|
-
key="$(read_claude_key "$local_settings_file")"
|
|
1762
|
-
[ -n "$key" ] || key="$(read_claude_key "$settings_file")"
|
|
1763
|
-
else
|
|
1764
|
-
key="$(read_claude_key "$settings_file")"
|
|
1765
|
-
[ -n "$key" ] || key="$(read_claude_key "$local_settings_file")"
|
|
1766
|
-
fi
|
|
1767
|
-
# Last resort: `off` moves the key header out of the settings files and into
|
|
1768
|
-
# the parked sidecar, so an install/update run while toggled off finds nothing
|
|
1769
|
-
# above even though the key is still on disk. Same {"env":{…}} shape, so
|
|
1770
|
-
# read_claude_key reads it directly.
|
|
1771
|
-
[ -n "$key" ] || key="$(read_claude_key "$settings_dir/.weave-parked.json")"
|
|
1772
|
-
printf '%s' "$key"
|
|
1773
|
-
}
|
|
2495
|
+
# read_installed_key itself is defined above, next to the other settings readers.
|
|
1774
2496
|
|
|
1775
2497
|
# prompt_for_key reads a key from the controlling terminal into $api_key. Only
|
|
1776
2498
|
# ever called on an interactive install; callers check first.
|
|
@@ -1862,11 +2584,12 @@ fi
|
|
|
1862
2584
|
# Claude: <settings_dir>/commands/{force-model,unforce-model}.md → /force-model
|
|
1863
2585
|
# opencode: <commands_dir>/{force-model,unforce-model}.md → /force-model
|
|
1864
2586
|
#
|
|
1865
|
-
# Codex intentionally
|
|
1866
|
-
# commands and does not load these Markdown wrappers.
|
|
1867
|
-
#
|
|
1868
|
-
# ` /force-model gpt-5.6-terra`)
|
|
1869
|
-
# router
|
|
2587
|
+
# Codex is intentionally not listed here: its slash menu only exposes built-in
|
|
2588
|
+
# commands and does not load these Markdown wrappers. Codex instead gets native
|
|
2589
|
+
# `$name` skills (see install_codex_prompt_skills), each of which sends the
|
|
2590
|
+
# leading-space prompt form (for example, ` /force-model gpt-5.6-terra`) that
|
|
2591
|
+
# the router parses. Which client gets which directive comes from the embedded
|
|
2592
|
+
# registry, not from a list here.
|
|
1870
2593
|
#
|
|
1871
2594
|
# Files come from install/commands/ in the repo (or the colocated commands/
|
|
1872
2595
|
# directory the npm package ships alongside install.sh).
|
|
@@ -1893,13 +2616,22 @@ install_slash_commands() {
|
|
|
1893
2616
|
# command-capable clients. The router-off/on/status wrappers shell out to
|
|
1894
2617
|
# this installer to flip the *local* config, so they're Claude Code-only and
|
|
1895
2618
|
# need the install scope baked into the command (the .md can't discover it
|
|
1896
|
-
# at invocation time). {{SCOPE}} is substituted accordingly.
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2619
|
+
# at invocation time). {{SCOPE}} is substituted accordingly. router-models
|
|
2620
|
+
# (alias models) is in the same boat: it shells out to read this install's
|
|
2621
|
+
# endpoint and key.
|
|
2622
|
+
cmds=""
|
|
2623
|
+
while IFS= read -r cmd; do
|
|
2624
|
+
[ -f "$commands_src_dir/$cmd.md" ] || continue
|
|
2625
|
+
case ",$cmd," in
|
|
2626
|
+
*,router-off,*|*,router-on,*|*,router-status,*|*,router-session,*|*,router-models,*|*,models,*)
|
|
2627
|
+
[ "$target" = "claude" ] || continue ;;
|
|
2628
|
+
esac
|
|
2629
|
+
[ -n "$cmds" ] && cmds="$cmds "
|
|
2630
|
+
cmds="$cmds$cmd"
|
|
2631
|
+
done <<EOF
|
|
2632
|
+
$(weave_registry_names "$target")
|
|
2633
|
+
EOF
|
|
2634
|
+
installed="$(printf '%s' "$cmds" | tr ' ' ',' | sed 's/,/, /g')"
|
|
1903
2635
|
|
|
1904
2636
|
# Bake the same scope selector the toggle needs to find this install: --dir
|
|
1905
2637
|
# overrides scope (mirrors install/uninstall path resolution), so a sandbox
|
|
@@ -1923,7 +2655,21 @@ install_slash_commands() {
|
|
|
1923
2655
|
# cp-equivalent for the others since the token is absent).
|
|
1924
2656
|
local body; body="$(cat "$src")"
|
|
1925
2657
|
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
1926
|
-
|
|
2658
|
+
# Wrappers installed before ownership markers existed carry no marker. A
|
|
2659
|
+
# file whose body still matches what this installer would write is one of
|
|
2660
|
+
# ours from an older version, so adopt it rather than treating it as
|
|
2661
|
+
# user-owned — otherwise every pre-marker install is stranded: never
|
|
2662
|
+
# refreshed, never uninstalled, and skipped by the statusline refresh.
|
|
2663
|
+
# Anything that does not match byte-for-byte is left alone.
|
|
2664
|
+
if [ -e "$dst" ] && { [ ! -f "$dst" ] || ! grep -Fq "<!-- weave-router managed command: $cmd -->" "$dst"; }; then
|
|
2665
|
+
if [ -f "$dst" ] && [ "$(cat "$dst")" = "$body" ]; then
|
|
2666
|
+
: # an unmarked copy of our own wrapper; fall through and adopt it
|
|
2667
|
+
else
|
|
2668
|
+
warn "A user-owned $target command already exists at $dst; leaving it untouched."
|
|
2669
|
+
continue
|
|
2670
|
+
fi
|
|
2671
|
+
fi
|
|
2672
|
+
printf '%s\n<!-- weave-router managed command: %s -->' "$body" "$cmd" >"$dst"
|
|
1927
2673
|
done
|
|
1928
2674
|
seed_command_baseline "$commands_src_dir" "$cmds"
|
|
1929
2675
|
ok "Slash commands written to $dst_dir ($installed)"
|
|
@@ -1984,18 +2730,18 @@ remove_obsolete_codex_prompt_wrappers() {
|
|
|
1984
2730
|
refuse_if_symlink "$dst_dir"
|
|
1985
2731
|
fi
|
|
1986
2732
|
|
|
1987
|
-
|
|
2733
|
+
while IFS= read -r cmd; do
|
|
1988
2734
|
src="$commands_src_dir/$cmd.md"
|
|
1989
2735
|
dst="$dst_dir/$cmd.md"
|
|
1990
2736
|
[ -f "$src" ] && [ -f "$dst" ] || continue
|
|
1991
|
-
# A symlink is user-controlled at user scope. Leave it untouched; project
|
|
1992
|
-
# scope rejects it through the same guard used by normal installs.
|
|
1993
2737
|
[ -L "$dst" ] && continue
|
|
1994
2738
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1995
2739
|
refuse_if_symlink "$dst"
|
|
1996
2740
|
fi
|
|
1997
2741
|
cmp -s "$src" "$dst" && rm -f "$dst"
|
|
1998
|
-
done
|
|
2742
|
+
done <<EOF
|
|
2743
|
+
$(weave_registry_names codex)
|
|
2744
|
+
EOF
|
|
1999
2745
|
rmdir "$dst_dir" 2>/dev/null || true
|
|
2000
2746
|
}
|
|
2001
2747
|
|
|
@@ -2048,13 +2794,49 @@ install_codex_disable_routing_skill() {
|
|
|
2048
2794
|
ok "Codex skill installed: \$disable-routing"
|
|
2049
2795
|
}
|
|
2050
2796
|
|
|
2051
|
-
#
|
|
2797
|
+
# Install prompt directives as Codex-native skills. The skill itself emits a
|
|
2798
|
+
# leading-space normal prompt, which is the only portable way to reach the
|
|
2799
|
+
# router directive parser without claiming Codex's reserved slash namespace.
|
|
2800
|
+
install_codex_prompt_skills() {
|
|
2801
|
+
local canonical skill_src dst_dir dst_file scope_args body
|
|
2802
|
+
while IFS= read -r canonical; do
|
|
2803
|
+
skill_src="$script_dir/codex-skills/$canonical/SKILL.md"
|
|
2804
|
+
[ -f "$skill_src" ] || continue
|
|
2805
|
+
grep -Fq "<!-- weave-router managed $canonical skill -->" "$skill_src" || continue
|
|
2806
|
+
dst_dir="$codex_dir/skills/$canonical"
|
|
2807
|
+
dst_file="$dst_dir/SKILL.md"
|
|
2808
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
2809
|
+
refuse_if_symlink "$codex_dir/skills"
|
|
2810
|
+
refuse_if_symlink "$dst_dir"
|
|
2811
|
+
refuse_if_symlink "$dst_file"
|
|
2812
|
+
elif [ -L "$codex_dir/skills" ] || [ -L "$dst_dir" ] || [ -L "$dst_file" ]; then
|
|
2813
|
+
warn "Codex skill path contains a symlink; leaving $canonical untouched."
|
|
2814
|
+
continue
|
|
2815
|
+
fi
|
|
2816
|
+
if [ -e "$dst_file" ] && { [ ! -f "$dst_file" ] || ! grep -Fq "<!-- weave-router managed $canonical skill -->" "$dst_file"; }; then
|
|
2817
|
+
warn "A user-owned Codex $canonical skill already exists at $dst_file; leaving it untouched."
|
|
2818
|
+
continue
|
|
2819
|
+
fi
|
|
2820
|
+
mkdir -p "$dst_dir"
|
|
2821
|
+
scope_args=""
|
|
2822
|
+
if [ -n "$install_dir" ]; then scope_args=" --dir $(printf '%q' "$install_dir")"
|
|
2823
|
+
elif [ "$scope" = project ]; then scope_args=" --scope project"; fi
|
|
2824
|
+
body="$(<"$skill_src")"
|
|
2825
|
+
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
2826
|
+
printf '%s\n' "$body" >"$dst_file"
|
|
2827
|
+
ok "Codex skill installed: \$$canonical"
|
|
2828
|
+
done <<EOF
|
|
2829
|
+
$(weave_registry_skill_names codex)
|
|
2830
|
+
EOF
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2052
2833
|
|
|
2053
2834
|
if [ "$target" = "codex" ]; then
|
|
2054
2835
|
write_codex_config "$codex_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
2055
2836
|
ok "Codex config written to $codex_config_file"
|
|
2056
2837
|
remove_obsolete_codex_prompt_wrappers "$codex_dir/prompts"
|
|
2057
2838
|
install_codex_disable_routing_skill
|
|
2839
|
+
install_codex_prompt_skills
|
|
2058
2840
|
info "Codex router directives: begin the message with one space, e.g. ' /force-model gpt-5.6-terra'."
|
|
2059
2841
|
|
|
2060
2842
|
# Project scope: ensure the per-teammate config (which holds the router key)
|
|
@@ -2242,6 +3024,10 @@ if [ "$target" = "pi" ]; then
|
|
|
2242
3024
|
fi
|
|
2243
3025
|
fi
|
|
2244
3026
|
|
|
3027
|
+
if [ -n "$lsp_langs" ]; then
|
|
3028
|
+
install_lsp_servers "$lsp_langs"
|
|
3029
|
+
fi
|
|
3030
|
+
|
|
2245
3031
|
printf "\n"
|
|
2246
3032
|
printf "%s✓%s %s%sWeave Router installed for pi.%s\n" \
|
|
2247
3033
|
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
@@ -2420,6 +3206,23 @@ weave_command_tracked_by_git() {
|
|
|
2420
3206
|
git -C "$(dirname "$1")" ls-files --error-unmatch -- "$1" >/dev/null 2>&1
|
|
2421
3207
|
}
|
|
2422
3208
|
|
|
3209
|
+
# weave_installed_command_names lists the wrappers this install may refresh.
|
|
3210
|
+
# The statusline ships standalone (no registry.sh beside it), so the installed
|
|
3211
|
+
# set — itself written from the registry — is the only source of truth here.
|
|
3212
|
+
#
|
|
3213
|
+
# Files written before ownership markers existed carry none, so matching on the
|
|
3214
|
+
# marker alone would freeze every pre-marker install out of refreshes forever.
|
|
3215
|
+
# List every wrapper instead and let the baseline comparison below decide: a
|
|
3216
|
+
# file is replaced only when its bytes still match the last canonical copy, so
|
|
3217
|
+
# a user-authored command is never touched whether or not it carries a marker.
|
|
3218
|
+
weave_installed_command_names() {
|
|
3219
|
+
local dir="$1" file
|
|
3220
|
+
for file in "$dir"/*.md; do
|
|
3221
|
+
[ -f "$file" ] || continue
|
|
3222
|
+
printf '%s\n' "$(basename "$file" .md)"
|
|
3223
|
+
done
|
|
3224
|
+
}
|
|
3225
|
+
|
|
2423
3226
|
# weave_render_command prints $1 with the installer's {{SCOPE}} placeholder
|
|
2424
3227
|
# replaced by $2, matching how install_slash_commands writes the same file.
|
|
2425
3228
|
# Trailing newlines are stripped on both sides of every comparison below.
|
|
@@ -2489,8 +3292,7 @@ weave_sync_commands() {
|
|
|
2489
3292
|
# Detach stdin (CC pipes JSON to us) so curl can't consume it, and silence
|
|
2490
3293
|
# everything so no output leaks into the statusline.
|
|
2491
3294
|
exec </dev/null
|
|
2492
|
-
|
|
2493
|
-
router-off router-on router-status router-session; do
|
|
3295
|
+
while IFS= read -r name; do
|
|
2494
3296
|
installed="$cmd_dir/$name.md"
|
|
2495
3297
|
# Only ever refresh a wrapper that is already installed: a missing one
|
|
2496
3298
|
# was uninstalled or deliberately deleted, and resurrecting it would be
|
|
@@ -2519,10 +3321,10 @@ weave_sync_commands() {
|
|
|
2519
3321
|
if [ -f "$prev" ]; then
|
|
2520
3322
|
new_body="$(weave_render_command "$raw" "$scope_args")"
|
|
2521
3323
|
prev_body="$(weave_render_command "$prev" "$scope_args")"
|
|
2522
|
-
installed_body="$(cat "$installed" 2>/dev/null)" || installed_body=""
|
|
3324
|
+
installed_body="$(cat "$installed" 2>/dev/null | sed '/^<!-- weave-router managed command: .* -->$/d')" || installed_body=""
|
|
2523
3325
|
if [ "$prev_body" = "$installed_body" ] && [ "$new_body" != "$installed_body" ]; then
|
|
2524
3326
|
tmp="$installed.tmp.$$"
|
|
2525
|
-
if printf '%s\n' "$new_body" >"$tmp" 2>/dev/null; then
|
|
3327
|
+
if printf '%s\n<!-- weave-router managed command: %s -->' "$new_body" "$name" >"$tmp" 2>/dev/null; then
|
|
2526
3328
|
mv "$tmp" "$installed" 2>/dev/null || rm -f "$tmp"
|
|
2527
3329
|
else
|
|
2528
3330
|
rm -f "$tmp"
|
|
@@ -2530,13 +3332,152 @@ weave_sync_commands() {
|
|
|
2530
3332
|
fi
|
|
2531
3333
|
fi
|
|
2532
3334
|
mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
|
|
2533
|
-
done
|
|
3335
|
+
done <<EOF
|
|
3336
|
+
$(weave_installed_command_names "$cmd_dir")
|
|
3337
|
+
EOF
|
|
2534
3338
|
) >/dev/null 2>&1 &
|
|
2535
3339
|
disown 2>/dev/null || true
|
|
2536
3340
|
return 0
|
|
2537
3341
|
}
|
|
2538
3342
|
weave_sync_commands 2>/dev/null || true
|
|
2539
3343
|
|
|
3344
|
+
# ---------- org "hide terminal surfaces" gate ----------
|
|
3345
|
+
#
|
|
3346
|
+
# When the org has hidden the router's terminal surfaces, the statusline
|
|
3347
|
+
# renders nothing: this prints no output and exits 0, leaving the slot blank.
|
|
3348
|
+
# The setting comes from GET /v1/display-settings with the install's own
|
|
3349
|
+
# router key, but the foreground path NEVER touches the network: it decides
|
|
3350
|
+
# solely from a per-install cache file (TTL WEAVE_DISPLAY_SETTINGS_TTL_SECONDS,
|
|
3351
|
+
# default 1h), and a missing or stale cache fails open — the statusline
|
|
3352
|
+
# renders normally. When the cache is missing or stale a detached background
|
|
3353
|
+
# refresh re-fetches the setting and rewrites the cache atomically, so the
|
|
3354
|
+
# next turn picks up the fresh value; a refresh failure simply leaves the
|
|
3355
|
+
# cache stale, which keeps failing open rather than pinning the gate closed.
|
|
3356
|
+
#
|
|
3357
|
+
# Claude Code runs the statusline every turn, so several invocations can see a
|
|
3358
|
+
# stale cache at once. Letting each fetch independently is not safe: the
|
|
3359
|
+
# responses can land out of order, and the loser's mv would replace a newer
|
|
3360
|
+
# setting with an older one AND stamp it fresh, pinning the gate on a stale
|
|
3361
|
+
# value for a full TTL. Refreshes are therefore serialized per cache key on a
|
|
3362
|
+
# mkdir mutex (atomic everywhere; flock is absent on macOS), held across both
|
|
3363
|
+
# the fetch and the write. A refresh that finds the mutex held exits rather
|
|
3364
|
+
# than queueing — the in-flight one is at least as fresh as anything it would
|
|
3365
|
+
# fetch, so waiting only to overwrite it is the bug. The one path that is not
|
|
3366
|
+
# strictly exclusive is reclaiming a lock whose holder died; see below.
|
|
3367
|
+
weave_hidden_gate() {
|
|
3368
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
3369
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
3370
|
+
|
|
3371
|
+
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
|
|
3372
|
+
mkdir -p "$cache_dir" 2>/dev/null || return 1
|
|
3373
|
+
local self="${BASH_SOURCE[0]:-$0}"
|
|
3374
|
+
local script_slug
|
|
3375
|
+
script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
|
|
3376
|
+
local cache="$cache_dir/display-settings${script_slug}"
|
|
3377
|
+
local ttl="${WEAVE_DISPLAY_SETTINGS_TTL_SECONDS:-3600}"
|
|
3378
|
+
|
|
3379
|
+
local now mtime fresh="false"
|
|
3380
|
+
now="$(date +%s 2>/dev/null)" || now=0
|
|
3381
|
+
if [ -f "$cache" ]; then
|
|
3382
|
+
mtime="$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null)" || mtime=0
|
|
3383
|
+
if [ -n "${mtime:-}" ] && [ "$mtime" -gt 0 ] && [ $(( now - mtime )) -lt "$ttl" ]; then
|
|
3384
|
+
fresh="true"
|
|
3385
|
+
fi
|
|
3386
|
+
fi
|
|
3387
|
+
|
|
3388
|
+
# Foreground decision: only a fresh cache hides the surfaces. Anything else
|
|
3389
|
+
# (no cache, stale cache, unreadable cache) renders normally so a slow or
|
|
3390
|
+
# unreachable router can never stall a turn or wedge the statusline blank.
|
|
3391
|
+
if [ "$fresh" = "true" ]; then
|
|
3392
|
+
[ "$(cat "$cache" 2>/dev/null)" = "1" ]
|
|
3393
|
+
return
|
|
3394
|
+
fi
|
|
3395
|
+
|
|
3396
|
+
# Background refresh for the next invocation. Resolve the router base URL
|
|
3397
|
+
# and key inside the subshell from the Claude Code settings the installer
|
|
3398
|
+
# wrote: project/--dir installs put both under <base>/.claude alongside
|
|
3399
|
+
# this script (key in settings.local.json), while a user-scope install
|
|
3400
|
+
# lives under ~/.weave and reads ~/.claude. Resolve relative to the
|
|
3401
|
+
# script's own location, falling back to user scope, so a project install
|
|
3402
|
+
# never reads (or leaks) the user-scope key. ANTHROPIC_BASE_URL and
|
|
3403
|
+
# WEAVE_ROUTER_BASE_URL may also be set in the environment. A file:// base
|
|
3404
|
+
# URL is the offline/test seam: curl reads it as the response body
|
|
3405
|
+
# directly, so the endpoint path is meaningless for it; real router URLs
|
|
3406
|
+
# (https) get /v1/display-settings appended.
|
|
3407
|
+
(
|
|
3408
|
+
exec </dev/null
|
|
3409
|
+
|
|
3410
|
+
# Take the per-cache-key mutex, or bail. mkdir is the portable atomic
|
|
3411
|
+
# test-and-set. A crashed holder would otherwise block refreshes forever,
|
|
3412
|
+
# so a lock older than the fetch timeout is treated as abandoned and
|
|
3413
|
+
# reclaimed. Releasing from a trap covers every exit path below.
|
|
3414
|
+
lock="$cache.lock"
|
|
3415
|
+
if ! mkdir "$lock" 2>/dev/null; then
|
|
3416
|
+
lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
|
|
3417
|
+
lock_now="$(date +%s 2>/dev/null)" || lock_now=0
|
|
3418
|
+
if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
|
|
3419
|
+
exit 0
|
|
3420
|
+
fi
|
|
3421
|
+
# Reclaiming an abandoned lock must not be delete-then-recreate. With
|
|
3422
|
+
# `rm -rf` + `mkdir`, refreshers that all see the same stale lock each
|
|
3423
|
+
# delete the next one's freshly created directory, so many end up holding
|
|
3424
|
+
# it at once (measured at 50 claimants: 4-11 concurrent holders) and their
|
|
3425
|
+
# writes can land out of order. Renaming is atomic, so only one racer can
|
|
3426
|
+
# move a given directory aside and the losers exit instead of clobbering
|
|
3427
|
+
# the winner (same measurement: 1-2). It is not a perfect mutex — a
|
|
3428
|
+
# straggler can still reclaim the new lock a winner just created, since
|
|
3429
|
+
# nothing distinguishes it from the stale one — but real refreshes arrive
|
|
3430
|
+
# one per turn rather than 50 at once, and the staleness threshold below
|
|
3431
|
+
# is what bounds the rest.
|
|
3432
|
+
dead="$lock.dead.$$"
|
|
3433
|
+
mv "$lock" "$dead" 2>/dev/null || exit 0
|
|
3434
|
+
rm -rf "$dead" 2>/dev/null
|
|
3435
|
+
mkdir "$lock" 2>/dev/null || exit 0
|
|
3436
|
+
fi
|
|
3437
|
+
trap 'rmdir "$lock" 2>/dev/null' EXIT
|
|
3438
|
+
|
|
3439
|
+
self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd)"
|
|
3440
|
+
settings_base="$HOME"
|
|
3441
|
+
case "$self_dir" in
|
|
3442
|
+
*/.claude) settings_base="${self_dir%/.claude}" ;;
|
|
3443
|
+
esac
|
|
3444
|
+
settings="$settings_base/.claude/settings.json"
|
|
3445
|
+
local_settings="$settings_base/.claude/settings.local.json"
|
|
3446
|
+
base_url="${WEAVE_ROUTER_BASE_URL:-${ANTHROPIC_BASE_URL:-}}"
|
|
3447
|
+
key="${WEAVE_ROUTER_KEY:-}"
|
|
3448
|
+
if [ -z "$key" ] && [ -f "$settings" ]; then
|
|
3449
|
+
key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$settings" 2>/dev/null | head -n1)"
|
|
3450
|
+
fi
|
|
3451
|
+
if [ -z "$key" ] && [ -f "$local_settings" ]; then
|
|
3452
|
+
key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$local_settings" 2>/dev/null | head -n1)"
|
|
3453
|
+
fi
|
|
3454
|
+
if [ -z "$base_url" ] && [ -f "$settings" ]; then
|
|
3455
|
+
base_url="$(jq -r '.env.ANTHROPIC_BASE_URL // empty' "$settings" 2>/dev/null)"
|
|
3456
|
+
fi
|
|
3457
|
+
[ -n "$base_url" ] && [ -n "$key" ] || exit 0
|
|
3458
|
+
url="${base_url%/}"
|
|
3459
|
+
case "$url" in
|
|
3460
|
+
file://*) ;;
|
|
3461
|
+
*) url="$url/v1/display-settings" ;;
|
|
3462
|
+
esac
|
|
3463
|
+
body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
|
|
3464
|
+
hidden="$(printf '%s' "$body" | jq -r '.hide_terminal_surfaces // false' 2>/dev/null)"
|
|
3465
|
+
tmp="$cache.tmp.$$"
|
|
3466
|
+
if [ "$hidden" = "true" ]; then
|
|
3467
|
+
printf '1' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
|
|
3468
|
+
else
|
|
3469
|
+
printf '0' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
|
|
3470
|
+
fi
|
|
3471
|
+
rm -f "$tmp" 2>/dev/null
|
|
3472
|
+
) >/dev/null 2>&1 &
|
|
3473
|
+
disown 2>/dev/null || true
|
|
3474
|
+
return 1
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
if weave_hidden_gate </dev/null; then
|
|
3478
|
+
exit 0
|
|
3479
|
+
fi
|
|
3480
|
+
|
|
2540
3481
|
input="$(cat)"
|
|
2541
3482
|
transcript_path="$(printf '%s' "$input" | jq -r '.transcript_path // empty')"
|
|
2542
3483
|
# Prefer model.id over display_name: pricing keys + the routed model id in
|
|
@@ -2577,6 +3518,7 @@ prices='{
|
|
|
2577
3518
|
"claude-sonnet-5": 0.003,
|
|
2578
3519
|
"deepseek/deepseek-v4-flash": 0.0001134,
|
|
2579
3520
|
"deepseek/deepseek-v4-pro": 0.00174,
|
|
3521
|
+
"deepseek/deepseek-v4-pro-0813": 0.00174,
|
|
2580
3522
|
"gemini-2.0-flash": 0.0001,
|
|
2581
3523
|
"gemini-2.0-flash-lite": 0.000075,
|
|
2582
3524
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -2589,6 +3531,7 @@ prices='{
|
|
|
2589
3531
|
"gemini-3.5-flash": 0.0015,
|
|
2590
3532
|
"gemini-3.5-flash-lite": 0.0003,
|
|
2591
3533
|
"gemini-3.6-flash": 0.0015,
|
|
3534
|
+
"gemini-3.7-flash": 0.0015,
|
|
2592
3535
|
"google/gemma-4-26b-a4b-it": 0.00015,
|
|
2593
3536
|
"gpt-4.1": 0.002,
|
|
2594
3537
|
"gpt-4.1-mini": 0.0004,
|
|
@@ -2608,7 +3551,9 @@ prices='{
|
|
|
2608
3551
|
"gpt-5.5-nano": 0.00015,
|
|
2609
3552
|
"gpt-5.5-pro": 0.03,
|
|
2610
3553
|
"gpt-5.6-luna": 0.001,
|
|
3554
|
+
"gpt-5.6-luna-pro": 0.001,
|
|
2611
3555
|
"gpt-5.6-sol": 0.005,
|
|
3556
|
+
"gpt-5.6-sol-pro": 0.005,
|
|
2612
3557
|
"gpt-5.6-terra": 0.0025,
|
|
2613
3558
|
"grok-4.5": 0.002,
|
|
2614
3559
|
"grok-4.6": 0.002,
|
|
@@ -2631,7 +3576,8 @@ prices='{
|
|
|
2631
3576
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
2632
3577
|
"z-ai/glm-5": 0.001,
|
|
2633
3578
|
"z-ai/glm-5.1": 0.0014,
|
|
2634
|
-
"z-ai/glm-5.2": 0.0014
|
|
3579
|
+
"z-ai/glm-5.2": 0.0014,
|
|
3580
|
+
"z-ai/glm-5.3-flash": 0.00015
|
|
2635
3581
|
},
|
|
2636
3582
|
"output": {
|
|
2637
3583
|
"claude-fable-5": 0.05,
|
|
@@ -2648,6 +3594,7 @@ prices='{
|
|
|
2648
3594
|
"claude-sonnet-5": 0.015,
|
|
2649
3595
|
"deepseek/deepseek-v4-flash": 0.0002791,
|
|
2650
3596
|
"deepseek/deepseek-v4-pro": 0.00348,
|
|
3597
|
+
"deepseek/deepseek-v4-pro-0813": 0.00348,
|
|
2651
3598
|
"gemini-2.0-flash": 0.0004,
|
|
2652
3599
|
"gemini-2.0-flash-lite": 0.0003,
|
|
2653
3600
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -2660,6 +3607,7 @@ prices='{
|
|
|
2660
3607
|
"gemini-3.5-flash": 0.009,
|
|
2661
3608
|
"gemini-3.5-flash-lite": 0.0025,
|
|
2662
3609
|
"gemini-3.6-flash": 0.0075,
|
|
3610
|
+
"gemini-3.7-flash": 0.0075,
|
|
2663
3611
|
"google/gemma-4-26b-a4b-it": 0.0006,
|
|
2664
3612
|
"gpt-4.1": 0.008,
|
|
2665
3613
|
"gpt-4.1-mini": 0.0016,
|
|
@@ -2679,7 +3627,9 @@ prices='{
|
|
|
2679
3627
|
"gpt-5.5-nano": 0.0006,
|
|
2680
3628
|
"gpt-5.5-pro": 0.18,
|
|
2681
3629
|
"gpt-5.6-luna": 0.006,
|
|
3630
|
+
"gpt-5.6-luna-pro": 0.006,
|
|
2682
3631
|
"gpt-5.6-sol": 0.03,
|
|
3632
|
+
"gpt-5.6-sol-pro": 0.03,
|
|
2683
3633
|
"gpt-5.6-terra": 0.015,
|
|
2684
3634
|
"grok-4.5": 0.006,
|
|
2685
3635
|
"grok-4.6": 0.006,
|
|
@@ -2702,7 +3652,8 @@ prices='{
|
|
|
2702
3652
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
2703
3653
|
"z-ai/glm-5": 0.0032,
|
|
2704
3654
|
"z-ai/glm-5.1": 0.0044,
|
|
2705
|
-
"z-ai/glm-5.2": 0.0044
|
|
3655
|
+
"z-ai/glm-5.2": 0.0044,
|
|
3656
|
+
"z-ai/glm-5.3-flash": 0.0005
|
|
2706
3657
|
}
|
|
2707
3658
|
}'
|
|
2708
3659
|
# END_GENERATED_PRICES
|
|
@@ -2757,6 +3708,12 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
2757
3708
|
# * unrecognized model → "… isn't a recognized model · keeping
|
|
2758
3709
|
# automatic routing" — a NO-OP: the prior
|
|
2759
3710
|
# pin, if any, is left untouched
|
|
3711
|
+
# * listing (historical) → "… pick a model by id …" / "no models are
|
|
3712
|
+
# available to pin …" — also NO-OPs. The
|
|
3713
|
+
# router no longer emits these, but a
|
|
3714
|
+
# transcript written while it did still
|
|
3715
|
+
# carries them, and they must not read as
|
|
3716
|
+
# a clear or a live pin loses its [forced]
|
|
2760
3717
|
# These persist on disk (the ingress stripper only scrubs them from upstream
|
|
2761
3718
|
# requests). Classify each weave-router turn newest-first, skip the no-op
|
|
2762
3719
|
# "rejected" acks, and let the latest real state change decide: an "applied"
|
|
@@ -2770,6 +3727,7 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
2770
3727
|
| ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
|
|
2771
3728
|
| if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
|
|
2772
3729
|
elif ($t | test("isn.t a recognized model")) then "REJECTED"
|
|
3730
|
+
elif ($t | test("pick a model by id|no models are available to pin")) then "REJECTED"
|
|
2773
3731
|
else "CLEARED" end' 2>/dev/null \
|
|
2774
3732
|
| grep -m1 -v '^REJECTED$' || true)"
|
|
2775
3733
|
if [[ "$force_state" == APPLIED\ * ]]; then
|
|
@@ -3034,8 +3992,8 @@ write_claude_settings() {
|
|
|
3034
3992
|
ok "Settings written to $settings_file"
|
|
3035
3993
|
|
|
3036
3994
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
3037
|
-
jq -n --arg header "$custom_headers" '{
|
|
3038
|
-
env: { ANTHROPIC_CUSTOM_HEADERS: $header }
|
|
3995
|
+
jq -n --arg header "$custom_headers" --arg router_url "$base_url" '{
|
|
3996
|
+
env: { ANTHROPIC_CUSTOM_HEADERS: $header, WEAVE_ROUTER_BASE_URL: $router_url }
|
|
3039
3997
|
}' >"$tmp_patch"
|
|
3040
3998
|
if [ -f "$local_settings_file" ]; then
|
|
3041
3999
|
# Also strip ANTHROPIC_BASE_URL: `off` in project scope points this file
|