One preset per mode, and a first-run setup that fills it in
The openrouter default moves its hot tiers: opus to z-ai/glm-5.3-flash and sonnet to deepseek/deepseek-v4-flash-0731. haiku and fable are unchanged. `cheap` and `lmstudio-qwen` are gone, leaving exactly one preset per mode so `claude-mode <mode>` is never ambiguous and there is no menu to read before the thing you asked for happens. The surviving lmstudio preset keeps the Qwen3.6 model rather than KAT-Coder: the two differed mainly in that KAT's chat template carries the message-order assertion this README already warns about, so between two presets that had to become one, the one that is known to work won. More presets are still a `preset new` away; the shipped set is a starting point, not a ceiling. Which is the other half of this. A shipped preset was never a working configuration - OpenRouter and Z.AI have no key stored, and lmstudio's model ids were whatever happened to be installed on the machine this was packaged on. That was left for the user to discover through a failure. Now the shipped presets carry `configured: false`, preflight blocks on it, and `claude-mode setup <mode>` walks through what is actually needed: key, server URL and auth for LM Studio, then models chosen from the provider's own catalogue rather than typed from memory. A switch that trips this in a terminal offers to run setup there and then instead of printing a command to type next. Absent means configured, deliberately: presets that predate this and any built by hand with `preset new` do not suddenly start demanding a wizard. The panel gets a "Set up <mode>…" button that hands the whole flow to a terminal, since a bar popup can host neither a hidden key prompt nor a filter-select list. Two bugs found while testing it, both real: ask_value printed its prompt to stdout while being called inside $( ), so the prompt text came back glued to the front of the answer and set-url rejected the result. Moved to stderr, which is why warn and err already go there. lms_catalogue never sent the API key. On a server with authentication switched on - the case just added support for - /api/v0/models answers 401 like anything else, so the catalogue came back empty and every caller silently concluded the server had no models installed. It now sends the preset's credential, as do the three other call sites that read it.
This commit is contained in:
@@ -127,13 +127,15 @@ used" — so `claude-mode openrouter` always means `default`.
|
|||||||
|
|
||||||
## Presets shipped
|
## Presets shipped
|
||||||
|
|
||||||
|
One per mode, so `claude-mode <mode>` is never ambiguous and there is no
|
||||||
|
menu to read before the thing you asked for happens. Build more with
|
||||||
|
`preset new` whenever one stops being enough.
|
||||||
|
|
||||||
| preset | provider | opus | sonnet | haiku | fable |
|
| preset | provider | opus | sonnet | haiku | fable |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `default` | openrouter | `deepseek/deepseek-v4-flash-0731` | `qwen/qwen3.7-flash` | `openrouter/free` | `z-ai/glm-5.3` |
|
| `default` | openrouter | `z-ai/glm-5.3-flash` | `deepseek/deepseek-v4-flash-0731` | `openrouter/free` | `z-ai/glm-5.3` |
|
||||||
| `cheap` | openrouter | `deepseek/deepseek-v4-pro` | `deepseek/deepseek-v4-flash-0731` | `qwen/qwen3.7-flash` | `openai/gpt-5.6-luna-pro` |
|
|
||||||
| `zai` | zai | `glm-5.3` | `glm-5.3` | `glm-4.7` | `glm-5.3` |
|
| `zai` | zai | `glm-5.3` | `glm-5.3` | `glm-4.7` | `glm-5.3` |
|
||||||
| `lmstudio` | lmstudio | `kwaipilot_kat-coder-v2.5-dev` (all tiers) | | | |
|
| `lmstudio` | lmstudio | whatever setup finds on your server (all tiers) | | | |
|
||||||
| `lmstudio-qwen` | lmstudio | `qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved` (all tiers) | | | |
|
|
||||||
|
|
||||||
Presets are plain JSON in `~/.claude-mode/presets/`. A preset declares its
|
Presets are plain JSON in `~/.claude-mode/presets/`. A preset declares its
|
||||||
`provider`; `claude-mode lmstudio default` is rejected rather than silently
|
`provider`; `claude-mode lmstudio default` is rejected rather than silently
|
||||||
@@ -164,8 +166,8 @@ CLAUDE_CODE_AUTO_COMPACT_WINDOW = <contextTokens>
|
|||||||
|
|
||||||
| preset | contextTokens |
|
| preset | contextTokens |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `default`, `cheap`, `zai` | 1,000,000 |
|
| `default`, `zai` | 1,000,000 |
|
||||||
| `lmstudio`, `lmstudio-qwen` | 262,144 |
|
| `lmstudio` | 262,144 |
|
||||||
|
|
||||||
`doctor` cross-checks the declared window against each tier's *actual* model
|
`doctor` cross-checks the declared window against each tier's *actual* model
|
||||||
window and names any tier that falls short — `default` maps haiku to
|
window and names any tier that falls short — `default` maps haiku to
|
||||||
@@ -246,17 +248,18 @@ Scanning your installed models' templates:
|
|||||||
| **`qwen/qwen3.5-9b`**, **`prism-ml/bonsai-27b`** | **assert** |
|
| **`qwen/qwen3.5-9b`**, **`prism-ml/bonsai-27b`** | **assert** |
|
||||||
|
|
||||||
`doctor` now reports this per model, and `models` flags affected entries with
|
`doctor` now reports this per model, and `models` flags affected entries with
|
||||||
`TEMPLATE RISK`. **The fix is to use a model without the flag** — which is
|
`TEMPLATE RISK`. **The fix is to use a model without the flag.** The Qwen3.6
|
||||||
exactly the `lmstudio-qwen` preset, verified end-to-end with a cold JIT load,
|
line above is what the single `lmstudio` preset ships pointed at, verified
|
||||||
streaming, and tool calls.
|
end-to-end with a cold JIT load, streaming, and tool calls.
|
||||||
|
|
||||||
Honest caveat: KAT-Coder's template *does* contain the assertion, but I could not
|
Honest caveat: KAT-Coder's template *does* contain the assertion, but I could not
|
||||||
reproduce the failure against it here — cold JIT, streaming, tools, system
|
reproduce the failure against it here — cold JIT, streaming, tools, system
|
||||||
blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to
|
blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to
|
||||||
depend on which parser strategy the runtime picks. If it spams, switch:
|
depend on which parser strategy the runtime picks. If the model setup lands on
|
||||||
|
does spam, run setup again and pick another:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
claude-mode lmstudio lmstudio-qwen
|
claude-mode setup lmstudio
|
||||||
```
|
```
|
||||||
|
|
||||||
Other notes: use a model with **>25k context** (`doctor` warns below that), and
|
Other notes: use a model with **>25k context** (`doctor` warns below that), and
|
||||||
@@ -402,6 +405,38 @@ active configuration: mode, preset, model map, context window, key backend, and
|
|||||||
the switchable preset catalogue. No key material. A switch and a `status` both
|
the switchable preset catalogue. No key material. A switch and a `status` both
|
||||||
refresh it; the command exists for anything that wants to force it.
|
refresh it; the command exists for anything that wants to force it.
|
||||||
|
|
||||||
|
## First-run setup
|
||||||
|
|
||||||
|
A shipped preset is a starting point, not a working configuration. OpenRouter
|
||||||
|
and Z.AI have no key stored. LM Studio's model ids are whatever happened to be
|
||||||
|
installed on the machine this was packaged on, which is almost certainly not
|
||||||
|
yours.
|
||||||
|
|
||||||
|
So the shipped presets carry `configured: false`, and preflight treats that as a
|
||||||
|
blocker with its own remedy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude-mode setup openrouter # key, then models from OpenRouter's catalogue
|
||||||
|
claude-mode setup zai # key, then per-tier GLM models
|
||||||
|
claude-mode setup lmstudio # server URL, auth, then models from that server
|
||||||
|
claude-mode setup anthropic # nothing to do; it uses your existing login
|
||||||
|
```
|
||||||
|
|
||||||
|
Setup asks only what it cannot work out, shows the current model map before
|
||||||
|
offering to change it, and picks from the provider's own catalogue rather than
|
||||||
|
asking anyone to type a model id from memory. LM Studio maps one model across
|
||||||
|
every tier, since a local server has one loaded at a time and per-tier mapping
|
||||||
|
would just pay the load cost on every tier change; the remote gateways ask per
|
||||||
|
tier, which is the point of them.
|
||||||
|
|
||||||
|
In a terminal, a switch that trips this offers to run setup there and then
|
||||||
|
rather than printing a command to type next. It sets `configured: true` on the
|
||||||
|
way out.
|
||||||
|
|
||||||
|
**Absent means configured.** Presets that predate this, and any built by hand
|
||||||
|
with `preset new`, are nobody's business but yours and will not start demanding
|
||||||
|
a wizard.
|
||||||
|
|
||||||
## Refusing a switch that would not work
|
## Refusing a switch that would not work
|
||||||
|
|
||||||
A switch writes settings.json and is picked up by the *next* `claude` launch, so
|
A switch writes settings.json and is picked up by the *next* `claude` launch, so
|
||||||
@@ -417,6 +452,7 @@ So the preconditions are checked before the write, not after:
|
|||||||
|
|
||||||
| mode | checked | when it fails |
|
| mode | checked | when it fails |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| any gateway | the preset has been through setup at least once | `claude-mode setup <mode>` |
|
||||||
| `anthropic` | nothing to check | — |
|
| `anthropic` | nothing to check | — |
|
||||||
| `openrouter`, `zai` | a key exists for the preset's `keyRef`, and the helper is executable | `claude-mode set-key <ref>` |
|
| `openrouter`, `zai` | a key exists for the preset's `keyRef`, and the helper is executable | `claude-mode set-key <ref>` |
|
||||||
| `lmstudio` (anywhere) | the server answers, and accepts the credential the preset would send | see below |
|
| `lmstudio` (anywhere) | the server answers, and accepts the credential the preset would send | see below |
|
||||||
@@ -554,11 +590,12 @@ stock glyph in this bar measures:
|
|||||||
Choosing a target does not switch immediately — it runs the same two checks the
|
Choosing a target does not switch immediately — it runs the same two checks the
|
||||||
CLI does, and both can stop it:
|
CLI does, and both can stop it:
|
||||||
|
|
||||||
1. **Preflight.** If the mode has no key stored, or its server is not answering,
|
1. **Preflight.** If the mode has not been set up, has no key stored, or its
|
||||||
the panel says which and offers the fix: *Store the key…* opens a terminal
|
server is not answering, the panel says which and offers the fix: *Set up
|
||||||
for the hidden prompt (a bar popup has nowhere to host one), *Check again*
|
<mode>…* opens a terminal and runs the whole first-run flow there (a bar popup
|
||||||
re-runs the preflight once the server is up, and *Server settings…* opens the
|
can host neither a hidden key prompt nor a filter-select model list), *Store
|
||||||
form below. The gear on any LM Studio preset row opens the same form without
|
the key…* opens a terminal for just the prompt, *Check again* re-runs the
|
||||||
|
preflight, and *Server settings…* opens the form below. The gear on any LM Studio preset row opens the same form without
|
||||||
waiting for a failure — per row, because two LM Studio presets can point at
|
waiting for a failure — per row, because two LM Studio presets can point at
|
||||||
two different machines.
|
two different machines.
|
||||||
|
|
||||||
|
|||||||
+280
-8
@@ -274,6 +274,7 @@ claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
|||||||
claude-mode doctor verify auth, endpoint, model ids, env
|
claude-mode doctor verify auth, endpoint, model ids, env
|
||||||
claude-mode repair strip [1m] tags from cached model ids
|
claude-mode repair strip [1m] tags from cached model ids
|
||||||
claude-mode health refresh health.json (machine-readable state)
|
claude-mode health refresh health.json (machine-readable state)
|
||||||
|
claude-mode setup <mode> [--terminal] first-run setup: key, server, models
|
||||||
claude-mode preflight <mode> [preset] check a mode can actually serve, without switching
|
claude-mode preflight <mode> [preset] check a mode can actually serve, without switching
|
||||||
claude-mode sessions [--stop|--restart]
|
claude-mode sessions [--stop|--restart]
|
||||||
running sessions; close or reopen them
|
running sessions; close or reopen them
|
||||||
@@ -453,6 +454,13 @@ cm_preflight() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! preset_configured "$pf"; then
|
||||||
|
cm_pf_set 'needs-setup' "$(mode_label "$mode" | cut -d- -f1 | sed 's/ *$//') has not been set up yet" \
|
||||||
|
"The shipped preset is a starting point: it has no key stored, and its model ids are whatever was on the machine this was packaged on. Setup asks for what it needs and picks models from the provider's own catalogue." \
|
||||||
|
"claude-mode setup $mode" 'setup'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
base="$(jget "$pf" baseUrl)"; CM_PF_BASEURL="$base"
|
base="$(jget "$pf" baseUrl)"; CM_PF_BASEURL="$base"
|
||||||
auth_mode="$(jget "$pf" auth.mode)"; [ -z "$auth_mode" ] && auth_mode=vault
|
auth_mode="$(jget "$pf" auth.mode)"; [ -z "$auth_mode" ] && auth_mode=vault
|
||||||
|
|
||||||
@@ -868,9 +876,24 @@ set_mode() {
|
|||||||
if [ "$CM_FORCE" -eq 0 ] && ! cm_preflight "$mode" "$preset_name"; then
|
if [ "$CM_FORCE" -eq 0 ] && ! cm_preflight "$mode" "$preset_name"; then
|
||||||
err "$CM_PF_TITLE"
|
err "$CM_PF_TITLE"
|
||||||
[ -n "$CM_PF_DETAIL" ] && say "$CM_PF_DETAIL"
|
[ -n "$CM_PF_DETAIL" ] && say "$CM_PF_DETAIL"
|
||||||
[ -n "$CM_PF_REMEDY" ] && printf ' %sfix:%s %s\n' "$C_DIM" "$C_RESET" "$CM_PF_REMEDY"
|
|
||||||
printf ' %s--force switches anyway%s\n' "$C_DIM" "$C_RESET"
|
# Standing in a terminal with the fix one keystroke away, printing
|
||||||
return 1
|
# the command to type next is a poor substitute for running it.
|
||||||
|
if ui_interactive && [ "$CM_PF_KIND" = "setup" ]; then
|
||||||
|
printf '\n'
|
||||||
|
if ask_yes "set up $mode now?"; then
|
||||||
|
cmd_setup "$mode" "$preset_name" || return 1
|
||||||
|
cm_preflight "$mode" "$preset_name" || {
|
||||||
|
err "$CM_PF_TITLE"; return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[ -n "$CM_PF_REMEDY" ] && printf ' %sfix:%s %s\n' "$C_DIM" "$C_RESET" "$CM_PF_REMEDY"
|
||||||
|
printf ' %s--force switches anyway%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
[ -f "$preset_file" ] || { err "preset '$preset_name' not found"; return 1; }
|
[ -f "$preset_file" ] || { err "preset '$preset_name' not found"; return 1; }
|
||||||
fi
|
fi
|
||||||
@@ -1107,9 +1130,26 @@ or_catalogue() {
|
|||||||
curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null | "$PY" "$JSON" or-models 2>/dev/null
|
curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null | "$PY" "$JSON" or-models 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The credential a preset would send. Empty when there is nothing to send.
|
||||||
|
cm_preset_token() {
|
||||||
|
local pf="$1" am ref
|
||||||
|
am="$(jget "$pf" auth.mode)"; [ -z "$am" ] && am=vault
|
||||||
|
if [ "$am" = "vault" ]; then
|
||||||
|
ref="$(jget "$pf" auth.keyRef)"; [ -z "$ref" ] && ref=openrouter
|
||||||
|
cm_vault_get "$ref" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
jget "$pf" auth.token
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The token is not optional decoration. An LM Studio server with authentication
|
||||||
|
# switched on answers /api/v0/models with 401 like anything else, so without it
|
||||||
|
# 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() {
|
lms_catalogue() {
|
||||||
local base="${1%/}"
|
local base="${1%/}" token="${2:-}"
|
||||||
curl -fsS --max-time 10 "$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null
|
curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||||
|
"$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1181,7 +1221,7 @@ cmd_models() {
|
|||||||
case "$mode" in
|
case "$mode" in
|
||||||
lmstudio)
|
lmstudio)
|
||||||
head_ "models installed in LM Studio at $(jget "$pf" baseUrl)"
|
head_ "models installed in LM Studio at $(jget "$pf" baseUrl)"
|
||||||
lms_catalogue "$(jget "$pf" baseUrl)" | while IFS=$'\t' read -r id st ctx; do
|
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
|
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||||
printf ' %-58s %-11s %s\n' "$id" "$st" "$ctx"
|
printf ' %-58s %-11s %s\n' "$id" "$st" "$ctx"
|
||||||
done
|
done
|
||||||
@@ -1295,7 +1335,7 @@ else: print(' ok spend %.2f of %.2f limit (%s), %.2f remaining' % (use, lim,
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$mode" = "lmstudio" ]; then
|
if [ "$mode" = "lmstudio" ]; then
|
||||||
local cat; cat="$(lms_catalogue "$base")"
|
local cat; cat="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"
|
||||||
if [ -n "$cat" ]; then
|
if [ -n "$cat" ]; then
|
||||||
ok "LM Studio reachable at $base ($(printf '%s\n' "$cat" | wc -l | tr -d ' ') models installed)"
|
ok "LM Studio reachable at $base ($(printf '%s\n' "$cat" | wc -l | tr -d ' ') models installed)"
|
||||||
local t id row st ctx
|
local t id row st ctx
|
||||||
@@ -1636,7 +1676,7 @@ ui_pick_model() {
|
|||||||
while IFS=$'\t' read -r id st ctx; do
|
while IFS=$'\t' read -r id st ctx; do
|
||||||
[ -n "$id" ] || continue
|
[ -n "$id" ] || continue
|
||||||
ids+=("$id"); ui_add_item "$id" "state: $st max context: $ctx"
|
ids+=("$id"); ui_add_item "$id" "state: $st max context: $ctx"
|
||||||
done < <(lms_catalogue "$base")
|
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||||
;;
|
;;
|
||||||
zai)
|
zai)
|
||||||
ids+=('glm-5.3'); ui_add_item 'glm-5.3' 'flagship coding model - opus/sonnet tier'
|
ids+=('glm-5.3'); ui_add_item 'glm-5.3' 'flagship coding model - opus/sonnet tier'
|
||||||
@@ -1783,6 +1823,217 @@ ui_menu() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# First-run setup
|
||||||
|
#
|
||||||
|
# A shipped preset is a starting point, not a working configuration. OpenRouter
|
||||||
|
# needs a key before it can serve anything; LM Studio needs to be told where the
|
||||||
|
# server is and which of the models it actually has installed to use - and the
|
||||||
|
# ids it ships with are whatever happened to be on the machine this was written
|
||||||
|
# on, which is almost certainly not yours.
|
||||||
|
#
|
||||||
|
# So a preset says whether it has been through setup. `configured: false` is
|
||||||
|
# written into the shipped presets and cleared once setup has run, and preflight
|
||||||
|
# treats it as a blocker: better to be walked through it once than to switch
|
||||||
|
# into something that half-works and produces a confusing failure later.
|
||||||
|
#
|
||||||
|
# Absent means configured. That is deliberate - presets that predate this, and
|
||||||
|
# ones the user built by hand with `preset new`, are their own business and must
|
||||||
|
# not suddenly start demanding a wizard.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
preset_configured() {
|
||||||
|
local v; v="$(jget "$1" configured)"
|
||||||
|
[ "$v" = "false" ] && return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_configured() {
|
||||||
|
"$PY" "$JSON" set-flag "$1" configured true >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# A y/N prompt that defaults to no on anything that is not a clear yes.
|
||||||
|
ask_yes() {
|
||||||
|
local prompt="$1" reply
|
||||||
|
printf ' %s [y/N] ' "$prompt"
|
||||||
|
IFS= read -r reply || return 1
|
||||||
|
case "$reply" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prompt with a default shown in brackets; empty input keeps the default.
|
||||||
|
#
|
||||||
|
# The prompt goes to stderr for the same reason warn/err do: this is called
|
||||||
|
# inside $( ), where anything on stdout is captured as the return value. Printed
|
||||||
|
# to stdout it came back as part of the answer - " server base URL [...]: " with
|
||||||
|
# the typed URL glued on the end, which set-url then rejected.
|
||||||
|
ask_value() {
|
||||||
|
local prompt="$1" default="$2" reply
|
||||||
|
if [ -n "$default" ]; then printf ' %s [%s]: ' "$prompt" "$default" >&2
|
||||||
|
else printf ' %s: ' "$prompt" >&2; fi
|
||||||
|
IFS= read -r reply || return 1
|
||||||
|
[ -n "$reply" ] && printf '%s' "$reply" || printf '%s' "$default"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_key() {
|
||||||
|
local ref="$1" label="$2"
|
||||||
|
if cm_vault_has "$ref"; then
|
||||||
|
ok "a key is already stored for '$ref' ($(cm_vault_backend_label))"
|
||||||
|
ask_yes "replace it?" || return 0
|
||||||
|
else
|
||||||
|
say "$label needs an API key. It goes into $(cm_vault_backend_label),"
|
||||||
|
say 'not into settings.json.'
|
||||||
|
fi
|
||||||
|
cmd_set_key "$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Offer the provider's own catalogue rather than asking someone to type a model
|
||||||
|
# id from memory. Falls back to typing when the catalogue cannot be reached,
|
||||||
|
# because being offline should not block finishing setup.
|
||||||
|
setup_models() {
|
||||||
|
local pf="$1" provider="$2" name="$3"
|
||||||
|
printf '\n'
|
||||||
|
say 'current model map:'
|
||||||
|
local t v
|
||||||
|
for t in "${TIERS[@]}"; do
|
||||||
|
v="$(jget "$pf" "models.$t")"
|
||||||
|
[ -n "$v" ] && printf ' %-8s %s\n' "$t" "$v"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
ask_yes 'change which models back these tiers?' || return 0
|
||||||
|
|
||||||
|
if ! ui_interactive; then
|
||||||
|
warn 'model picking needs an interactive terminal'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$provider" = "lmstudio" ]; then
|
||||||
|
# One model for every tier is the normal shape for a local server: it
|
||||||
|
# has one loaded at a time, and mapping tiers to different models just
|
||||||
|
# means paying the load cost on every tier change.
|
||||||
|
local base ids=() id st ctx
|
||||||
|
base="$(jget "$pf" baseUrl)"
|
||||||
|
while IFS=$'\t' read -r id st ctx; do
|
||||||
|
[ -n "$id" ] || continue
|
||||||
|
ids+=("$id")
|
||||||
|
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||||
|
|
||||||
|
if [ "${#ids[@]}" -eq 0 ]; then
|
||||||
|
warn 'the server returned no models; type an id by hand instead'
|
||||||
|
local manual; manual="$(ask_value 'model id for every tier' "$(jget "$pf" models.opus)")"
|
||||||
|
[ -n "$manual" ] && "$PY" "$JSON" set-all "$pf" "$manual" >/dev/null && ok "all tiers -> $manual"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui_reset_items
|
||||||
|
for id in "${ids[@]}"; do ui_add_item "$id" 'use this for every tier'; done
|
||||||
|
if ui_filter_select "model for all tiers of '$name'" 'esc = keep current'; then
|
||||||
|
"$PY" "$JSON" set-all "$pf" "${ids[$UI_SEL]}" >/dev/null
|
||||||
|
ok "all tiers -> ${ids[$UI_SEL]}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remote gateways map a different model per tier, which is the whole point
|
||||||
|
# of them, so each tier is asked for separately.
|
||||||
|
local cur
|
||||||
|
for t in "${TIERS[@]}"; do
|
||||||
|
cur="$(jget "$pf" "models.$t")"
|
||||||
|
ui_pick_model "$pf" "$t" "$cur" || continue
|
||||||
|
[ -n "$UI_PICKED" ] || continue
|
||||||
|
"$PY" "$JSON" set-tier "$pf" "$t" "$UI_PICKED" >/dev/null && ok "$t -> $UI_PICKED"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_lmstudio_server() {
|
||||||
|
local pf="$1" name="$2" url probe token
|
||||||
|
url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url='http://127.0.0.1:1234'
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
say 'LM Studio does not have to be on this machine - a LAN address or'
|
||||||
|
say 'anything reachable through a tunnel or proxy works just as well.'
|
||||||
|
url="$(ask_value 'server base URL' "$url")"
|
||||||
|
"$PY" "$JSON" set-url "$pf" "$url" >/dev/null || return 1
|
||||||
|
ok "baseUrl -> $url"
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
if ask_yes 'does that server require an API key?'; then
|
||||||
|
local ref; ref="$(ask_value 'key name to store it under' 'lmstudio')"
|
||||||
|
"$PY" "$JSON" set-auth "$pf" key "$ref" >/dev/null
|
||||||
|
ok "auth -> vault key '$ref'"
|
||||||
|
cm_vault_has "$ref" || cmd_set_key "$ref"
|
||||||
|
token="$(cm_vault_get "$ref" 2>/dev/null || true)"
|
||||||
|
else
|
||||||
|
"$PY" "$JSON" set-auth "$pf" none >/dev/null
|
||||||
|
ok 'auth -> none (inline placeholder token)'
|
||||||
|
token='lmstudio'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
say "checking $url ..."
|
||||||
|
probe="$(cm_probe_server "$url" "$token")"
|
||||||
|
case "$probe" in
|
||||||
|
ok) ok 'server answered' ;;
|
||||||
|
auth) err 'the server refused that credential'; return 1 ;;
|
||||||
|
notfound) err 'something answered there, but not an LM Studio API' ; return 1 ;;
|
||||||
|
skip) warn 'curl is missing, so the server was not checked' ;;
|
||||||
|
*) err 'nothing answered at that address'
|
||||||
|
say 'start the server and run: claude-mode setup lmstudio'
|
||||||
|
return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_setup() {
|
||||||
|
local mode="${1:-}" name pf
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
anthropic)
|
||||||
|
head_ 'setup: anthropic'
|
||||||
|
ok 'nothing to configure - it uses your existing Claude login'
|
||||||
|
return 0 ;;
|
||||||
|
openrouter|zai|lmstudio) ;;
|
||||||
|
z.ai|z-ai) mode=zai ;;
|
||||||
|
'') err 'usage: claude-mode setup <mode>'; return 1 ;;
|
||||||
|
*) err "unknown mode '$mode'"; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
name="$(resolve_preset "$mode" "${2:-}")" || return 1
|
||||||
|
pf="$(preset_path "$name")"
|
||||||
|
|
||||||
|
if ! ui_interactive; then
|
||||||
|
err 'setup needs an interactive terminal'
|
||||||
|
say "run: claude-mode setup $mode"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
head_ "setup: $mode / preset '$name'"
|
||||||
|
say "$(mode_label "$mode")"
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
openrouter)
|
||||||
|
printf '\n'
|
||||||
|
setup_key openrouter 'OpenRouter'
|
||||||
|
setup_models "$pf" openrouter "$name"
|
||||||
|
;;
|
||||||
|
zai)
|
||||||
|
printf '\n'
|
||||||
|
say 'get a key from https://z.ai/manage-apikey/apikey-list'
|
||||||
|
setup_key zai 'Z.AI'
|
||||||
|
setup_models "$pf" zai "$name"
|
||||||
|
;;
|
||||||
|
lmstudio)
|
||||||
|
setup_lmstudio_server "$pf" "$name" || return 1
|
||||||
|
setup_models "$pf" lmstudio "$name"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mark_configured "$pf"
|
||||||
|
printf '\n'
|
||||||
|
ok "$mode is set up"
|
||||||
|
say "switch to it with: claude-mode $mode"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Dispatch
|
# Dispatch
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1836,6 +2087,27 @@ case "$cmd" in
|
|||||||
doctor) cmd_doctor ;;
|
doctor) cmd_doctor ;;
|
||||||
health) write_health "$(state_mode)" "$(state_preset)" ;;
|
health) write_health "$(state_mode)" "$(state_preset)" ;;
|
||||||
preflight) cmd_preflight "${1:-}" "${2:-}" ;;
|
preflight) cmd_preflight "${1:-}" "${2:-}" ;;
|
||||||
|
setup)
|
||||||
|
# --terminal for callers with no stdin to offer. The bar widget
|
||||||
|
# cannot host a hidden key prompt or a filter-select list, so it
|
||||||
|
# asks a terminal to host the whole flow instead.
|
||||||
|
_mode="${1:-}"; _term=0; _rest=''
|
||||||
|
for _a in "$@"; do
|
||||||
|
case "$_a" in
|
||||||
|
--terminal) _term=1 ;;
|
||||||
|
"$_mode") ;;
|
||||||
|
*) _rest="$_a" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "$_term" -eq 1 ]; then
|
||||||
|
_t="$(cm_terminal_cmd)"
|
||||||
|
setsid nohup "$_t" -e bash -lc \
|
||||||
|
"'$0' setup '$_mode' $_rest; printf '\n press enter to close '; read -r _" \
|
||||||
|
>/dev/null 2>&1 &
|
||||||
|
ok "opened $_t to set up $_mode"
|
||||||
|
else
|
||||||
|
cmd_setup "$_mode" "$_rest"
|
||||||
|
fi ;;
|
||||||
sessions) cmd_sessions "$@" ;;
|
sessions) cmd_sessions "$@" ;;
|
||||||
repair)
|
repair)
|
||||||
scope=''
|
scope=''
|
||||||
|
|||||||
@@ -579,6 +579,30 @@ def cmd_set_auth(argv):
|
|||||||
save(path, p)
|
save(path, p)
|
||||||
print(json.dumps(p["auth"]))
|
print(json.dumps(p["auth"]))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set_flag(argv):
|
||||||
|
"""set-flag <preset> <key> true|false
|
||||||
|
|
||||||
|
Only used for `configured` so far. Kept generic because a preset-level
|
||||||
|
boolean written by hand is exactly the kind of thing that ends up as a
|
||||||
|
string "false", which is truthy everywhere that matters.
|
||||||
|
"""
|
||||||
|
path, key, val = argv[0], argv[1], argv[2]
|
||||||
|
p = load(path)
|
||||||
|
p[key] = (val == "true")
|
||||||
|
save(path, p)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set_all(argv):
|
||||||
|
"""set-all <preset> <model-id> - point every tier and the subagent at one id."""
|
||||||
|
path, model = argv[0], argv[1]
|
||||||
|
p = load(path)
|
||||||
|
p["models"] = {t: model for t in TIERS}
|
||||||
|
if p.get("subagentModel") and p["subagentModel"] != "inherit":
|
||||||
|
p["subagentModel"] = model
|
||||||
|
save(path, p)
|
||||||
|
print(model)
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
"health": cmd_health,
|
"health": cmd_health,
|
||||||
"preflight-json": cmd_preflight_json,
|
"preflight-json": cmd_preflight_json,
|
||||||
@@ -592,6 +616,8 @@ COMMANDS = {
|
|||||||
"models": cmd_models,
|
"models": cmd_models,
|
||||||
"set-tier": cmd_set_tier,
|
"set-tier": cmd_set_tier,
|
||||||
"set-url": cmd_set_url,
|
"set-url": cmd_set_url,
|
||||||
|
"set-flag": cmd_set_flag,
|
||||||
|
"set-all": cmd_set_all,
|
||||||
"set-auth": cmd_set_auth,
|
"set-auth": cmd_set_auth,
|
||||||
"scaffold": cmd_scaffold,
|
"scaffold": cmd_scaffold,
|
||||||
"get": cmd_get,
|
"get": cmd_get,
|
||||||
|
|||||||
@@ -279,6 +279,17 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First-run setup asks for a key behind a hidden prompt and picks models from
|
||||||
|
// a filter-select list - neither of which a bar popup can host. The terminal
|
||||||
|
// gets the whole flow; this side just launches it and re-checks afterwards.
|
||||||
|
function runSetup() {
|
||||||
|
if (root.pendingMode === "") return
|
||||||
|
setupProc.command = root.cli(["setup", root.pendingMode, "--terminal"])
|
||||||
|
setupProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process { id: setupProc; running: false }
|
||||||
|
|
||||||
function retryPending() {
|
function retryPending() {
|
||||||
if (root.pendingMode === "") { root.resetFlow(); return }
|
if (root.pendingMode === "") { root.resetFlow(); return }
|
||||||
var m = root.pendingMode, p = root.pendingPreset
|
var m = root.pendingMode, p = root.pendingPreset
|
||||||
@@ -688,6 +699,12 @@ Panel {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Style.space(7)
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton {
|
||||||
|
label: "Set up " + Modes.title(root.pendingMode) + "…"
|
||||||
|
primary: true
|
||||||
|
visible: root.blocker && String(root.blocker.remedyKind) === "setup"
|
||||||
|
onTriggered: root.runSetup()
|
||||||
|
}
|
||||||
PillButton {
|
PillButton {
|
||||||
label: "Store the key…"
|
label: "Store the key…"
|
||||||
primary: true
|
primary: true
|
||||||
@@ -698,10 +715,11 @@ Panel {
|
|||||||
label: "Server settings…"
|
label: "Server settings…"
|
||||||
primary: root.blocker && ["start-server", "set-url", "needs-key"].indexOf(String(root.blocker.remedyKind)) >= 0
|
primary: root.blocker && ["start-server", "set-url", "needs-key"].indexOf(String(root.blocker.remedyKind)) >= 0
|
||||||
visible: root.pendingMode === "lmstudio"
|
visible: root.pendingMode === "lmstudio"
|
||||||
|
&& !(root.blocker && String(root.blocker.remedyKind) === "setup")
|
||||||
onTriggered: root.openServerSettings(root.pendingPreset)
|
onTriggered: root.openServerSettings(root.pendingPreset)
|
||||||
}
|
}
|
||||||
PillButton {
|
PillButton {
|
||||||
label: root.blocker && String(root.blocker.remedyKind) === "start-server"
|
label: root.blocker && ["start-server", "setup"].indexOf(String(root.blocker.remedyKind)) >= 0
|
||||||
? "Check again" : "Try again"
|
? "Check again" : "Try again"
|
||||||
primary: root.blocker && String(root.blocker.remedyKind) === "start-server"
|
primary: root.blocker && String(root.blocker.remedyKind) === "start-server"
|
||||||
onTriggered: root.retryPending()
|
onTriggered: root.retryPending()
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"provider": "openrouter",
|
|
||||||
"description": "Minimum cost. Everything on flash-class models with 1M context.",
|
|
||||||
"baseUrl": "https://openrouter.ai/api",
|
|
||||||
"auth": {
|
|
||||||
"mode": "vault",
|
|
||||||
"keyRef": "openrouter"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"opus": "deepseek/deepseek-v4-pro",
|
|
||||||
"sonnet": "deepseek/deepseek-v4-flash-0731",
|
|
||||||
"haiku": "qwen/qwen3.7-flash",
|
|
||||||
"fable": "openai/gpt-5.6-luna-pro"
|
|
||||||
},
|
|
||||||
"subagentModel": "qwen/qwen3.7-flash",
|
|
||||||
"gatewayModelDiscovery": true,
|
|
||||||
"contextTokens": 1000000
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"provider": "openrouter",
|
"provider": "openrouter",
|
||||||
"description": "Daily driver. Cheap flash models on the hot tiers, GLM on fable.",
|
"description": "Daily driver. Flash-class models on the hot tiers, GLM on fable.",
|
||||||
"baseUrl": "https://openrouter.ai/api",
|
"baseUrl": "https://openrouter.ai/api",
|
||||||
"auth": {
|
"auth": {
|
||||||
"mode": "vault",
|
"mode": "vault",
|
||||||
"keyRef": "openrouter"
|
"keyRef": "openrouter"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"opus": "deepseek/deepseek-v4-flash-0731",
|
"opus": "z-ai/glm-5.3-flash",
|
||||||
"sonnet": "qwen/qwen3.7-flash",
|
"sonnet": "deepseek/deepseek-v4-flash-0731",
|
||||||
"haiku": "openrouter/free",
|
"haiku": "openrouter/free",
|
||||||
"fable": "z-ai/glm-5.3"
|
"fable": "z-ai/glm-5.3"
|
||||||
},
|
},
|
||||||
"subagentModel": "inherit",
|
"subagentModel": "inherit",
|
||||||
"gatewayModelDiscovery": true,
|
"gatewayModelDiscovery": true,
|
||||||
"contextTokens": 1000000
|
"contextTokens": 1000000,
|
||||||
|
"configured": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"provider": "lmstudio",
|
|
||||||
"description": "Local Qwen3.6-35B-A3B uncensored (heretic, MTP-preserved). Clean chat template - verified working with tools + streaming.",
|
|
||||||
"baseUrl": "http://127.0.0.1:1234",
|
|
||||||
"auth": {
|
|
||||||
"mode": "literal",
|
|
||||||
"token": "lmstudio"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"opus": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
|
||||||
"sonnet": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
|
||||||
"haiku": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
|
||||||
"fable": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved"
|
|
||||||
},
|
|
||||||
"subagentModel": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
|
||||||
"gatewayModelDiscovery": true,
|
|
||||||
"extraEnv": {
|
|
||||||
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
|
||||||
},
|
|
||||||
"contextTokens": 262144
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
{
|
{
|
||||||
"provider": "lmstudio",
|
"provider": "lmstudio",
|
||||||
"description": "Local KAT-Coder v2.5. NOTE: its chat template asserts message order - if you hit 'Unable to generate parser for this template', use the lmstudio-qwen preset.",
|
"description": "Local LM Studio server. Set up picks the model from whatever the server actually has installed.",
|
||||||
"baseUrl": "http://127.0.0.1:1234",
|
"baseUrl": "http://127.0.0.1:1234",
|
||||||
"auth": {
|
"auth": {
|
||||||
"mode": "literal",
|
"mode": "literal",
|
||||||
"token": "lmstudio"
|
"token": "lmstudio"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"opus": "kwaipilot_kat-coder-v2.5-dev",
|
"opus": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
||||||
"sonnet": "kwaipilot_kat-coder-v2.5-dev",
|
"sonnet": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
||||||
"haiku": "kwaipilot_kat-coder-v2.5-dev",
|
"haiku": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
||||||
"fable": "kwaipilot_kat-coder-v2.5-dev"
|
"fable": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved"
|
||||||
},
|
},
|
||||||
"subagentModel": "kwaipilot_kat-coder-v2.5-dev",
|
"subagentModel": "qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved",
|
||||||
"gatewayModelDiscovery": true,
|
"gatewayModelDiscovery": true,
|
||||||
"extraEnv": {
|
"extraEnv": {
|
||||||
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
||||||
},
|
},
|
||||||
"contextTokens": 262144
|
"contextTokens": 262144,
|
||||||
|
"configured": false
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -18,5 +18,6 @@
|
|||||||
"API_TIMEOUT_MS": "3000000",
|
"API_TIMEOUT_MS": "3000000",
|
||||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
||||||
},
|
},
|
||||||
"contextTokens": 1000000
|
"contextTokens": 1000000,
|
||||||
|
"configured": false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user