Providers as data; add Ollama and Custom endpoints
Every gateway provider is now an entry in providers.json - endpoint and auth template, how its model list is read, how it is probed before a switch, what setup asks, which doctor checks apply, and its title, colour and logo - installed next to the presets and read by all three consumers: the bash CLI (through cm-json.py), the Windows script, and the bar widget (through health.json). anthropic stays built in; it is the native login, not a gateway. Behaviour that differs in kind stays in code, chosen by name from the entry: catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules (always, lenient, local), and named doctor checks. A provider that reuses them is an entry and a default preset, with no code. The widget draws providers from health.json, so a new one needs no QML change and no shell restart. The existing three are unchanged in behaviour: their blank presets come out byte-identical from the file, and setup, doctor, models and the picker run the same checks through the generic paths. Ollama: local server on :11434, placeholder token, one model for every tier, models from /api/tags. doctor reads the context each loaded model actually runs with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096 tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A bare model name matches its :latest tag. Custom: any Anthropic-compatible endpoint. Ships with no address and is refused until it has one; key optional; models from /v1/models when the endpoint has a list, and a lenient probe so a proxy without one is not blocked. Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no server address; the server form, setup and set-auth use each provider's own default URL, key name and placeholder token instead of LM Studio's; the Windows build gains the no-models and no-address guards it never had. Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a USERPROFILE sandbox on winbox. 1.12.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+352
-202
@@ -32,7 +32,9 @@ JSON="$CM_BIN/cm-json.py"
|
||||
CM_FORCE=0
|
||||
CM_SAME_ENDPOINT=0 # set by reapply_if_active for a tier-only edit
|
||||
|
||||
MODES=(anthropic openrouter zai lmstudio)
|
||||
# anthropic is built in; every gateway provider is appended from providers.json
|
||||
# at startup (see cm_providers_load).
|
||||
MODES=(anthropic)
|
||||
TIERS=(opus sonnet haiku fable)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -219,22 +221,21 @@ err() { printf ' %sFAIL%s %s\n' "$C_BOLD$C_RED" "$C_RESET" "$*" >&2; }
|
||||
head_() { printf '\n%s%s%s\n' "$C_BOLD$C_CYAN" "$*" "$C_RESET"; }
|
||||
|
||||
mode_color() {
|
||||
case "$1" in
|
||||
anthropic) printf '%s' "$C_MAGENTA" ;;
|
||||
openrouter) printf '%s' "$C_CYAN" ;;
|
||||
zai) printf '%s' "$C_GREEN" ;;
|
||||
lmstudio) printf '%s' "$C_YELLOW" ;;
|
||||
*) printf '%s' "$C_GRAY" ;;
|
||||
[ "$1" = anthropic ] && { printf '%s' "$C_MAGENTA"; return; }
|
||||
case "$(prov_field "$1" 5)" in
|
||||
cyan) printf '%s' "$C_CYAN" ;;
|
||||
green) printf '%s' "$C_GREEN" ;;
|
||||
yellow) printf '%s' "$C_YELLOW" ;;
|
||||
magenta) printf '%s' "$C_MAGENTA" ;;
|
||||
white) printf '%s' "$C_WHITE" ;;
|
||||
dkcyan) printf '%s' "$C_DKCYAN" ;;
|
||||
*) printf '%s' "$C_GRAY" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
mode_label() {
|
||||
case "$1" in
|
||||
anthropic) printf 'Anthropic - your subscription login, no gateway' ;;
|
||||
openrouter) printf 'OpenRouter - remote, pay-per-token, any vendor' ;;
|
||||
zai) printf 'Z.AI - GLM coding plan' ;;
|
||||
lmstudio) printf 'LM Studio - local server, offline, free' ;;
|
||||
esac
|
||||
[ "$1" = anthropic ] && { printf 'Anthropic - your subscription login, no gateway'; return; }
|
||||
prov_field "$1" 4
|
||||
}
|
||||
|
||||
# Pure ASCII on purpose - renders identically in every terminal and locale.
|
||||
@@ -254,15 +255,20 @@ show_banner() {
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
||||
claude-mode - switch Claude Code between Anthropic and gateway providers
|
||||
|
||||
claude-mode interactive menu
|
||||
claude-mode status active mode, preset, model map
|
||||
|
||||
claude-mode anthropic native login (clears all gateway config)
|
||||
claude-mode openrouter [preset] remote gateway (default: default)
|
||||
claude-mode zai [preset] Z.AI GLM coding plan (default: zai)
|
||||
claude-mode lmstudio [preset] local LM Studio (default: lmstudio)
|
||||
EOF
|
||||
# One line per provider in providers.json, so a new one documents itself.
|
||||
local id
|
||||
for id in $(provider_ids); do
|
||||
printf ' claude-mode %-26s %s (default: %s)\n' "$id [preset]" \
|
||||
"$(prov_field "$id" 4 | sed 's/^[^-]*- //')" "$(default_preset_for "$id")"
|
||||
done
|
||||
cat <<'EOF'
|
||||
|
||||
claude-mode presets list presets
|
||||
claude-mode preset show <name>
|
||||
@@ -350,6 +356,66 @@ tsv_find() {
|
||||
|
||||
tsv_field() { printf '%s' "$1" | cut -f"$2"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Providers
|
||||
#
|
||||
# Every gateway provider is an entry in providers.json; this script only knows
|
||||
# the *kinds* of behaviour (how a catalogue is fetched, how a server is probed)
|
||||
# and picks one by name from the entry. The table is read once, in the main
|
||||
# shell before dispatch - read inside a $( ) it would be re-read on every
|
||||
# lookup. Columns are cm-json.py's PROVIDER_TSV:
|
||||
#
|
||||
# 1 id 2 aliases 3 title 4 label 5 color 6 defaultPreset
|
||||
# 7 serverEditable 8 probe 9 probePaths 10 catalogueKind 11 perServer
|
||||
# 12 setupKey 13 setupModels 14 keyUrl 15 guardrail 16 doctor
|
||||
# 17 defaultKeyRef 18 literalToken 19 defaultBaseUrl 20 serverHint
|
||||
# 21 serverStart
|
||||
#
|
||||
# Always read with cut, never `IFS=$'\t' read`: tab counts as whitespace to
|
||||
# read, so an empty column (most providers have no aliases) would vanish and
|
||||
# shift every column after it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CM_PROVIDERS_TSV=''
|
||||
|
||||
cm_providers_load() {
|
||||
CM_PROVIDERS_TSV="$("$PY" "$JSON" provider-tsv)" || {
|
||||
echo "claude-mode: could not read providers.json (next to $CM_BIN)" >&2; exit 1; }
|
||||
local id
|
||||
for id in $(provider_ids); do MODES+=("$id"); done
|
||||
}
|
||||
|
||||
provider_ids() { printf '%s\n' "$CM_PROVIDERS_TSV" | cut -f1; }
|
||||
|
||||
prov_field() {
|
||||
local row
|
||||
row="$(printf '%s\n' "$CM_PROVIDERS_TSV" | tsv_find "$1")" || return 1
|
||||
tsv_field "$row" "$2"
|
||||
}
|
||||
|
||||
# An id or an alias (z.ai, z-ai) to the provider id; non-zero if neither.
|
||||
provider_resolve() {
|
||||
local w line
|
||||
w="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
||||
[ -n "$w" ] || return 1
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
if [ "${line%%$'\t'*}" = "$w" ]; then printf '%s\n' "$w"; return 0; fi
|
||||
case ",$(tsv_field "$line" 2)," in
|
||||
*",$w,"*) printf '%s\n' "${line%%$'\t'*}"; return 0 ;;
|
||||
esac
|
||||
done <<EOF_PROVIDERS
|
||||
$CM_PROVIDERS_TSV
|
||||
EOF_PROVIDERS
|
||||
return 1
|
||||
}
|
||||
|
||||
# Whether a provider's entry lists a named doctor check.
|
||||
doctor_has() {
|
||||
case ",$(prov_field "$1" 16)," in *",$2,"*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
term_cols() {
|
||||
local c=''
|
||||
if command -v tput >/dev/null 2>&1; then c="$(tput cols 2>/dev/null)"; fi
|
||||
@@ -367,13 +433,7 @@ term_cols() {
|
||||
# The existence test is what lets a stale choice degrade instead of break.
|
||||
# cm-json.py's BUILTIN_DEFAULT_PRESET and the widget's Modes.DEFAULT_PRESET
|
||||
# mirror the built-in names.
|
||||
builtin_default_preset() {
|
||||
case "$1" in
|
||||
openrouter) printf 'default' ;;
|
||||
zai) printf 'zai' ;;
|
||||
lmstudio) printf 'lmstudio' ;;
|
||||
esac
|
||||
}
|
||||
builtin_default_preset() { prov_field "$1" 6; }
|
||||
|
||||
default_preset_for() {
|
||||
local chosen
|
||||
@@ -452,11 +512,11 @@ cm_probe_timeout() { cm_url_is_local "$1" && printf '4' || printf '10'; }
|
||||
# The distinction matters because the remedies are opposites: `refused` means go
|
||||
# and start the server, `auth` means the server is fine and the key is not.
|
||||
cm_probe_server() {
|
||||
local base="${1%/}" token="${2:-}" t code ep
|
||||
local base="${1%/}" token="${2:-}" paths="${3:-/api/v0/models,/v1/models}" t code ep
|
||||
command -v curl >/dev/null 2>&1 || { printf 'skip'; return 0; }
|
||||
t="$(cm_probe_timeout "$base")"
|
||||
|
||||
for ep in /api/v0/models /v1/models; do
|
||||
for ep in $(printf '%s' "$paths" | tr ',' ' '); do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time "$t" \
|
||||
${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base$ep" 2>/dev/null)"
|
||||
@@ -521,6 +581,15 @@ cm_preflight() {
|
||||
base="$(jget "$pf" baseUrl)"; CM_PF_BASEURL="$base"
|
||||
auth_mode="$(jget "$pf" auth.mode)"; [ -z "$auth_mode" ] && auth_mode=vault
|
||||
|
||||
# A custom endpoint ships with no address, since there is no sensible one
|
||||
# to guess; switching to it would point every session at nothing.
|
||||
if [ -z "$base" ]; then
|
||||
cm_pf_set 'no-url' "Preset '$preset' has no server address" \
|
||||
"$(prov_field "$mode" 3) needs to know where the server is before anything can be sent to it." \
|
||||
"claude-mode preset url $preset <base-url>" 'set-url'
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$auth_mode" = "vault" ]; then
|
||||
key_ref="$(jget "$pf" auth.keyRef)"; [ -z "$key_ref" ] && key_ref=openrouter
|
||||
CM_PF_KEYREF="$key_ref"
|
||||
@@ -538,17 +607,21 @@ cm_preflight() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# LM Studio is checked wherever it is; other providers only when they are
|
||||
# pointed at this machine. A public gateway that is briefly unreachable is
|
||||
# the network's problem and not worth blocking a config change over.
|
||||
if [ -n "$base" ] && { [ "$mode" = "lmstudio" ] || cm_url_is_local "$base"; }; then
|
||||
# A server provider is checked wherever it is (probe "always", or
|
||||
# "lenient" for a proxy that may list no models); a public gateway only
|
||||
# when it has been pointed at this machine (probe "local"). A public
|
||||
# gateway that is briefly unreachable is the network's problem and not
|
||||
# worth blocking a config change over.
|
||||
local probe_rule title
|
||||
probe_rule="$(prov_field "$mode" 8)"; title="$(prov_field "$mode" 3)"
|
||||
if [ "$probe_rule" = always ] || [ "$probe_rule" = lenient ] || cm_url_is_local "$base"; then
|
||||
local token='' probe where
|
||||
if [ "$auth_mode" = "vault" ]; then
|
||||
token="$(cm_vault_get "$key_ref" 2>/dev/null || true)"
|
||||
else
|
||||
token="$(jget "$pf" auth.token)"
|
||||
fi
|
||||
probe="$(cm_probe_server "$base" "$token")"
|
||||
probe="$(cm_probe_server "$base" "$token" "$(prov_field "$mode" 9)")"
|
||||
cm_url_is_local "$base" && where='on this machine' || where='at that address'
|
||||
|
||||
case "$probe" in
|
||||
@@ -560,13 +633,17 @@ cm_preflight() {
|
||||
"claude-mode set-key $key_ref" 'set-key'
|
||||
else
|
||||
cm_pf_set 'server-auth' 'The server wants an API key' \
|
||||
"$base is running but is refusing an unauthenticated request. This preset is set to send LM Studio's placeholder token, which only works on a server with authentication switched off." \
|
||||
"$base is running but is refusing an unauthenticated request. This preset is set to send $title's placeholder token, which only works on a server with authentication switched off." \
|
||||
"claude-mode preset auth $preset key" 'needs-key'
|
||||
fi
|
||||
return 1 ;;
|
||||
notfound)
|
||||
cm_pf_set 'server-wrong' 'That address answered, but not as LM Studio' \
|
||||
"Something is listening at $base, but neither /api/v0/models nor /v1/models is there. Check the port, or whether a proxy in front of it is rewriting the path." \
|
||||
# A proxy in front of a Messages API often serves no model
|
||||
# list at all; for one of those, something answering is
|
||||
# as much as can be checked.
|
||||
[ "$probe_rule" = lenient ] && return 0
|
||||
cm_pf_set 'server-wrong' "That address answered, but not as $title" \
|
||||
"Something is listening at $base, but $(prov_field "$mode" 9 | sed 's/,/ and /g') is not there. Check the port, or whether a proxy in front of it is rewriting the path." \
|
||||
"claude-mode preset url $preset <base-url>" 'set-url'
|
||||
return 1 ;;
|
||||
*)
|
||||
@@ -582,14 +659,11 @@ cm_preflight() {
|
||||
|
||||
cmd_preflight() {
|
||||
local mode="${1:-}" preset="${2:-}" p
|
||||
case "$mode" in
|
||||
anthropic) ;;
|
||||
openrouter|zai|lmstudio)
|
||||
p="$(resolve_preset "$mode" "$preset" 2>/dev/null)" || p="$preset"
|
||||
preset="$p" ;;
|
||||
z.ai|z-ai) mode=zai; p="$(resolve_preset zai "$preset" 2>/dev/null)" || p="$preset"; preset="$p" ;;
|
||||
*) err "unknown mode '$mode'"; return 1 ;;
|
||||
esac
|
||||
if [ "$mode" != anthropic ]; then
|
||||
mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; }
|
||||
p="$(resolve_preset "$mode" "$preset" 2>/dev/null)" || p="$preset"
|
||||
preset="$p"
|
||||
fi
|
||||
|
||||
if cm_preflight "$mode" "$preset"; then
|
||||
"$PY" "$JSON" preflight-json ok "$mode" "$preset" '' '' '' '' '' '' ''
|
||||
@@ -1046,7 +1120,8 @@ write_health() {
|
||||
# nothing actionable to print for them.
|
||||
show_guardrail_status() {
|
||||
local mode="$1" key="$2" code
|
||||
[ "$mode" = "openrouter" ] || return 0
|
||||
# An OpenRouter feature, flagged per provider rather than by name.
|
||||
[ "$(prov_field "$mode" 15)" = 1 ] || return 0
|
||||
[ -n "$key" ] || return 0
|
||||
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 25 \
|
||||
@@ -1187,26 +1262,41 @@ cm_cache_catalogue() {
|
||||
"$PY" "$JSON" cache-models "$CM_MODELS_CACHE" "$1" "$2" "${3:-}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
or_catalogue() {
|
||||
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)"
|
||||
# One catalogue fetch, for any provider: fetched according to its
|
||||
# catalogue.kind, parsed to TSV, and left in the cache for the panel. Prints
|
||||
# the TSV; the status says whether the fetch itself worked. TSV columns by kind:
|
||||
#
|
||||
# openrouter id ctx $in $out lmstudio id state ctx
|
||||
# ollama id params quant family openai id
|
||||
# static id note (a list kept in providers.json)
|
||||
provider_catalogue() {
|
||||
local pid="$1" base="${2%/}" token="${3:-}" out rc
|
||||
case "$(prov_field "$pid" 10)" in
|
||||
openrouter)
|
||||
out="$(curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null \
|
||||
| "$PY" "$JSON" or-models 2>/dev/null)" ;;
|
||||
lmstudio)
|
||||
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)" ;;
|
||||
ollama)
|
||||
out="$(curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/tags" 2>/dev/null | "$PY" "$JSON" ollama-models 2>/dev/null)" ;;
|
||||
openai)
|
||||
# Both header styles: a proxy in front of Anthropic wants
|
||||
# x-api-key, one in front of anything else wants Bearer.
|
||||
out="$(curl -fsS --max-time 15 ${token:+-H "Authorization: Bearer $token"} \
|
||||
${token:+-H "x-api-key: $token"} -H 'anthropic-version: 2023-06-01' \
|
||||
"$base/v1/models" 2>/dev/null | "$PY" "$JSON" openai-models 2>/dev/null)" ;;
|
||||
static)
|
||||
out="$("$PY" "$JSON" provider-static "$pid" 2>/dev/null)" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
rc=$?
|
||||
cm_cache_catalogue openrouter "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" <<<"$out"
|
||||
cm_cache_catalogue "$pid" "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" "$base" <<<"$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.
|
||||
cm_preset_token() {
|
||||
local pf="$1" am ref
|
||||
@@ -1219,19 +1309,11 @@ cm_preset_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() {
|
||||
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"
|
||||
}
|
||||
# The token passed to provider_catalogue is not optional decoration. A local
|
||||
# server with authentication switched on answers its model list with 401 like
|
||||
# anything else, so without it the catalogue comes back empty and every caller
|
||||
# silently believes the server has no models - on exactly the setups that need
|
||||
# the list most.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
@@ -1325,18 +1407,16 @@ cmd_models() {
|
||||
provider=openrouter # on anthropic, the catalogue worth browsing
|
||||
fi
|
||||
|
||||
local quiet=$(( refresh || as_json ))
|
||||
case "$provider" in
|
||||
lmstudio)
|
||||
[ "$quiet" -eq 1 ] || head_ "models installed in LM Studio at $base"
|
||||
tsv="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"; rc=$? ;;
|
||||
zai)
|
||||
[ "$quiet" -eq 1 ] || head_ 'Z.AI GLM models (from Z.AI docs - no public catalogue endpoint)'
|
||||
tsv="$(zai_catalogue)"; rc=$? ;;
|
||||
*)
|
||||
[ "$quiet" -eq 1 ] || head_ 'fetching https://openrouter.ai/api/v1/models ...'
|
||||
tsv="$(or_catalogue)"; rc=$? ;;
|
||||
esac
|
||||
local quiet=$(( refresh || as_json )) kind title
|
||||
kind="$(prov_field "$provider" 10)"; title="$(prov_field "$provider" 3)"
|
||||
if [ "$quiet" -eq 0 ]; then
|
||||
case "$kind" in
|
||||
static) head_ "$title models (from its docs - no public catalogue endpoint)" ;;
|
||||
openrouter) head_ 'fetching https://openrouter.ai/api/v1/models ...' ;;
|
||||
*) head_ "models on the $title server at $base" ;;
|
||||
esac
|
||||
fi
|
||||
tsv="$(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")"; rc=$?
|
||||
|
||||
if [ "$as_json" -eq 1 ]; then
|
||||
jget "$CM_MODELS_CACHE" "providers.$provider"
|
||||
@@ -1346,6 +1426,10 @@ cmd_models() {
|
||||
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"
|
||||
# Not necessarily a fault: plenty of proxies serve Messages and
|
||||
# nothing else.
|
||||
[ "$(prov_field "$provider" 8)" = lenient ] && \
|
||||
say 'some endpoints serve no model list at all - model ids can still be typed by hand'
|
||||
return 1
|
||||
fi
|
||||
ok "cached $(printf '%s\n' "$tsv" | grep -c .) $provider model(s)"
|
||||
@@ -1357,10 +1441,12 @@ cmd_models() {
|
||||
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" ;;
|
||||
case "$kind" in
|
||||
lmstudio) printf ' %-58s %-11s %s\n' "$id" "$c2" "$c3" ;;
|
||||
static) say "$(printf '%-8s - %s' "$id" "$c2")" ;;
|
||||
ollama) printf ' %-44s %-8s %s\n' "$id" "$c2" "$c3" ;;
|
||||
openrouter) printf ' %-52s %10s $%-8s $%s\n' "$id" "$c2" "$c3" "$c4" ;;
|
||||
*) printf ' %s\n' "$id" ;;
|
||||
esac
|
||||
done <<<"$tsv"
|
||||
}
|
||||
@@ -1399,6 +1485,106 @@ cmd_set_key() {
|
||||
printf '%s' "$secret" | cm_vault_set "$ref" && ok "stored key '$ref' via $(cm_vault_backend_label)"
|
||||
}
|
||||
|
||||
# The preset's model ids against what the provider actually offers. A server
|
||||
# provider that does not answer is a failure; a hosted catalogue that cannot be
|
||||
# fetched is only a warning, since the endpoint may be fine regardless.
|
||||
doctor_catalogue() {
|
||||
local mode="$1" pf="$2" base="$3" kind title cat t id row declared c2 c3
|
||||
kind="$(prov_field "$mode" 10)"; title="$(prov_field "$mode" 3)"
|
||||
declared="$(jget "$pf" contextTokens)"
|
||||
cat="$(provider_catalogue "$mode" "$base" "$(cm_preset_token "$pf")")"
|
||||
if [ -z "$cat" ]; then
|
||||
if [ "$(prov_field "$mode" 7)" = 1 ]; then
|
||||
local start; start="$(prov_field "$mode" 21)"
|
||||
if [ "$(prov_field "$mode" 8)" = lenient ]; then
|
||||
warn "$title at $base lists no models - fine for a proxy, but the ids below cannot be checked"
|
||||
else
|
||||
err "$title not reachable at $base${start:+ - $start}"
|
||||
fi
|
||||
else
|
||||
warn "could not fetch the $title model list"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
[ "$(prov_field "$mode" 7)" = 1 ] && ok "$title reachable at $base ($(printf '%s\n' "$cat" | grep -c .) models)"
|
||||
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
# Ollama lists every model with its tag, and a bare name means
|
||||
# `:latest` - `qwen3-coder` is served as `qwen3-coder:latest`.
|
||||
if [ -z "$row" ] && [ "$kind" = ollama ]; then
|
||||
case "$id" in *:*) ;; *) row="$(printf '%s\n' "$cat" | tsv_find "$id:latest")" || row='' ;; esac
|
||||
fi
|
||||
if [ -z "$row" ]; then
|
||||
# A fixed list is documentation, not the provider's word: an id
|
||||
# missing from it may still be served.
|
||||
if [ "$kind" = static ]; then ok "$(printf '%-6s %s (not in the documented list)' "$t" "$id")"
|
||||
else err "$t model NOT available from $title: $id"; fi
|
||||
continue
|
||||
fi
|
||||
c2="$(tsv_field "$row" 2)"; c3="$(tsv_field "$row" 3)"
|
||||
case "$kind" in
|
||||
openrouter) ok "$(printf '%-6s %s [ctx %s]' "$t" "$id" "$c2")"; doctor_ctx_vs "$t" "$c2" "$declared" ;;
|
||||
lmstudio) ok "$(printf '%-6s %s [%s, ctx %s]' "$t" "$id" "$c2" "$c3")"; doctor_ctx_vs "$t" "$c3" "$declared" ;;
|
||||
ollama) ok "$(printf '%-6s %s [%s %s]' "$t" "$id" "$c2" "$c3")" ;;
|
||||
*) ok "$(printf '%-6s %s' "$t" "$id")" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$declared" ] && ok "declared context window: $declared tokens" \
|
||||
|| warn 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early'
|
||||
}
|
||||
|
||||
doctor_ctx_vs() {
|
||||
local t="$1" ctx="$2" declared="$3"
|
||||
{ [ -n "$declared" ] && [ -n "$ctx" ] && [ "$ctx" -lt "$declared" ]; } 2>/dev/null || return 0
|
||||
if [ "$t" = haiku ]; then
|
||||
warn "$t model has $ctx ctx, below the declared $declared - harmless, haiku runs short background tasks"
|
||||
else
|
||||
warn "$t model has $ctx ctx, below the declared $declared - this tier can overflow"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ollama sets the context window on the server, not per request from Claude
|
||||
# Code: 4096 tokens unless `ollama serve` runs with OLLAMA_CONTEXT_LENGTH, and
|
||||
# anything past it is cut off without an error. contextTokens in a preset only
|
||||
# tells Claude Code what to expect - it cannot change what the server does - so
|
||||
# say what the server is actually running where that can be seen, and what to
|
||||
# set where it cannot.
|
||||
doctor_ollama_context() {
|
||||
local pf="$1" base="$2" declared ids id max live ps seen=0 short=0 t body
|
||||
declared="$(jget "$pf" contextTokens)"; [ -n "$declared" ] || return 0
|
||||
ids="$(for t in "${TIERS[@]}"; do jget "$pf" "models.$t"; done | sort -u | grep . || true)"
|
||||
[ -n "$ids" ] || return 0
|
||||
ps="$(curl -fsS --max-time 5 "$base/api/ps" 2>/dev/null)"
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
body="$("$PY" -c 'import json,sys; print(json.dumps({"model": sys.argv[1]}))' "$id")"
|
||||
max="$(curl -fsS --max-time 8 -X POST -H 'content-type: application/json' -d "$body" \
|
||||
"$base/api/show" 2>/dev/null | "$PY" "$JSON" ollama-ctx show 2>/dev/null)"
|
||||
if [ -n "$max" ] && [ "$max" -lt "$declared" ] 2>/dev/null; then
|
||||
warn "$id supports at most $max tokens, below the declared $declared - lower contextTokens"
|
||||
fi
|
||||
live="$(printf '%s' "$ps" | "$PY" "$JSON" ollama-ctx ps "$id" 2>/dev/null)"
|
||||
[ -n "$live" ] || continue
|
||||
seen=1
|
||||
if [ "$live" -lt "$declared" ] 2>/dev/null; then
|
||||
short=1
|
||||
warn "$id is loaded with a $live-token context, below the declared $declared - requests past it are cut off"
|
||||
else
|
||||
ok "$id is loaded with a $live-token context"
|
||||
fi
|
||||
done <<EOF_IDS
|
||||
$ids
|
||||
EOF_IDS
|
||||
if [ "$seen" -eq 0 ]; then
|
||||
warn "none of these models is loaded, so the server's context window cannot be checked"
|
||||
say "Ollama defaults to 4096 tokens; run it with OLLAMA_CONTEXT_LENGTH=$declared or requests past that are cut off silently"
|
||||
elif [ "$short" -eq 1 ]; then
|
||||
say "restart it with OLLAMA_CONTEXT_LENGTH=$declared (or lower contextTokens to match)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_doctor() {
|
||||
local mode preset pf
|
||||
mode="$(state_mode)"; preset="$(state_preset)"; pf="$(preset_path "$preset")"
|
||||
@@ -1456,7 +1642,7 @@ cmd_doctor() {
|
||||
else err 'apiKeyHelper produced no output'; fi
|
||||
fi
|
||||
|
||||
if [ -n "$key" ] && [ "$mode" = "openrouter" ]; then
|
||||
if [ -n "$key" ] && doctor_has "$mode" openrouter-key; then
|
||||
local kinfo
|
||||
kinfo="$(curl -fsS --max-time 20 -H "Authorization: Bearer $key" https://openrouter.ai/api/v1/key 2>/dev/null)"
|
||||
if [ -n "$kinfo" ]; then
|
||||
@@ -1474,60 +1660,24 @@ else: print(' ok spend %.2f of %.2f limit (%s), %.2f remaining' % (use, lim,
|
||||
|
||||
show_guardrail_status "$mode" "$key"
|
||||
fi
|
||||
if [ -n "$key" ] && [ "$mode" = "zai" ]; then
|
||||
# No key-info endpoint: the cheapest real check is a 1-token
|
||||
# message against the Anthropic-compatible surface itself.
|
||||
if [ -n "$key" ] && doctor_has "$mode" message-check; then
|
||||
if curl -fsS --max-time 45 -X POST "$base/v1/messages" \
|
||||
-H 'content-type: application/json' -H "x-api-key: $key" \
|
||||
-H "authorization: Bearer $key" -H 'anthropic-version: 2023-06-01' \
|
||||
-d "{\"model\":\"$(jget "$pf" models.haiku)\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" >/dev/null 2>&1; then
|
||||
ok "Z.AI endpoint accepted the key ($base/v1/messages)"
|
||||
ok "$(prov_field "$mode" 3) endpoint accepted the key ($base/v1/messages)"
|
||||
else
|
||||
err 'Z.AI request failed'
|
||||
err "$(prov_field "$mode" 3) request failed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ok "inline token '$(jget "$pf" auth.token)' (no secret in settings.json)"
|
||||
fi
|
||||
|
||||
if [ "$mode" = "lmstudio" ]; then
|
||||
local cat; cat="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"
|
||||
if [ -n "$cat" ]; then
|
||||
ok "LM Studio reachable at $base ($(printf '%s\n' "$cat" | wc -l | tr -d ' ') models installed)"
|
||||
local t id row st ctx
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
if [ -z "$row" ]; then err "$t model NOT installed in LM Studio: $id"; continue; fi
|
||||
st="$(tsv_field "$row" 2)"; ctx="$(tsv_field "$row" 3)"
|
||||
ok "$t $id [$st, ctx $ctx]"
|
||||
done
|
||||
else
|
||||
err "LM Studio not reachable at $base - start the server (Developer > Start Server)"
|
||||
fi
|
||||
elif [ "$mode" = "openrouter" ]; then
|
||||
local cat; cat="$(or_catalogue)"
|
||||
if [ -n "$cat" ]; then
|
||||
local t id row ctx declared
|
||||
declared="$(jget "$pf" contextTokens)"
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
if [ -z "$row" ]; then err "$t model NOT available from OpenRouter: $id"; continue; fi
|
||||
ctx="$(tsv_field "$row" 2)"
|
||||
ok "$(printf '%-6s %s [ctx %s]' "$t" "$id" "$ctx")"
|
||||
if [ -n "$declared" ] && [ -n "$ctx" ] && [ "$ctx" -lt "$declared" ] 2>/dev/null; then
|
||||
if [ "$t" = "haiku" ]; then
|
||||
warn "$t model has $ctx ctx, below the declared $declared - harmless, haiku runs short background tasks"
|
||||
else
|
||||
warn "$t model has $ctx ctx, below the declared $declared - this tier can overflow"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
[ -n "$declared" ] && ok "declared context window: $declared tokens" \
|
||||
|| warn 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early'
|
||||
else
|
||||
warn 'could not fetch the OpenRouter catalogue'
|
||||
fi
|
||||
fi
|
||||
doctor_has "$mode" catalogue-models && doctor_catalogue "$mode" "$pf" "$base"
|
||||
doctor_has "$mode" ollama-context && doctor_ollama_context "$pf" "$base"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
@@ -1569,17 +1719,18 @@ cmd_preset() {
|
||||
a="$1"; shift
|
||||
case "$a" in
|
||||
--provider)
|
||||
[ $# -gt 0 ] || { err '--provider needs openrouter, zai or lmstudio'; return 1; }
|
||||
[ $# -gt 0 ] || { err "--provider needs one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
provider="$1"; shift ;;
|
||||
--blank) blank=1 ;;
|
||||
-*) err "unknown option '$a'"; return 1 ;;
|
||||
*) from="$a" ;;
|
||||
esac
|
||||
done
|
||||
case "$provider" in
|
||||
''|openrouter|zai|lmstudio) ;;
|
||||
*) err "unknown provider '$provider' - openrouter, zai or lmstudio"; return 1 ;;
|
||||
esac
|
||||
if [ -n "$provider" ]; then
|
||||
local want="$provider"
|
||||
provider="$(provider_resolve "$want")" || {
|
||||
err "unknown provider '$want' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
fi
|
||||
|
||||
if [ "$blank" -eq 1 ]; then
|
||||
[ -n "$provider" ] || { err '--blank needs --provider to know which template'; return 1; }
|
||||
@@ -1652,7 +1803,7 @@ cmd_preset() {
|
||||
local prov="$name" target="${3:-}" p cur got
|
||||
if [ -z "$prov" ]; then
|
||||
head_ 'default preset per provider'
|
||||
for p in openrouter zai lmstudio; do
|
||||
for p in $(provider_ids); do
|
||||
cur="$(jget "$CM_DEFAULTS" "$p")"
|
||||
if [ -n "$cur" ] && [ -f "$(preset_path "$cur")" ]; then
|
||||
printf ' %-11s %s (chosen)\n' "$p" "$cur"
|
||||
@@ -1671,10 +1822,8 @@ cmd_preset() {
|
||||
printf '\n %sclaude-mode preset default <provider> <name> (or --clear)%s\n' "$C_DIM" "$C_RESET"
|
||||
return 0
|
||||
fi
|
||||
case "$prov" in
|
||||
openrouter|zai|lmstudio) ;;
|
||||
*) err "unknown provider '$prov' - openrouter, zai or lmstudio"; return 1 ;;
|
||||
esac
|
||||
prov="$(provider_resolve "$name")" || {
|
||||
err "unknown provider '$name' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
if [ -z "$target" ]; then
|
||||
resolve_preset "$prov" && printf '\n'
|
||||
return
|
||||
@@ -1722,9 +1871,12 @@ cmd_preset() {
|
||||
reapply_if_active "$name"
|
||||
;;
|
||||
auth)
|
||||
local amode="${3:-}" ref="${4:-lmstudio}"
|
||||
local amode="${3:-}" ref="${4:-}"
|
||||
[ -n "$name" ] && [ -n "$amode" ] || { err 'usage: claude-mode preset auth <name> none|key [keyRef]'; return 1; }
|
||||
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
||||
# The provider's own slot name by default: lmstudio, ollama, custom.
|
||||
[ -n "$ref" ] || ref="$(prov_field "$(jget "$(preset_path "$name")" provider)" 17)"
|
||||
[ -n "$ref" ] || ref=lmstudio
|
||||
"$PY" "$JSON" set-auth "$(preset_path "$name")" "$amode" "$ref" >/dev/null || return 1
|
||||
if [ "$amode" = "key" ]; then
|
||||
ok "$name : auth -> vault key '$ref'"
|
||||
@@ -2012,27 +2164,18 @@ ui_pick_model() {
|
||||
ui_reset_items
|
||||
ui_add_item '<type an id manually>' 'enter any model id by hand'
|
||||
|
||||
local ids=() id ctx a b st
|
||||
case "$provider" in
|
||||
openrouter)
|
||||
while IFS=$'\t' read -r id ctx a b; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "context $ctx \$$a in / \$$b out per 1M"
|
||||
done < <(or_catalogue)
|
||||
;;
|
||||
lmstudio)
|
||||
while IFS=$'\t' read -r id st ctx; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "state: $st max context: $ctx"
|
||||
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||
;;
|
||||
zai)
|
||||
while IFS=$'\t' read -r id a; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "$a"
|
||||
done < <(zai_catalogue)
|
||||
;;
|
||||
esac
|
||||
local ids=() id ctx a b st kind
|
||||
kind="$(prov_field "$provider" 10)"
|
||||
while IFS=$'\t' read -r id a b st; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id")
|
||||
case "$kind" in
|
||||
openrouter) ui_add_item "$id" "context $a \$$b in / \$$st out per 1M" ;;
|
||||
lmstudio) ui_add_item "$id" "state: $a max context: $b" ;;
|
||||
ollama) ui_add_item "$id" "$a $b" ;;
|
||||
*) ui_add_item "$id" "$a" ;;
|
||||
esac
|
||||
done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")
|
||||
|
||||
if [ "${#ids[@]}" -gt 0 ]; then
|
||||
ui_filter_select "model for '$tier'" "current: $current" || return 1
|
||||
@@ -2083,7 +2226,7 @@ ui_edit_preset() {
|
||||
|
||||
ui_new_preset() {
|
||||
ui_reset_items
|
||||
local provs=(openrouter zai lmstudio) p
|
||||
local provs=($(provider_ids)) p
|
||||
for p in "${provs[@]}"; do ui_add_item "$p" "$(mode_label "$p")"; done
|
||||
ui_select 'new preset - which provider' '' || return
|
||||
local provider="${provs[$UI_SEL]}"
|
||||
@@ -2254,7 +2397,7 @@ setup_models() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$provider" = "lmstudio" ]; then
|
||||
if [ "$(prov_field "$provider" 13)" = one-for-all ]; 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.
|
||||
@@ -2263,7 +2406,7 @@ setup_models() {
|
||||
while IFS=$'\t' read -r id st ctx; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id")
|
||||
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||
done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")
|
||||
|
||||
if [ "${#ids[@]}" -eq 0 ]; then
|
||||
warn 'the server returned no models; type an id by hand instead'
|
||||
@@ -2292,40 +2435,48 @@ setup_models() {
|
||||
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'
|
||||
# Where a server provider (LM Studio, Ollama, a custom endpoint) lives, and
|
||||
# whether it wants a key. None of them has to be on this machine.
|
||||
setup_server() {
|
||||
local pf="$1" name="$2" mode="$3" url probe token title hint start
|
||||
title="$(prov_field "$mode" 3)"; hint="$(prov_field "$mode" 20)"; start="$(prov_field "$mode" 21)"
|
||||
url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url="$(prov_field "$mode" 19)"
|
||||
|
||||
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.'
|
||||
[ -n "$hint" ] && say "$hint"
|
||||
url="$(ask_value 'server base URL' "$url")"
|
||||
[ -n "$url" ] || { err 'a server address is needed'; return 1; }
|
||||
"$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')"
|
||||
local ref; ref="$(ask_value 'key name to store it under' "$(prov_field "$mode" 17)")"
|
||||
"$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'
|
||||
token="$(prov_field "$mode" 18)"
|
||||
ok "auth -> none (inline placeholder token '$token')"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
say "checking $url ..."
|
||||
probe="$(cm_probe_server "$url" "$token")"
|
||||
probe="$(cm_probe_server "$url" "$token" "$(prov_field "$mode" 9)")"
|
||||
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 ;;
|
||||
notfound)
|
||||
if [ "$(prov_field "$mode" 8)" = lenient ]; then
|
||||
warn "something answered, but it lists no models - fine for a proxy; model ids are typed by hand"
|
||||
else
|
||||
err "something answered there, but not a $title API"; return 1
|
||||
fi ;;
|
||||
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'
|
||||
say "${start:+$start, then }run: claude-mode setup $mode"
|
||||
return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
@@ -2339,10 +2490,8 @@ cmd_setup() {
|
||||
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 ;;
|
||||
*) mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; } ;;
|
||||
esac
|
||||
|
||||
name="$(resolve_preset "$mode" "${2:-}")" || return 1
|
||||
@@ -2357,23 +2506,19 @@ cmd_setup() {
|
||||
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
|
||||
# A server provider is asked where it is and whether it wants a key; a
|
||||
# hosted one always wants a key, and the entry says where to get one.
|
||||
if [ "$(prov_field "$mode" 7)" = 1 ]; then
|
||||
setup_server "$pf" "$name" "$mode" || return 1
|
||||
else
|
||||
local ref url
|
||||
ref="$(jget "$pf" auth.keyRef)"; [ -n "$ref" ] || ref="$(prov_field "$mode" 17)"
|
||||
url="$(prov_field "$mode" 14)"
|
||||
printf '\n'
|
||||
[ -n "$url" ] && say "get a key from $url"
|
||||
setup_key "$ref" "$(prov_field "$mode" 3)"
|
||||
fi
|
||||
setup_models "$pf" "$mode" "$name"
|
||||
|
||||
mark_configured "$pf"
|
||||
printf '\n'
|
||||
@@ -2785,6 +2930,7 @@ else:
|
||||
|
||||
command -v "$PY" >/dev/null 2>&1 || { echo "claude-mode: $PY not found (set CLAUDE_MODE_PYTHON)" >&2; exit 1; }
|
||||
init_root
|
||||
cm_providers_load
|
||||
|
||||
# --force is global and may appear anywhere; strip it before the mode arguments
|
||||
# are positional-matched below.
|
||||
@@ -2803,10 +2949,6 @@ case "$cmd" in
|
||||
''|menu) ui_menu ;;
|
||||
status) cmd_status ;;
|
||||
anthropic) set_mode anthropic ;;
|
||||
openrouter|zai|lmstudio)
|
||||
p="$(resolve_preset "$cmd" "${1:-}")" || exit 1
|
||||
set_mode "$cmd" "$p" ;;
|
||||
z.ai|z-ai) p="$(resolve_preset zai "${1:-}")" || exit 1; set_mode zai "$p" ;;
|
||||
presets) cmd_presets ;;
|
||||
preset) cmd_preset "$@" ;;
|
||||
set-key)
|
||||
@@ -2878,5 +3020,13 @@ case "$cmd" in
|
||||
for a in "$@"; do [ "$a" = "--all" ] && scope=all; done
|
||||
cmd_repair "$scope" ;;
|
||||
help|--help|-h) usage ;;
|
||||
*) err "unknown command '$cmd'"; usage; exit 1 ;;
|
||||
*)
|
||||
# Any provider in providers.json, by id or alias, is a mode.
|
||||
# Last, so a provider can never shadow a real command.
|
||||
if _prov="$(provider_resolve "$cmd")"; then
|
||||
p="$(resolve_preset "$_prov" "${1:-}")" || exit 1
|
||||
set_mode "$_prov" "$p"
|
||||
else
|
||||
err "unknown command '$cmd'"; usage; exit 1
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
+252
-37
@@ -20,6 +20,10 @@ Subcommands:
|
||||
preset-rename <dir> <old> <new> <state> [defaults] rename, repointing state + default
|
||||
auth-of <preset> "mode<TAB>keyRef"; fails if missing
|
||||
set-default <defaults> <provider> [name] choose (or clear) a provider default
|
||||
provider-tsv | provider-resolve <word> | provider-get <id> <path> | provider-static <id>
|
||||
read providers.json
|
||||
ollama-models | openai-models catalogue parsers (JSON on stdin)
|
||||
ollama-ctx show | ps <model> context lengths from Ollama (stdin)
|
||||
"""
|
||||
|
||||
import glob
|
||||
@@ -63,9 +67,162 @@ BASE_MANAGED = [
|
||||
|
||||
TIERS = ["opus", "sonnet", "haiku", "fable"]
|
||||
|
||||
# Mirrors builtin_default_preset() in the CLI: what `claude-mode <provider>`
|
||||
# picks when no preset is named and none has been chosen with `preset default`.
|
||||
BUILTIN_DEFAULT_PRESET = {"openrouter": "default", "zai": "zai", "lmstudio": "lmstudio"}
|
||||
# ---------------------------------------------------------------------------
|
||||
# Providers
|
||||
#
|
||||
# Every gateway provider is described in providers.json - endpoint, auth, how
|
||||
# its model list is fetched, how setup runs, which doctor checks apply, how it
|
||||
# is drawn - so adding one that reuses an existing behaviour is an entry there
|
||||
# rather than code in four places. What differs in *kind* (parsing a catalogue
|
||||
# format, probing a server) stays in code, picked by name from the entry.
|
||||
#
|
||||
# anthropic is not in it: it is the native login, not a gateway.
|
||||
#
|
||||
# The file sits one level above this script in both layouts - the repository
|
||||
# (linux/cm-json.py) and an install (~/.claude-mode/bin/cm-json.py) - so no
|
||||
# path has to be passed around. CM_PROVIDERS overrides it, for tests.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PROVIDERS_PATH = os.environ.get("CM_PROVIDERS") or os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "providers.json")
|
||||
_PROVIDERS = None
|
||||
|
||||
|
||||
def providers():
|
||||
global _PROVIDERS
|
||||
if _PROVIDERS is None:
|
||||
if not os.path.exists(PROVIDERS_PATH):
|
||||
raise SystemExit("providers.json not found at %s - re-run the installer" % PROVIDERS_PATH)
|
||||
data = load(PROVIDERS_PATH, {})
|
||||
items = data.get("providers") if isinstance(data, dict) else None
|
||||
_PROVIDERS = [p for p in (items or []) if isinstance(p, dict) and p.get("id")]
|
||||
return _PROVIDERS
|
||||
|
||||
|
||||
def provider(pid):
|
||||
for p in providers():
|
||||
if p["id"] == pid:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _dig(node, dotted, default=None):
|
||||
for part in dotted.split("."):
|
||||
if isinstance(node, dict) and part in node:
|
||||
node = node[part]
|
||||
else:
|
||||
return default
|
||||
return node
|
||||
|
||||
|
||||
def catalogue_kind(pid):
|
||||
return _dig(provider(pid) or {}, "catalogue.kind", "")
|
||||
|
||||
|
||||
def per_server_catalogue(pid):
|
||||
"""A catalogue that belongs to one server rather than to the provider -
|
||||
two LM Studio presets can point at two machines with different models."""
|
||||
return bool(_dig(provider(pid) or {}, "catalogue.perServer", False))
|
||||
|
||||
|
||||
def builtin_default_preset():
|
||||
"""What `claude-mode <provider>` picks when no preset is named and none has
|
||||
been chosen. Mirrored by builtin_default_preset() in the CLI."""
|
||||
return {p["id"]: p.get("defaultPreset") or p["id"] for p in providers()}
|
||||
|
||||
|
||||
# Column order of `provider-tsv`, which the shell reads once per run and looks
|
||||
# rows up in (bash 3.2 on macOS has no associative arrays). Append only: the
|
||||
# shell addresses these by number.
|
||||
PROVIDER_TSV = [
|
||||
("id", lambda p: p["id"]),
|
||||
("aliases", lambda p: ",".join(p.get("aliases") or [])),
|
||||
("title", lambda p: p.get("title") or p["id"]),
|
||||
("label", lambda p: p.get("label") or p.get("title") or p["id"]),
|
||||
("color", lambda p: p.get("color") or "gray"),
|
||||
("defaultPreset", lambda p: p.get("defaultPreset") or p["id"]),
|
||||
("serverEditable", lambda p: "1" if _dig(p, "server.editable") else "0"),
|
||||
("probe", lambda p: _dig(p, "server.probe", "none")),
|
||||
("probePaths", lambda p: ",".join(_dig(p, "server.paths", []) or [])),
|
||||
("catalogueKind", lambda p: _dig(p, "catalogue.kind", "")),
|
||||
("perServer", lambda p: "1" if _dig(p, "catalogue.perServer") else "0"),
|
||||
("setupKey", lambda p: _dig(p, "setup.key", "required")),
|
||||
("setupModels", lambda p: _dig(p, "setup.models", "per-tier")),
|
||||
("keyUrl", lambda p: _dig(p, "setup.keyUrl", "")),
|
||||
("guardrail", lambda p: "1" if p.get("guardrail") else "0"),
|
||||
("doctor", lambda p: ",".join(p.get("doctor") or [])),
|
||||
("defaultKeyRef", lambda p: _dig(p, "preset.auth.keyRef") or p["id"]),
|
||||
("literalToken", lambda p: _dig(p, "preset.auth.token") or p["id"]),
|
||||
("defaultBaseUrl", lambda p: _dig(p, "preset.baseUrl", "")),
|
||||
("serverHint", lambda p: _dig(p, "server.hint", "")),
|
||||
("serverStart", lambda p: _dig(p, "server.start", "")),
|
||||
]
|
||||
|
||||
|
||||
def cmd_provider_static(argv):
|
||||
"""provider-static <id> - a fixed catalogue from providers.json as id<TAB>note."""
|
||||
for m in _dig(provider(argv[0]) or {}, "catalogue.static", []) or []:
|
||||
if isinstance(m, dict) and m.get("id"):
|
||||
print("%s\t%s" % (m["id"], m.get("note", "")))
|
||||
|
||||
|
||||
def cmd_ollama_ctx(argv):
|
||||
"""ollama-ctx show | ps <model> (the matching Ollama response on stdin)
|
||||
|
||||
show: the model's own maximum, from /api/show's model_info
|
||||
"<architecture>.context_length".
|
||||
ps: the context a loaded model is actually running with, from /api/ps -
|
||||
which is the number that matters, because Ollama sets it on the
|
||||
server and cuts anything longer off without saying so.
|
||||
Prints nothing when the answer is not there to read.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(sys.stdin.read() or "{}")
|
||||
except ValueError:
|
||||
return
|
||||
if argv[0] == "show":
|
||||
info = data.get("model_info") or {}
|
||||
for k, v in info.items():
|
||||
if k.endswith(".context_length") and isinstance(v, int):
|
||||
print(v)
|
||||
return
|
||||
elif argv[0] == "ps" and len(argv) > 1:
|
||||
# A bare name is `:latest` to Ollama, and /api/ps reports the tag.
|
||||
want = {argv[1]} | ({argv[1] + ":latest"} if ":" not in argv[1] else set())
|
||||
for m in data.get("models") or []:
|
||||
if want & {m.get("name"), m.get("model")} and isinstance(m.get("context_length"), int):
|
||||
print(m["context_length"])
|
||||
return
|
||||
|
||||
|
||||
def cmd_provider_tsv(argv):
|
||||
"""provider-tsv - one tab-separated row per provider, columns as PROVIDER_TSV."""
|
||||
for p in providers():
|
||||
print("\t".join(str(fn(p)).replace("\t", " ") for _, fn in PROVIDER_TSV))
|
||||
|
||||
|
||||
def cmd_provider_resolve(argv):
|
||||
"""provider-resolve <word> - the provider id for an id or alias, else exit 1."""
|
||||
word = (argv[0] if argv else "").strip().lower()
|
||||
for p in providers():
|
||||
if word == p["id"] or word in [a.lower() for a in (p.get("aliases") or [])]:
|
||||
print(p["id"])
|
||||
return
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_provider_get(argv):
|
||||
"""provider-get <id> <dotted.path> - one value from a provider entry."""
|
||||
p = provider(argv[0])
|
||||
if p is None:
|
||||
sys.exit(1)
|
||||
val = _dig(p, argv[1])
|
||||
if isinstance(val, (dict, list)):
|
||||
print(json.dumps(val))
|
||||
elif isinstance(val, bool):
|
||||
print("true" if val else "false")
|
||||
elif val is not None:
|
||||
print(val)
|
||||
|
||||
|
||||
def load(path, default=None):
|
||||
@@ -229,31 +386,20 @@ def cmd_set_tier(argv):
|
||||
|
||||
|
||||
def cmd_scaffold(argv):
|
||||
provider = argv[0]
|
||||
base = {"provider": provider, "description": "new preset"}
|
||||
if provider == "openrouter":
|
||||
base["baseUrl"] = "https://openrouter.ai/api"
|
||||
base["auth"] = {"mode": "vault", "keyRef": "openrouter"}
|
||||
elif provider == "zai":
|
||||
base["baseUrl"] = "https://api.z.ai/api/anthropic"
|
||||
base["auth"] = {"mode": "vault", "keyRef": "zai"}
|
||||
elif provider == "lmstudio":
|
||||
base["baseUrl"] = "http://127.0.0.1:1234"
|
||||
base["auth"] = {"mode": "literal", "token": "lmstudio"}
|
||||
else:
|
||||
raise SystemExit("unknown provider '%s'" % provider)
|
||||
|
||||
"""scaffold <provider> - a blank preset built from the provider's template."""
|
||||
p = provider(argv[0])
|
||||
if p is None:
|
||||
raise SystemExit("unknown provider '%s'" % argv[0])
|
||||
tpl = p.get("preset") or {}
|
||||
base = {"provider": p["id"], "description": "new preset"}
|
||||
base["baseUrl"] = tpl.get("baseUrl", "")
|
||||
base["auth"] = dict(tpl.get("auth") or {"mode": "vault", "keyRef": p["id"]})
|
||||
base["models"] = {t: "" for t in TIERS}
|
||||
base["subagentModel"] = "inherit"
|
||||
base["gatewayModelDiscovery"] = provider == "openrouter"
|
||||
base["contextTokens"] = 262144 if provider == "lmstudio" else 1000000
|
||||
if provider == "lmstudio":
|
||||
base["extraEnv"] = {"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"}
|
||||
if provider == "zai":
|
||||
base["extraEnv"] = {
|
||||
"API_TIMEOUT_MS": "3000000",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
||||
}
|
||||
base["gatewayModelDiscovery"] = bool(tpl.get("gatewayModelDiscovery"))
|
||||
base["contextTokens"] = int(tpl.get("contextTokens") or 200000)
|
||||
if tpl.get("extraEnv"):
|
||||
base["extraEnv"] = dict(tpl["extraEnv"])
|
||||
print(json.dumps(base, indent=2))
|
||||
|
||||
|
||||
@@ -416,6 +562,8 @@ def cmd_cache_models(argv):
|
||||
"""
|
||||
path, provider, ok = argv[0], argv[1], argv[2] == "1"
|
||||
base = argv[3].rstrip("/") if len(argv) > 3 else ""
|
||||
kind = catalogue_kind(provider)
|
||||
per_server = per_server_catalogue(provider)
|
||||
|
||||
def num(s, cast):
|
||||
try:
|
||||
@@ -423,6 +571,8 @@ def cmd_cache_models(argv):
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# The TSV columns are whatever that kind's parser prints (or-models,
|
||||
# lms-models, ollama-models, openai-models, or a static list's id/note).
|
||||
rows = []
|
||||
for line in sys.stdin.read().splitlines():
|
||||
parts = line.split("\t")
|
||||
@@ -430,17 +580,20 @@ def cmd_cache_models(argv):
|
||||
if not mid:
|
||||
continue
|
||||
m = {"id": mid}
|
||||
if provider == "openrouter":
|
||||
if kind == "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":
|
||||
elif kind == "lmstudio":
|
||||
state, ctx = (parts[1:] + ["", ""])[:2]
|
||||
m["state"] = state
|
||||
m["contextTokens"] = num(ctx, int)
|
||||
else:
|
||||
m["note"] = parts[1] if len(parts) > 1 else ""
|
||||
elif kind == "ollama":
|
||||
params, quant = (parts[1:] + ["", ""])[:2]
|
||||
m["note"] = " ".join(x for x in (params, quant) if x and x != "-")
|
||||
elif len(parts) > 1 and parts[1]:
|
||||
m["note"] = parts[1]
|
||||
rows.append(m)
|
||||
|
||||
try:
|
||||
@@ -458,11 +611,11 @@ def cmd_cache_models(argv):
|
||||
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
if ok:
|
||||
node = {"fetchedAt": now, "ok": True, "models": rows}
|
||||
if provider == "lmstudio":
|
||||
if per_server:
|
||||
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:
|
||||
# A list from a different server is not stale, it is wrong.
|
||||
if per_server and node.get("baseUrl", "") != base:
|
||||
node = {"baseUrl": base, "models": []}
|
||||
node["ok"] = False
|
||||
node["failedAt"] = now
|
||||
@@ -479,6 +632,35 @@ def cmd_lms_models(argv):
|
||||
print("%s\t%s\t%s" % (m.get("id", ""), m.get("state", "unknown"), m.get("max_context_length", "")))
|
||||
|
||||
|
||||
def cmd_ollama_models(argv):
|
||||
"""Ollama /api/tags on stdin -> 'id<TAB>parameters<TAB>quantization<TAB>family'.
|
||||
|
||||
The id is `name` (e.g. qwen3-coder:30b), which is what the Anthropic
|
||||
endpoint accepts as a model. /api/tags carries no context length - that is
|
||||
a server setting (OLLAMA_CONTEXT_LENGTH), not a property of the model file.
|
||||
"""
|
||||
data = json.loads(sys.stdin.read())
|
||||
for m in sorted(data.get("models") or [], key=lambda x: x.get("name", "")):
|
||||
d = m.get("details") or {}
|
||||
# "-" rather than empty: bash's `read` with a tab IFS merges empty
|
||||
# fields, which would slide the later columns left.
|
||||
print("%s\t%s\t%s\t%s" % (m.get("name") or m.get("model", ""), d.get("parameter_size") or "-",
|
||||
d.get("quantization_level") or "-", d.get("family") or "-"))
|
||||
|
||||
|
||||
def cmd_openai_models(argv):
|
||||
"""A `GET /v1/models` list on stdin -> one id per line.
|
||||
|
||||
Covers both the OpenAI shape and Anthropic's own ({"data": [{"id": ...}]}),
|
||||
which is what a proxy in front of either tends to serve.
|
||||
"""
|
||||
data = json.loads(sys.stdin.read())
|
||||
items = data.get("data") if isinstance(data, dict) else data
|
||||
for m in sorted((items or []), key=lambda x: (x or {}).get("id", "")):
|
||||
if isinstance(m, dict) and m.get("id"):
|
||||
print(m["id"])
|
||||
|
||||
|
||||
# A model id carrying a bracket suffix - e.g. `claude-fable-5[1m]` - is Claude
|
||||
# Code's extended-context marker. It belongs to Anthropic's 1M models and no
|
||||
# gateway recognises it. A session that had it can carry the tag onto a new id
|
||||
@@ -586,7 +768,8 @@ def cmd_health(argv):
|
||||
"schema": 1,
|
||||
"tool": "claude-mode",
|
||||
"version": version or "0.0.0",
|
||||
"updatedAt": __import__("datetime").datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"updatedAt": __import__("datetime").datetime.now(
|
||||
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"os": "posix",
|
||||
"mode": mode,
|
||||
"preset": "" if mode == "anthropic" else preset,
|
||||
@@ -662,7 +845,7 @@ def cmd_health(argv):
|
||||
if not isinstance(chosen_raw, dict):
|
||||
chosen_raw = {}
|
||||
effective, chosen = {}, {}
|
||||
for prov, builtin in BUILTIN_DEFAULT_PRESET.items():
|
||||
for prov, builtin in builtin_default_preset().items():
|
||||
pick = chosen_raw.get(prov)
|
||||
if pick and pick in names:
|
||||
effective[prov] = chosen[prov] = pick
|
||||
@@ -674,6 +857,25 @@ def cmd_health(argv):
|
||||
h["defaultPresetFor"] = effective
|
||||
h["defaultPresetChosen"] = chosen
|
||||
|
||||
# The providers as the widget needs them, in menu order: how to draw each
|
||||
# one and which panel features apply. With this published, a provider
|
||||
# added to providers.json shows up in the bar without touching its QML -
|
||||
# or restarting the shell, which a change to Modes.js would need.
|
||||
h["providers"] = [{
|
||||
"id": p["id"],
|
||||
"title": p.get("title") or p["id"],
|
||||
"blurb": p.get("blurb", ""),
|
||||
"logo": p.get("logo", ""),
|
||||
"logoScale": float(p.get("logoScale") or 1.0),
|
||||
"glyph": p.get("glyph", ""),
|
||||
"serverEditable": bool(_dig(p, "server.editable")),
|
||||
"serverHint": _dig(p, "server.hint", ""),
|
||||
"defaultBaseUrl": _dig(p, "preset.baseUrl", ""),
|
||||
"perServerCatalogue": bool(_dig(p, "catalogue.perServer")),
|
||||
"defaultKeyRef": _dig(p, "preset.auth.keyRef") or p["id"],
|
||||
"keyOptional": _dig(p, "setup.key", "required") != "required",
|
||||
} for p in providers()]
|
||||
|
||||
save(os.path.join(root, "health.json"), h)
|
||||
|
||||
|
||||
@@ -760,10 +962,16 @@ def cmd_set_auth(argv):
|
||||
never sees it either way.
|
||||
"""
|
||||
path, mode = argv[0], argv[1]
|
||||
key_ref = argv[2] if len(argv) > 2 and argv[2] else "lmstudio"
|
||||
p = load(path)
|
||||
# The provider's own names, not LM Studio's: an Ollama preset switched to
|
||||
# "none" sends Ollama's placeholder, and one switched to "key" defaults to
|
||||
# a vault slot named after its provider.
|
||||
pid = p.get("provider", "")
|
||||
key_ref = argv[2] if len(argv) > 2 and argv[2] else (
|
||||
_dig(provider(pid) or {}, "preset.auth.keyRef") or pid or "lmstudio")
|
||||
if mode == "none":
|
||||
p["auth"] = {"mode": "literal", "token": "lmstudio"}
|
||||
token = _dig(provider(pid) or {}, "preset.auth.token") or pid or "lmstudio"
|
||||
p["auth"] = {"mode": "literal", "token": token}
|
||||
elif mode == "key":
|
||||
p["auth"] = {"mode": "vault", "keyRef": key_ref}
|
||||
else:
|
||||
@@ -1279,6 +1487,13 @@ COMMANDS = {
|
||||
"strip-tags": cmd_strip_tags,
|
||||
"or-models": cmd_or_models,
|
||||
"lms-models": cmd_lms_models,
|
||||
"ollama-models": cmd_ollama_models,
|
||||
"openai-models": cmd_openai_models,
|
||||
"provider-tsv": cmd_provider_tsv,
|
||||
"provider-resolve": cmd_provider_resolve,
|
||||
"provider-get": cmd_provider_get,
|
||||
"provider-static": cmd_provider_static,
|
||||
"ollama-ctx": cmd_ollama_ctx,
|
||||
"cache-models": cmd_cache_models,
|
||||
"apply": cmd_apply,
|
||||
"summary": cmd_summary,
|
||||
|
||||
@@ -82,6 +82,20 @@ if [ -f "$VERSION_SRC" ]; then
|
||||
green "version $(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
fi
|
||||
|
||||
# --- providers (shared with the Windows build) -----------------------------
|
||||
# Always replaced, unlike presets: it is the tool's own table of what each
|
||||
# provider is, not something a user edits. cm-json.py looks for it one level
|
||||
# above bin/.
|
||||
PROVIDERS_SRC="$SRC/../providers.json"
|
||||
[ -f "$PROVIDERS_SRC" ] || PROVIDERS_SRC="$SRC/providers.json"
|
||||
if [ -f "$PROVIDERS_SRC" ]; then
|
||||
install -m 0644 "$PROVIDERS_SRC" "$ROOT/providers.json"
|
||||
green "providers: $("$PY" -c 'import json,sys; print(", ".join(p["id"] for p in json.load(open(sys.argv[1]))["providers"]))' "$ROOT/providers.json")"
|
||||
else
|
||||
fail 'providers.json missing from the payload - claude-mode cannot run without it'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- presets (shared with the Windows build) -------------------------------
|
||||
PRESET_SRC="$SRC/../presets"
|
||||
[ -d "$PRESET_SRC" ] || PRESET_SRC="$SRC/presets"
|
||||
|
||||
Reference in New Issue
Block a user