#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <<'EOF' Usage: switch-models.sh --non-main-primary --non-main-fallbacks [--clear-session-pins] [--pattern ] Example: switch-models.sh \ --non-main-primary "nanogpt/zai-org/glm-5" \ --non-main-fallbacks "lightning_ai/lightning-ai/kimi-k2.5,nanogpt/zai-org/glm-4.7" \ --clear-session-pins \ --pattern "gemini-3-flash" Notes: - Updates agents.list[].model for home/security/research. - Keeps main/default model untouched. - Validates candidates against live /v1/models. - Optionally removes matching per-session model pins. EOF } PRIMARY="" FALLBACKS="" CLEAR_PINS=0 PATTERN="gemini-3-flash" while [[ $# -gt 0 ]]; do case "$1" in --non-main-primary) PRIMARY="${2:-}" shift 2 ;; --non-main-fallbacks) FALLBACKS="${2:-}" shift 2 ;; --clear-session-pins) CLEAR_PINS=1 shift ;; --pattern) PATTERN="${2:-}" shift 2 ;; -h|--help) usage exit 0 ;; *) echo "Unknown arg: $1" >&2 usage >&2 exit 1 ;; esac done if [[ -z "$PRIMARY" || -z "$FALLBACKS" ]]; then echo "ERROR: --non-main-primary and --non-main-fallbacks are required" >&2 exit 1 fi IFS=',' read -ra FB <<< "$FALLBACKS" if [[ ${#FB[@]} -lt 1 ]]; then echo "ERROR: at least 1 fallback required" >&2 exit 1 fi for m in "$PRIMARY" "${FB[@]}"; do m_clean="$(echo "${m#llm-proxy/}" | xargs)" "$SCRIPT_DIR/validate-model.sh" "$m_clean" >/dev/null echo "validated-live: $m_clean" done STATE_FILE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/openclaw.json" if [[ ! -f "$STATE_FILE" ]]; then for alt in \ "${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json" \ "${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json" \ "/opt/openclaw/state/openclaw.json"; do if [[ -f "$alt" ]]; then STATE_FILE="$alt" break fi done fi [[ -f "$STATE_FILE" ]] || { echo "ERROR: openclaw.json not found" >&2; exit 1; } # Validate against local configured catalog too (gateway uses this on restart) node - <<'NODE' "$STATE_FILE" "$PRIMARY" "$FALLBACKS" const fs=require('fs'); const p=process.argv[2]; const primary=process.argv[3].replace(/^llm-proxy\//,''); const fallbacks=process.argv[4].split(',').map(s=>s.trim().replace(/^llm-proxy\//,'')).filter(Boolean); const j=JSON.parse(fs.readFileSync(p,'utf8')); const catalog=new Set((j.models?.providers?.['llm-proxy']?.models||[]).map(m=>m.id)); const missing=[]; if(!catalog.has(primary)) missing.push(primary); for (const f of fallbacks) if(!catalog.has(f)) missing.push(f); if (missing.length) { console.error('ERROR: target models missing from local llm-proxy catalog in openclaw.json'); for (const m of [...new Set(missing)]) console.error(' - '+m); process.exit(2); } console.log('validated-local-catalog: ok'); NODE primary_full="llm-proxy/${PRIMARY#llm-proxy/}" raw_fb="${FALLBACKS}" fb_json="$(node - <<'NODE' "$PRIMARY" "$raw_fb" const primary=process.argv[2].replace(/^llm-proxy\//,''); const raw=process.argv[3].split(',').map(s=>s.trim().replace(/^llm-proxy\//,'')).filter(Boolean); const seen=new Set(); const out=[]; for (const item of raw) { if (item===primary) continue; if (seen.has(item)) continue; seen.add(item); out.push(`llm-proxy/${item}`); } process.stdout.write(JSON.stringify(out)); NODE )" for aid in home security research; do cmd1="openclaw config set agents.list[\"$aid\"].model.primary $primary_full" echo "$cmd1" eval "$cmd1" cmd2="openclaw config set agents.list[\"$aid\"].model.fallbacks '$fb_json' --json" echo "$cmd2" eval "$cmd2" done if [[ "$CLEAR_PINS" -eq 1 ]]; then echo "clearing matching session model pins pattern=$PATTERN" for aid in home security research; do sess="/home/node/.openclaw/agents/${aid}/sessions/sessions.json" [[ -f "$sess" ]] || continue node - <<'NODE' "$sess" "$PATTERN" "$aid" const fs=require('fs'); const file=process.argv[2]; const pattern=new RegExp(process.argv[3],'i'); const aid=process.argv[4]; const j=JSON.parse(fs.readFileSync(file,'utf8')); let removed=0; for (const [k,v] of Object.entries(j)) { const model=(v&&v.model)?String(v.model):''; if (model && pattern.test(model)) { delete v.model; removed++; } } fs.writeFileSync(file, JSON.stringify(j,null,2)+'\n'); console.log(`agent=${aid} removed_model_pins=${removed}`); NODE done fi echo "running post-change audit..." "$SCRIPT_DIR/audit-model-state.sh" "$PATTERN" echo "done. restart gateway to apply runtime changes."