Initial commit: OpenClaw ops workspace
This commit is contained in:
78
skills/model-selector/scripts/audit-model-state.sh
Executable file
78
skills/model-selector/scripts/audit-model-state.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PATTERN="${1:-gemini-3-flash}"
|
||||
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
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "ERROR: openclaw.json not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Config: $STATE_FILE"
|
||||
echo "Match pattern: $PATTERN"
|
||||
|
||||
echo "\n== Config references =="
|
||||
rg -n "$PATTERN" "$STATE_FILE" || true
|
||||
|
||||
echo "\n== Cron payload model/message references =="
|
||||
openclaw cron list --json 2>/dev/null | jq -r --arg pat "$PATTERN" '
|
||||
.jobs[]
|
||||
| {
|
||||
id,
|
||||
name,
|
||||
agentId,
|
||||
payloadModel: (.payload.model // ""),
|
||||
message: (.payload.message // "")
|
||||
}
|
||||
| select((.payloadModel|test($pat;"i")) or (.message|test($pat;"i")))
|
||||
' || true
|
||||
|
||||
echo "\n== Session model pins =="
|
||||
for agent_dir in /home/node/.openclaw/agents/*; do
|
||||
[[ -d "$agent_dir" ]] || continue
|
||||
agent_id="$(basename "$agent_dir")"
|
||||
sess_file="$agent_dir/sessions/sessions.json"
|
||||
[[ -f "$sess_file" ]] || continue
|
||||
jq -r --arg aid "$agent_id" --arg pat "$PATTERN" '
|
||||
to_entries[]
|
||||
| select((.value.model // "" | tostring | test($pat;"i")))
|
||||
| "agent=" + $aid + " session=" + .key + " model=" + (.value.model|tostring)
|
||||
' "$sess_file" || true
|
||||
done
|
||||
|
||||
echo "\n== Invalid llm-proxy refs against local catalog =="
|
||||
node - <<'NODE' "$STATE_FILE"
|
||||
const fs=require('fs');
|
||||
const p=process.argv[2];
|
||||
const j=JSON.parse(fs.readFileSync(p,'utf8'));
|
||||
const catalog=new Set((j.models?.providers?.['llm-proxy']?.models||[]).map(m=>'llm-proxy/'+m.id));
|
||||
const refs=[];
|
||||
const push=(where,val)=>{ if(!val) return; if(Array.isArray(val)){val.forEach((v,i)=>refs.push([`${where}[${i}]`,v]));} else refs.push([where,val]); };
|
||||
push('agents.defaults.model.primary', j.agents?.defaults?.model?.primary);
|
||||
push('agents.defaults.model.fallbacks', j.agents?.defaults?.model?.fallbacks);
|
||||
for (const a of (j.agents?.list||[])) {
|
||||
push(`agents.list[${a.id}].model.primary`, a.model?.primary);
|
||||
push(`agents.list[${a.id}].model.fallbacks`, a.model?.fallbacks);
|
||||
}
|
||||
let bad=0;
|
||||
for (const [where,val] of refs){
|
||||
if (typeof val==='string' && val.startsWith('llm-proxy/') && !catalog.has(val)) {
|
||||
bad++;
|
||||
console.log(`INVALID ${where} -> ${val}`);
|
||||
}
|
||||
}
|
||||
if(!bad) console.log('none');
|
||||
NODE
|
||||
97
skills/model-selector/scripts/clear-session-model-pins.sh
Executable file
97
skills/model-selector/scripts/clear-session-model-pins.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
clear-session-model-pins.sh --agent <agent-id> [--channel <channel-id>] [--sessions-file <path>]
|
||||
|
||||
Examples:
|
||||
clear-session-model-pins.sh --agent home
|
||||
clear-session-model-pins.sh --agent home --channel 1470162839284224184
|
||||
|
||||
Notes:
|
||||
- Removes per-session "model" keys so agent defaults apply again.
|
||||
- By default targets: /home/node/.openclaw/agents/<agent>/sessions/sessions.json
|
||||
EOF
|
||||
}
|
||||
|
||||
AGENT_ID=""
|
||||
CHANNEL_ID=""
|
||||
SESSIONS_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--agent)
|
||||
AGENT_ID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--channel)
|
||||
CHANNEL_ID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sessions-file)
|
||||
SESSIONS_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$AGENT_ID" ]]; then
|
||||
echo "--agent is required" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$SESSIONS_FILE" ]]; then
|
||||
SESSIONS_FILE="/home/node/.openclaw/agents/${AGENT_ID}/sessions/sessions.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SESSIONS_FILE" ]]; then
|
||||
echo "sessions file not found: $SESSIONS_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - <<PY
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(${SESSIONS_FILE@Q})
|
||||
channel = ${CHANNEL_ID@Q}
|
||||
|
||||
with path.open() as f:
|
||||
data = json.load(f)
|
||||
|
||||
removed = 0
|
||||
scanned = 0
|
||||
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, dict):
|
||||
continue
|
||||
scanned += 1
|
||||
|
||||
if channel:
|
||||
if f"channel:{channel}" not in key:
|
||||
continue
|
||||
|
||||
if "model" in value:
|
||||
del value["model"]
|
||||
removed += 1
|
||||
|
||||
with path.open("w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"scanned={scanned}")
|
||||
print(f"removed_model_pins={removed}")
|
||||
print(f"sessions_file={path}")
|
||||
PY
|
||||
74
skills/model-selector/scripts/list-models.sh
Executable file
74
skills/model-selector/scripts/list-models.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# list-models.sh — Query the LLM proxy /v1/models endpoint
|
||||
# Usage:
|
||||
# list-models.sh # List all model IDs (sorted)
|
||||
# list-models.sh --providers # List unique provider names
|
||||
# list-models.sh --json # Raw JSON response
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve proxy URL and API key from environment or defaults
|
||||
PROXY_URL="${LLM_PROXY_URL:-https://llm-proxy.ext.ben.io/v1}"
|
||||
PROXY_KEY="${PROXY_API_KEY:-${LLM_PROXY_API_KEY:-}}"
|
||||
|
||||
if [[ -z "$PROXY_KEY" ]]; then
|
||||
# Try to read from openclaw config
|
||||
for cfg_path in \
|
||||
"${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json" \
|
||||
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
|
||||
if [[ -f "$cfg_path" ]]; then
|
||||
# Extract apiKey from llm-proxy provider config (handles JSON5 comments)
|
||||
key=$(grep -A5 '"llm-proxy"' "$cfg_path" | grep '"apiKey"' | head -1 | sed 's/.*"apiKey"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)
|
||||
if [[ -n "$key" && "$key" != *'${'* ]]; then
|
||||
PROXY_KEY="$key"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$PROXY_KEY" ]]; then
|
||||
echo "ERROR: No API key found. Set PROXY_API_KEY or LLM_PROXY_API_KEY environment variable." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Strip trailing /v1 from PROXY_URL if present, then always append /v1/models
|
||||
# This prevents double /v1/v1/ when LLM_PROXY_URL already includes /v1
|
||||
PROXY_BASE="${PROXY_URL%/v1}"
|
||||
PROXY_BASE="${PROXY_BASE%/}"
|
||||
|
||||
response=$(curl -s -f -H "Authorization: Bearer $PROXY_KEY" "${PROXY_BASE}/v1/models" 2>&1) || {
|
||||
echo "ERROR: Failed to query ${PROXY_BASE}/v1/models" >&2
|
||||
echo "$response" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--providers)
|
||||
echo "$response" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
providers = sorted(set(m['id'].split('/')[0] for m in data.get('data', [])))
|
||||
for p in providers:
|
||||
print(p)
|
||||
"
|
||||
;;
|
||||
--json)
|
||||
echo "$response"
|
||||
;;
|
||||
--count)
|
||||
echo "$response" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(len(data.get('data', [])))
|
||||
"
|
||||
;;
|
||||
*)
|
||||
echo "$response" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
models = sorted(m['id'] for m in data.get('data', []))
|
||||
for m in models:
|
||||
print(m)
|
||||
"
|
||||
;;
|
||||
esac
|
||||
97
skills/model-selector/scripts/show-current.sh
Executable file
97
skills/model-selector/scripts/show-current.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# show-current.sh — Display the current model configuration from openclaw state
|
||||
# Usage: show-current.sh
|
||||
set -euo pipefail
|
||||
|
||||
# Find the state file
|
||||
STATE_FILE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json"
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
# Try alternative locations
|
||||
for alt in \
|
||||
"/opt/openclaw/state/openclaw.json" \
|
||||
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
|
||||
if [[ -f "$alt" ]]; then
|
||||
STATE_FILE="$alt"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "ERROR: Cannot find openclaw.json state file" >&2
|
||||
echo "Searched:" >&2
|
||||
echo " ${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json" >&2
|
||||
echo " /opt/openclaw/state/openclaw.json" >&2
|
||||
echo " ${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📁 Config file: $STATE_FILE"
|
||||
echo ""
|
||||
|
||||
python3 -c "
|
||||
import json, sys, re
|
||||
|
||||
# Read file and strip JSON5 comments for parsing
|
||||
with open('$STATE_FILE', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Strip single-line comments (// ...) but not inside strings
|
||||
lines = content.split('\n')
|
||||
cleaned = []
|
||||
for line in lines:
|
||||
stripped = line.rstrip()
|
||||
s = stripped.lstrip()
|
||||
if s.startswith('//'):
|
||||
continue
|
||||
in_string = False
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(stripped):
|
||||
c = stripped[i]
|
||||
if c == '\"' and (i == 0 or stripped[i-1] != '\\\\'):
|
||||
in_string = not in_string
|
||||
elif c == '/' and i + 1 < len(stripped) and stripped[i+1] == '/' and not in_string:
|
||||
break
|
||||
result.append(c)
|
||||
i += 1
|
||||
cleaned.append(''.join(result))
|
||||
|
||||
# Remove trailing commas (JSON5)
|
||||
json_str = '\n'.join(cleaned)
|
||||
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
|
||||
|
||||
try:
|
||||
cfg = json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
cfg = json.loads(content)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'ERROR: Failed to parse config: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
agents = cfg.get('agents', {})
|
||||
defaults = agents.get('defaults', {})
|
||||
model = defaults.get('model', {})
|
||||
|
||||
if isinstance(model, str):
|
||||
print(f'🎯 Primary: {model}')
|
||||
print(f'⛓️ Fallbacks: (none configured)')
|
||||
else:
|
||||
primary = model.get('primary', '(not set)')
|
||||
fallbacks = model.get('fallbacks', [])
|
||||
print(f'🎯 Primary: {primary}')
|
||||
print(f'⛓️ Fallbacks ({len(fallbacks)}):')
|
||||
for i, fb in enumerate(fallbacks, 1):
|
||||
print(f' {i}. {fb}')
|
||||
|
||||
# Check for per-agent model overrides
|
||||
agent_list = agents.get('list', [])
|
||||
overrides = [(a.get('id', '?'), a.get('model', '')) for a in agent_list if 'model' in a]
|
||||
if overrides:
|
||||
print()
|
||||
print('⚠️ Per-agent model overrides:')
|
||||
for aid, amodel in overrides:
|
||||
print(f' {aid}: {amodel}')
|
||||
" 2>&1
|
||||
166
skills/model-selector/scripts/switch-models.sh
Executable file
166
skills/model-selector/scripts/switch-models.sh
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/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 <catalog-id> --non-main-fallbacks <m1,m2,...> [--clear-session-pins] [--pattern <old-model-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."
|
||||
216
skills/model-selector/scripts/update-model.sh
Executable file
216
skills/model-selector/scripts/update-model.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
# update-model.sh — Update model configuration in openclaw state file
|
||||
# Usage:
|
||||
# update-model.sh --primary <model-id>
|
||||
# update-model.sh --fallbacks <model1,model2,model3>
|
||||
# update-model.sh --primary <model-id> --fallbacks <model1,model2>
|
||||
#
|
||||
# All model IDs are validated against /v1/models before writing.
|
||||
# A backup of the current config is created before any changes.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
PRIMARY=""
|
||||
FALLBACKS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--primary)
|
||||
PRIMARY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--fallbacks)
|
||||
FALLBACKS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: update-model.sh [--primary <model-id>] [--fallbacks <model1,model2,...>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --primary Set the primary model (will be prefixed with llm-proxy/)"
|
||||
echo " --fallbacks Comma-separated list of fallback models (min 2 required)"
|
||||
echo ""
|
||||
echo "All model IDs are validated against /v1/models before writing."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PRIMARY" && -z "$FALLBACKS" ]]; then
|
||||
echo "ERROR: Must specify --primary and/or --fallbacks" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the state file
|
||||
STATE_FILE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json"
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
for alt in \
|
||||
"/opt/openclaw/state/openclaw.json" \
|
||||
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
|
||||
if [[ -f "$alt" ]]; then
|
||||
STATE_FILE="$alt"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "ERROR: Cannot find openclaw.json state file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📁 Config file: $STATE_FILE"
|
||||
|
||||
# Validate all model IDs first
|
||||
echo ""
|
||||
echo "🔍 Validating model IDs against /v1/models..."
|
||||
VALIDATION_FAILED=0
|
||||
|
||||
if [[ -n "$PRIMARY" ]]; then
|
||||
# Strip llm-proxy/ prefix for validation
|
||||
PRIMARY_CLEAN="${PRIMARY#llm-proxy/}"
|
||||
if ! "$SCRIPT_DIR/validate-model.sh" "$PRIMARY_CLEAN" 2>&1; then
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$FALLBACKS" ]]; then
|
||||
IFS=',' read -ra FB_ARRAY <<< "$FALLBACKS"
|
||||
|
||||
if [[ ${#FB_ARRAY[@]} -lt 2 ]]; then
|
||||
echo "❌ ERROR: Minimum 2 fallback models required (got ${#FB_ARRAY[@]})" >&2
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
|
||||
for fb in "${FB_ARRAY[@]}"; do
|
||||
fb_clean="${fb#llm-proxy/}"
|
||||
fb_clean="$(echo "$fb_clean" | xargs)" # trim whitespace
|
||||
if ! "$SCRIPT_DIR/validate-model.sh" "$fb_clean" 2>&1; then
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $VALIDATION_FAILED -ne 0 ]]; then
|
||||
echo ""
|
||||
echo "❌ Validation failed. No changes made." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create backup
|
||||
BACKUP="${STATE_FILE}.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$STATE_FILE" "$BACKUP"
|
||||
echo ""
|
||||
echo "💾 Backup saved: $BACKUP"
|
||||
|
||||
# Apply changes using Python for safe JSON manipulation
|
||||
python3 -c "
|
||||
import json, sys, re
|
||||
|
||||
state_file = '$STATE_FILE'
|
||||
primary = '${PRIMARY}'.strip() or None
|
||||
fallbacks_raw = '${FALLBACKS}'.strip() or None
|
||||
|
||||
# Read and parse (handle JSON5 comments)
|
||||
with open(state_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Strip comments for parsing
|
||||
lines = content.split('\n')
|
||||
cleaned = []
|
||||
for line in lines:
|
||||
s = line.lstrip()
|
||||
if s.startswith('//'):
|
||||
continue
|
||||
in_string = False
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(line):
|
||||
c = line[i]
|
||||
if c == '\"' and (i == 0 or line[i-1] != '\\\\'):
|
||||
in_string = not in_string
|
||||
elif c == '/' and i + 1 < len(line) and line[i+1] == '/' and not in_string:
|
||||
break
|
||||
result.append(c)
|
||||
i += 1
|
||||
cleaned.append(''.join(result))
|
||||
|
||||
# Remove trailing commas before } or ] (JSON5 feature)
|
||||
json_str = '\n'.join(cleaned)
|
||||
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
|
||||
|
||||
try:
|
||||
cfg = json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
cfg = json.loads(content)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'ERROR: Failed to parse config: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Ensure path exists
|
||||
if 'agents' not in cfg:
|
||||
cfg['agents'] = {}
|
||||
if 'defaults' not in cfg['agents']:
|
||||
cfg['agents']['defaults'] = {}
|
||||
if 'model' not in cfg['agents']['defaults']:
|
||||
cfg['agents']['defaults']['model'] = {}
|
||||
|
||||
model = cfg['agents']['defaults']['model']
|
||||
if isinstance(model, str):
|
||||
model = {'primary': model}
|
||||
cfg['agents']['defaults']['model'] = model
|
||||
|
||||
old_primary = model.get('primary', '(none)')
|
||||
old_fallbacks = model.get('fallbacks', [])
|
||||
|
||||
# Apply primary
|
||||
if primary:
|
||||
# Ensure llm-proxy/ prefix
|
||||
if not primary.startswith('llm-proxy/'):
|
||||
primary = f'llm-proxy/{primary}'
|
||||
model['primary'] = primary
|
||||
|
||||
# Apply fallbacks
|
||||
if fallbacks_raw:
|
||||
fbs = [fb.strip() for fb in fallbacks_raw.split(',') if fb.strip()]
|
||||
fbs = [f'llm-proxy/{fb}' if not fb.startswith('llm-proxy/') else fb for fb in fbs]
|
||||
model['fallbacks'] = fbs
|
||||
|
||||
# Remove per-agent model overrides that match the old primary
|
||||
# (they were likely set by the same drift that caused the issue)
|
||||
agent_list = cfg.get('agents', {}).get('list', [])
|
||||
removed_overrides = []
|
||||
for agent in agent_list:
|
||||
if 'model' in agent:
|
||||
removed_overrides.append((agent.get('id', '?'), agent['model']))
|
||||
del agent['model']
|
||||
|
||||
# Write back
|
||||
with open(state_file, 'w') as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
# Print summary
|
||||
print()
|
||||
print('✅ Configuration updated:')
|
||||
print()
|
||||
print(f' Primary: {old_primary} → {model.get(\"primary\", \"(none)\")}')
|
||||
print(f' Fallbacks:')
|
||||
for i, fb in enumerate(model.get('fallbacks', []), 1):
|
||||
old_marker = '' if fb in old_fallbacks else ' (new)'
|
||||
print(f' {i}. {fb}{old_marker}')
|
||||
if removed_overrides:
|
||||
print()
|
||||
print(' 🧹 Cleared per-agent model overrides:')
|
||||
for aid, amodel in removed_overrides:
|
||||
print(f' {aid}: {amodel} → (uses default)')
|
||||
" 2>&1
|
||||
|
||||
echo ""
|
||||
echo "Done. Restart OpenClaw for changes to take effect."
|
||||
39
skills/model-selector/scripts/validate-model.sh
Executable file
39
skills/model-selector/scripts/validate-model.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# validate-model.sh — Validate that a model ID exists in the LLM proxy
|
||||
# Usage: validate-model.sh <model-id>
|
||||
# Exit 0 if valid, 1 if not found
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: validate-model.sh <model-id>" >&2
|
||||
echo "Example: validate-model.sh nanogpt/deepseek-chat" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODEL_ID="$1"
|
||||
|
||||
# Strip llm-proxy/ prefix if present (user might pass the openclaw.json format)
|
||||
MODEL_ID="${MODEL_ID#llm-proxy/}"
|
||||
|
||||
# Get the live model list
|
||||
available=$("$SCRIPT_DIR/list-models.sh" 2>/dev/null) || {
|
||||
echo "ERROR: Could not fetch model list from LLM proxy" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if echo "$available" | grep -qxF "$MODEL_ID"; then
|
||||
echo "✅ Model '$MODEL_ID' is available"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Model '$MODEL_ID' NOT found in /v1/models" >&2
|
||||
# Suggest close matches
|
||||
partial=$(echo "$available" | grep -i "$(echo "$MODEL_ID" | sed 's|.*/||')" | head -5)
|
||||
if [[ -n "$partial" ]]; then
|
||||
echo "" >&2
|
||||
echo "Did you mean one of these?" >&2
|
||||
echo "$partial" | sed 's/^/ /' >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user