Edit a preset's tiers from the bar panel
The gear on every preset row now opens an editor: the four tiers with the model each maps to, and for each one field that filters the provider's catalogue as you type (every word must match; arrows and Enter work), listed inline with context length and price, and that also takes any id typed by hand. LM Studio's server form moves one click inside the editor. The panel card moves from PopupCard to KeyboardPanel. PopupCard is an xdg-popup, which only receives keys after focus is routed through its parent surface, so no text field in it could ever be typed into - the existing server URL form included. KeyboardPanel primes layer-shell keyboard focus on open, which is why every shell panel with a text field uses it. Esc now closes the panel. The picker reads ~/.claude-mode/models-cache.json, which or_catalogue and lms_catalogue now write as a side effect, so models, doctor, setup and the menu's picker all keep it fresh and the panel never hits the network itself. One node per provider with its own fetchedAt/ok; a failed fetch keeps the old list, an LM Studio list is tied to its server, and no key or key name is ever stored. `models` gains --preset, --refresh and --json, and the Z.AI list now lives in one place. A tier edit to the active preset re-applies without the running-sessions prompt: that prompt guards against the endpoint or key moving, and a tier edit moves neither (preset url/auth still ask). A failed re-apply now says the edit was saved. `preset set` on an unknown name no longer creates it. Panel edits run through one chain that stops on the first failure and refreshes health.json at the end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+130
-31
@@ -17,6 +17,7 @@ CM_PRESETS="$CM_ROOT/presets"
|
||||
CM_BACKUPS="$CM_ROOT/backups"
|
||||
CM_STATE="$CM_ROOT/state.json"
|
||||
CM_IGNORED="$CM_ROOT/ignored-sessions.json"
|
||||
CM_MODELS_CACHE="$CM_ROOT/models-cache.json"
|
||||
CM_SETTINGS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
CM_SETTINGS="$CM_SETTINGS_DIR/settings.json"
|
||||
CM_HELPER="$CM_BIN/claude-key-helper.sh"
|
||||
@@ -28,6 +29,7 @@ JSON="$CM_BIN/cm-json.py"
|
||||
. "$CM_BIN/cm-vault.sh"
|
||||
|
||||
CM_FORCE=0
|
||||
CM_SAME_ENDPOINT=0 # set by reapply_if_active for a tier-only edit
|
||||
|
||||
MODES=(anthropic openrouter zai lmstudio)
|
||||
TIERS=(opus sonnet haiku fable)
|
||||
@@ -272,6 +274,9 @@ claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
||||
|
||||
claude-mode set-key [ref] [key] store an API key (hidden prompt; key for scripts)
|
||||
claude-mode models [filter] models available from the active provider
|
||||
claude-mode models --preset <name> [--refresh|--json]
|
||||
that preset's provider instead; --refresh only
|
||||
updates the panel's cached list
|
||||
claude-mode doctor verify auth, endpoint, model ids, env
|
||||
claude-mode repair strip [1m] tags from cached model ids
|
||||
claude-mode health refresh health.json (machine-readable state)
|
||||
@@ -1132,8 +1137,32 @@ check_stray_env() {
|
||||
# Provider catalogues
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Every fetch leaves a copy behind for a reader that cannot afford the round
|
||||
# trip - the bar panel's model picker. It is written here, in the only places a
|
||||
# catalogue is ever fetched, so every command that already pays for the network
|
||||
# (models, doctor, setup, the menu's picker) keeps it fresh at no extra cost.
|
||||
cm_cache_catalogue() {
|
||||
"$PY" "$JSON" cache-models "$CM_MODELS_CACHE" "$1" "$2" "${3:-}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
or_catalogue() {
|
||||
curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null | "$PY" "$JSON" or-models 2>/dev/null
|
||||
local out rc
|
||||
out="$(curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null | "$PY" "$JSON" or-models 2>/dev/null)"
|
||||
rc=$?
|
||||
cm_cache_catalogue openrouter "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" <<<"$out"
|
||||
[ -n "$out" ] && printf '%s\n' "$out"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Z.AI publishes no catalogue endpoint, so this list comes from its docs - and
|
||||
# this is the one place it is kept.
|
||||
zai_catalogue() {
|
||||
local out
|
||||
out="$(printf '%s\t%s\n' \
|
||||
glm-5.3 'flagship coding model - opus/sonnet tier' \
|
||||
glm-4.7 'fast/cheap tier - haiku')"
|
||||
cm_cache_catalogue zai 1 <<<"$out"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
# The credential a preset would send. Empty when there is nothing to send.
|
||||
@@ -1153,9 +1182,13 @@ cm_preset_token() {
|
||||
# the catalogue comes back empty and every caller silently believes the server
|
||||
# has no models installed - on exactly the setups that need the list most.
|
||||
lms_catalogue() {
|
||||
local base="${1%/}" token="${2:-}"
|
||||
curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null
|
||||
local base="${1%/}" token="${2:-}" out rc
|
||||
out="$(curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null)"
|
||||
rc=$?
|
||||
cm_cache_catalogue lmstudio "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" "$base" <<<"$out"
|
||||
[ -n "$out" ] && printf '%s\n' "$out"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1221,30 +1254,73 @@ cmd_presets() {
|
||||
}
|
||||
|
||||
cmd_models() {
|
||||
local filter="${1:-}" mode preset pf
|
||||
mode="$(state_mode)"; preset="$(state_preset)"; pf="$(preset_path "$preset")"
|
||||
local filter='' pname='' refresh=0 as_json=0 a pf='' provider base='' tsv rc
|
||||
while [ $# -gt 0 ]; do
|
||||
a="$1"; shift
|
||||
case "$a" in
|
||||
--preset)
|
||||
[ $# -gt 0 ] || { err '--preset needs a preset name'; return 1; }
|
||||
pname="$1"; shift ;;
|
||||
--refresh) refresh=1 ;;
|
||||
--json) as_json=1 ;;
|
||||
-*) err "unknown option '$a'"; return 1 ;;
|
||||
*) filter="$a" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
# The active preset by default. The panel's editor names one instead,
|
||||
# because the preset being edited is often not the one in use - and two
|
||||
# LM Studio presets can point at two different servers.
|
||||
if [ -z "$pname" ] && [ "$(state_mode)" != "anthropic" ]; then
|
||||
pname="$(state_preset)"
|
||||
fi
|
||||
if [ -n "$pname" ]; then
|
||||
pf="$(preset_path "$pname")"
|
||||
[ -f "$pf" ] || { err "preset '$pname' not found"; return 1; }
|
||||
provider="$(jget "$pf" provider)"; [ -z "$provider" ] && provider=openrouter
|
||||
base="$(jget "$pf" baseUrl)"
|
||||
else
|
||||
provider=openrouter # on anthropic, the catalogue worth browsing
|
||||
fi
|
||||
|
||||
local quiet=$(( refresh || as_json ))
|
||||
case "$provider" in
|
||||
lmstudio)
|
||||
head_ "models installed in LM Studio at $(jget "$pf" baseUrl)"
|
||||
lms_catalogue "$(jget "$pf" baseUrl)" "$(cm_preset_token "$pf")" | while IFS=$'\t' read -r id st ctx; do
|
||||
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||
printf ' %-58s %-11s %s\n' "$id" "$st" "$ctx"
|
||||
done
|
||||
;;
|
||||
[ "$quiet" -eq 1 ] || head_ "models installed in LM Studio at $base"
|
||||
tsv="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"; rc=$? ;;
|
||||
zai)
|
||||
head_ 'Z.AI GLM models (from Z.AI docs - no public catalogue endpoint)'
|
||||
say 'glm-5.3 - flagship coding model (opus/sonnet tier)'
|
||||
say 'glm-4.7 - fast/cheap tier (haiku tier)'
|
||||
;;
|
||||
[ "$quiet" -eq 1 ] || head_ 'Z.AI GLM models (from Z.AI docs - no public catalogue endpoint)'
|
||||
tsv="$(zai_catalogue)"; rc=$? ;;
|
||||
*)
|
||||
head_ 'fetching https://openrouter.ai/api/v1/models ...'
|
||||
or_catalogue | while IFS=$'\t' read -r id ctx pin pout; do
|
||||
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||
printf ' %-52s %10s $%-8s $%s\n' "$id" "$ctx" "$pin" "$pout"
|
||||
done
|
||||
;;
|
||||
[ "$quiet" -eq 1 ] || head_ 'fetching https://openrouter.ai/api/v1/models ...'
|
||||
tsv="$(or_catalogue)"; rc=$? ;;
|
||||
esac
|
||||
|
||||
if [ "$as_json" -eq 1 ]; then
|
||||
jget "$CM_MODELS_CACHE" "providers.$provider"
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
if [ "$refresh" -eq 1 ]; then
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
err "could not fetch the $provider catalogue${base:+ from $base}; the cached list, if any, is kept"
|
||||
return 1
|
||||
fi
|
||||
ok "cached $(printf '%s\n' "$tsv" | grep -c .) $provider model(s)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ "$rc" -eq 0 ] || { err "could not fetch the $provider catalogue${base:+ from $base}"; return 1; }
|
||||
local id c2 c3 c4
|
||||
while IFS=$'\t' read -r id c2 c3 c4; do
|
||||
[ -n "$id" ] || continue
|
||||
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||
case "$provider" in
|
||||
lmstudio) printf ' %-58s %-11s %s\n' "$id" "$c2" "$c3" ;;
|
||||
zai) say "$(printf '%-8s - %s' "$id" "$c2")" ;;
|
||||
*) printf ' %-52s %10s $%-8s $%s\n' "$id" "$c2" "$c3" "$c4" ;;
|
||||
esac
|
||||
done <<<"$tsv"
|
||||
}
|
||||
|
||||
cmd_set_key() {
|
||||
@@ -1456,19 +1532,23 @@ cmd_preset() {
|
||||
set)
|
||||
local tier="${3:-}" model="${4:-}"
|
||||
[ -n "$name" ] && [ -n "$tier" ] && [ -n "$model" ] || { err 'usage: claude-mode preset set <name> <tier> <model-id>'; return 1; }
|
||||
# set-tier saves whatever it loaded, so a mistyped name would
|
||||
# otherwise quietly become a new, half-empty preset.
|
||||
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
||||
"$PY" "$JSON" set-tier "$(preset_path "$name")" "$tier" "$model" || return 1
|
||||
ok "$name : $tier -> $model"
|
||||
reapply_if_active "$name"
|
||||
reapply_if_active "$name" models
|
||||
;;
|
||||
all)
|
||||
local model="${3:-}"
|
||||
[ -n "$name" ] && [ -n "$model" ] || { err 'usage: claude-mode preset all <name> <model-id>'; return 1; }
|
||||
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
||||
local t
|
||||
for t in "${TIERS[@]}" subagent; do
|
||||
"$PY" "$JSON" set-tier "$(preset_path "$name")" "$t" "$model" || return 1
|
||||
done
|
||||
ok "$name : all tiers + subagent -> $model"
|
||||
reapply_if_active "$name"
|
||||
reapply_if_active "$name" models
|
||||
;;
|
||||
url)
|
||||
local url="${3:-}"
|
||||
@@ -1495,11 +1575,24 @@ cmd_preset() {
|
||||
esac
|
||||
}
|
||||
|
||||
# scope `models` marks an edit that changed tier mappings only. What makes a
|
||||
# switch dangerous to running sessions is the endpoint or the key changing under
|
||||
# them, and a tier edit changes neither - so there is nothing to ask them about,
|
||||
# and the bar panel (which cannot answer a prompt) can edit the active preset.
|
||||
reapply_if_active() {
|
||||
local name="$1"
|
||||
if [ "$(state_mode)" != "anthropic" ] && [ "$(state_preset)" = "$name" ]; then
|
||||
local name="$1" scope="${2:-}" mode
|
||||
mode="$(state_mode)"
|
||||
if [ "$mode" != "anthropic" ] && [ "$(state_preset)" = "$name" ]; then
|
||||
say 're-applying active preset...'
|
||||
set_mode "$(state_mode)" "$name"
|
||||
[ "$scope" = models ] && CM_SAME_ENDPOINT=1
|
||||
if ! set_mode "$mode" "$name"; then
|
||||
CM_SAME_ENDPOINT=0
|
||||
# Last on stderr on purpose: it is the line the panel shows, and
|
||||
# the file *was* written, which the failure above does not say.
|
||||
err "saved, but re-applying the active preset failed - run: claude-mode $mode $name"
|
||||
return 1
|
||||
fi
|
||||
CM_SAME_ENDPOINT=0
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1769,8 +1862,10 @@ ui_pick_model() {
|
||||
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||
;;
|
||||
zai)
|
||||
ids+=('glm-5.3'); ui_add_item 'glm-5.3' 'flagship coding model - opus/sonnet tier'
|
||||
ids+=('glm-4.7'); ui_add_item 'glm-4.7' 'fast/cheap tier - haiku'
|
||||
while IFS=$'\t' read -r id a; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "$a"
|
||||
done < <(zai_catalogue)
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -2151,6 +2246,10 @@ cm_confirm_sessions() {
|
||||
CM_SESSION_ACTION=none
|
||||
CM_SESSION_ROWS=''
|
||||
|
||||
# Re-applying the active preset after a tier edit: same endpoint, same key,
|
||||
# so running sessions are not at risk - see reapply_if_active.
|
||||
[ "$CM_SAME_ENDPOINT" -eq 1 ] && return 0
|
||||
|
||||
if ! cm_sessions_supported; then
|
||||
printf '\n'
|
||||
warn 'cannot list running sessions here (no /proc) - the switch will not wait for them'
|
||||
@@ -2584,7 +2683,7 @@ case "$cmd" in
|
||||
else
|
||||
cmd_set_key "$_ref" "$_secret"
|
||||
fi ;;
|
||||
models) cmd_models "${1:-}" ;;
|
||||
models) cmd_models "$@" ;;
|
||||
doctor) cmd_doctor ;;
|
||||
health) write_health "$(state_mode)" "$(state_preset)" ;;
|
||||
preflight) cmd_preflight "${1:-}" "${2:-}" ;;
|
||||
|
||||
@@ -16,6 +16,7 @@ Subcommands:
|
||||
presets <dir> "name<TAB>provider<TAB>desc"
|
||||
scan-sessions <projects> [max-age-days] [ignored] broken transcripts, as JSON
|
||||
ignore-session <ignored> <projects> add|remove|clear|list [id]
|
||||
cache-models <cache> <provider> <1|0> [baseUrl] store a TSV catalogue (stdin)
|
||||
"""
|
||||
|
||||
import glob
|
||||
@@ -313,6 +314,76 @@ def cmd_or_models(argv):
|
||||
print("%s\t%s\t%s\t%s" % (m.get("id", ""), m.get("context_length", ""), per_m("prompt"), per_m("completion")))
|
||||
|
||||
|
||||
def cmd_cache_models(argv):
|
||||
"""cache-models <cache.json> <provider> <1|0> [baseUrl] TSV catalogue on stdin
|
||||
|
||||
Keeps the last catalogue each provider returned, for a reader that cannot
|
||||
afford a network round trip - the bar panel's model picker. One node per
|
||||
provider with its own fetchedAt and ok, so a failed OpenRouter fetch never
|
||||
invalidates a fresh LM Studio list. A failed fetch keeps the previous list
|
||||
(a stale list beats none when the network blinks) and marks it ok=false.
|
||||
|
||||
Model ids, context lengths and prices only: never a key reference or token.
|
||||
Prints the number of models stored by this call.
|
||||
"""
|
||||
path, provider, ok = argv[0], argv[1], argv[2] == "1"
|
||||
base = argv[3].rstrip("/") if len(argv) > 3 else ""
|
||||
|
||||
def num(s, cast):
|
||||
try:
|
||||
return cast(s)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
rows = []
|
||||
for line in sys.stdin.read().splitlines():
|
||||
parts = line.split("\t")
|
||||
mid = parts[0].strip() if parts else ""
|
||||
if not mid:
|
||||
continue
|
||||
m = {"id": mid}
|
||||
if provider == "openrouter":
|
||||
ctx, pin, pout = (parts[1:] + ["", "", ""])[:3]
|
||||
m["contextTokens"] = num(ctx, int)
|
||||
m["priceIn"] = num(pin, float)
|
||||
m["priceOut"] = num(pout, float)
|
||||
elif provider == "lmstudio":
|
||||
state, ctx = (parts[1:] + ["", ""])[:2]
|
||||
m["state"] = state
|
||||
m["contextTokens"] = num(ctx, int)
|
||||
else:
|
||||
m["note"] = parts[1] if len(parts) > 1 else ""
|
||||
rows.append(m)
|
||||
|
||||
try:
|
||||
data = load(path, {})
|
||||
except (OSError, ValueError):
|
||||
data = {}
|
||||
providers = data.get("providers") if isinstance(data, dict) else None
|
||||
if not isinstance(providers, dict):
|
||||
providers = {}
|
||||
node = providers.get(provider)
|
||||
if not isinstance(node, dict):
|
||||
node = {}
|
||||
|
||||
now = __import__("datetime").datetime.now(
|
||||
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
if ok:
|
||||
node = {"fetchedAt": now, "ok": True, "models": rows}
|
||||
if provider == "lmstudio":
|
||||
node["baseUrl"] = base
|
||||
else:
|
||||
# A list from a different LM Studio server is not stale, it is wrong.
|
||||
if provider == "lmstudio" and node.get("baseUrl", "") != base:
|
||||
node = {"baseUrl": base, "models": []}
|
||||
node["ok"] = False
|
||||
node["failedAt"] = now
|
||||
|
||||
providers[provider] = node
|
||||
save(path, {"schema": 1, "providers": providers})
|
||||
print(len(rows) if ok else 0)
|
||||
|
||||
|
||||
def cmd_lms_models(argv):
|
||||
"""LM Studio /api/v0/models on stdin -> 'id<TAB>state<TAB>ctx' lines."""
|
||||
data = json.loads(sys.stdin.read())
|
||||
@@ -1095,6 +1166,7 @@ COMMANDS = {
|
||||
"strip-tags": cmd_strip_tags,
|
||||
"or-models": cmd_or_models,
|
||||
"lms-models": cmd_lms_models,
|
||||
"cache-models": cmd_cache_models,
|
||||
"apply": cmd_apply,
|
||||
"summary": cmd_summary,
|
||||
"models": cmd_models,
|
||||
|
||||
Reference in New Issue
Block a user