From 3bab4b3b4fd2d5609e04d4642e6ade76e126a653 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 24 Feb 2026 11:44:12 -0600 Subject: [PATCH] Add model hygiene workflow and enforce OpenRouter-only profiles --- openclaw-setup-max/PRD.md | 4 +- openclaw-setup-max/README.md | 21 +- .../config/model-budget-guard.config.json | 3 +- .../config/model-profiles.config.json | 13 +- .../docs/operations/MODEL_SWITCHING.md | 16 +- .../install_model_budget_guard_launchd.sh | 2 - .../install_model_schedule_guard_launchd.sh | 2 - .../scripts/model_hygiene_workflow.sh | 189 ++++++++++++++++++ 8 files changed, 226 insertions(+), 24 deletions(-) create mode 100755 openclaw-setup-max/scripts/model_hygiene_workflow.sh diff --git a/openclaw-setup-max/PRD.md b/openclaw-setup-max/PRD.md index b54b803..55967ae 100644 --- a/openclaw-setup-max/PRD.md +++ b/openclaw-setup-max/PRD.md @@ -153,8 +153,8 @@ Outputs: ## 10) Acceptance Criteria -- AC-1: Running `bash ./scripts/model_profile_switch.sh free` changes default to `ollama/qwen3:14b`. -- AC-2: Running `bash ./scripts/model_profile_switch.sh paid` restores default to `openrouter/moonshotai/kimi-k2.5`. +- AC-1: Running `bash ./scripts/model_profile_switch.sh free` changes default to `openrouter/qwen/qwen3-coder:free`. +- AC-2: Running `bash ./scripts/model_profile_switch.sh paid` restores default to `openrouter/qwen/qwen3-coder:free`. - AC-3: With schedule enabled (`21`/`7`), profile changes correctly by local time window. - AC-4: If operator manually drifts model during scheduled window, schedule guard re-aligns on next run. - AC-5: Budget guard warns and reverts when high model remains active beyond thresholds. diff --git a/openclaw-setup-max/README.md b/openclaw-setup-max/README.md index 55ebad5..a8720fe 100644 --- a/openclaw-setup-max/README.md +++ b/openclaw-setup-max/README.md @@ -1,8 +1,8 @@ # OpenClaw Setup Max (Paid + Free Model Switching) This workspace runs OpenClaw with: -- A paid high-quality model profile (`openrouter/moonshotai/kimi-k2.5`) -- A free local profile (Ollama-only) +- A paid high-quality model profile (`openrouter/qwen/qwen3-coder:free`) +- A free OpenRouter profile - One-command live switching - Optional automatic day/night switching (example: free from 9pm-7am) - Optional budget guard that warns and auto-reverts from expensive models @@ -27,11 +27,11 @@ Everything else is grouped by function: ## 1) What You Get - `paid` profile: - - Primary: `openrouter/moonshotai/kimi-k2.5` - - Fallbacks: `ollama/qwen3:14b`, `ollama/llama3.2:3b` + - Primary: `openrouter/qwen/qwen3-coder:free` + - Fallbacks: `openrouter/qwen/qwen3-coder-next`, `openrouter/qwen/qwen3-14b` - `free` profile: - - Primary: `ollama/qwen3:14b` - - Fallbacks: `ollama/devstral:24b`, `ollama/llama3.2:3b` + - Primary: `openrouter/qwen/qwen3-coder:free` + - Fallbacks: `openrouter/qwen/qwen3-14b` Profiles are defined in `config/model-profiles.config.json`. @@ -41,6 +41,7 @@ Profiles are defined in `config/model-profiles.config.json`. - `config/model-schedule.config.json`: schedule settings (`dayProfile`, `nightProfile`, hours) - `config/model-budget-guard.config.json`: high-cost warning and auto-revert settings - `scripts/model_profile_switch.sh`: manual live switch command +- `scripts/model_hygiene_workflow.sh`: apply profile + prune legacy model/provider/auth config - `scripts/model_schedule_guard.sh`: scheduled switch worker - `scripts/model_budget_guard.sh`: budget guard worker - `scripts/install_model_schedule_guard_launchd.sh`: installs schedule LaunchAgent @@ -83,10 +84,16 @@ PULL_LOCAL_MODELS=false bash ./setup/setup_openclaw_ollama.sh ## 4) Quick Start (Most Common) -Switch to free mode now: +Run the hygiene workflow (recommended after any model changes): ```bash cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/model_hygiene_workflow.sh paid +``` + +Switch to free mode now: + +```bash bash ./scripts/model_profile_switch.sh free ``` diff --git a/openclaw-setup-max/config/model-budget-guard.config.json b/openclaw-setup-max/config/model-budget-guard.config.json index a7a00e2..51e2608 100644 --- a/openclaw-setup-max/config/model-budget-guard.config.json +++ b/openclaw-setup-max/config/model-budget-guard.config.json @@ -1,9 +1,8 @@ { "enabled": true, "sessionKey": "agent:main:main", - "lowModel": "ollama/qwen3:14b", + "lowModel": "openrouter/qwen/qwen3-coder:free", "highModels": [ - "openrouter/moonshotai/kimi-k2.5", "xai/grok-4-fast", "xai/grok-code-fast-1" ], diff --git a/openclaw-setup-max/config/model-profiles.config.json b/openclaw-setup-max/config/model-profiles.config.json index c1ad785..7fd0a3f 100644 --- a/openclaw-setup-max/config/model-profiles.config.json +++ b/openclaw-setup-max/config/model-profiles.config.json @@ -2,18 +2,17 @@ "profiles": { "paid": { "description": "High-quality paid model for deep work", - "primary": "openrouter/moonshotai/kimi-k2.5", + "primary": "openrouter/qwen/qwen3-coder:free", "fallbacks": [ - "ollama/qwen3:14b", - "ollama/llama3.2:3b" + "openrouter/qwen/qwen3-coder-next", + "openrouter/qwen/qwen3-14b" ] }, "free": { - "description": "All-local no-cost model stack", - "primary": "ollama/qwen3:14b", + "description": "OpenRouter free-first stack", + "primary": "openrouter/qwen/qwen3-coder:free", "fallbacks": [ - "ollama/devstral:24b", - "ollama/llama3.2:3b" + "openrouter/qwen/qwen3-14b" ] } } diff --git a/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md b/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md index ab13faa..7ee1194 100644 --- a/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md +++ b/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md @@ -2,8 +2,20 @@ This setup adds two model profiles: -- `paid` -> `openrouter/moonshotai/kimi-k2.5` with local Ollama fallbacks -- `free` -> local Ollama-only stack +- `paid` -> `openrouter/qwen/qwen3-coder:free` with OpenRouter fallbacks +- `free` -> `openrouter/qwen/qwen3-coder:free` with OpenRouter fallback + +## Recommended workflow (prevents legacy drift) + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/model_hygiene_workflow.sh paid +``` + +This command: +- Applies the selected profile +- Restages/restarts guard LaunchAgents +- Prunes legacy models/providers/auth entries not referenced by current config ## On-demand switch (immediate) diff --git a/openclaw-setup-max/scripts/install_model_budget_guard_launchd.sh b/openclaw-setup-max/scripts/install_model_budget_guard_launchd.sh index 0a4d705..189dbcb 100755 --- a/openclaw-setup-max/scripts/install_model_budget_guard_launchd.sh +++ b/openclaw-setup-max/scripts/install_model_budget_guard_launchd.sh @@ -38,8 +38,6 @@ cat > "$PLIST_PATH" < PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - OLLAMA_API_KEY - ollama-local MODEL_GUARD_CONFIG $MODEL_GUARD_CONFIG diff --git a/openclaw-setup-max/scripts/install_model_schedule_guard_launchd.sh b/openclaw-setup-max/scripts/install_model_schedule_guard_launchd.sh index 82021e2..20d83cb 100755 --- a/openclaw-setup-max/scripts/install_model_schedule_guard_launchd.sh +++ b/openclaw-setup-max/scripts/install_model_schedule_guard_launchd.sh @@ -40,8 +40,6 @@ cat > "$PLIST_PATH" < PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - OLLAMA_API_KEY - ollama-local MODEL_SCHEDULE_CONFIG $MODEL_SCHEDULE_CONFIG MODEL_PROFILES_CONFIG diff --git a/openclaw-setup-max/scripts/model_hygiene_workflow.sh b/openclaw-setup-max/scripts/model_hygiene_workflow.sh new file mode 100755 index 0000000..0152f2f --- /dev/null +++ b/openclaw-setup-max/scripts/model_hygiene_workflow.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROFILES_CONFIG="${MODEL_PROFILES_CONFIG:-$ROOT_DIR/config/model-profiles.config.json}" +BUDGET_CONFIG="${MODEL_GUARD_CONFIG:-$ROOT_DIR/config/model-budget-guard.config.json}" +AUTH_STORE="${OPENCLAW_AUTH_STORE:-$HOME/.openclaw/agents/main/agent/auth-profiles.json}" + +if ! command -v jq >/dev/null 2>&1; then + echo "[model-hygiene] jq is required" >&2 + exit 1 +fi +if ! command -v openclaw >/dev/null 2>&1; then + echo "[model-hygiene] openclaw CLI is required" >&2 + exit 1 +fi + +usage() { + cat <<'USAGE' +Usage: + bash ./scripts/model_hygiene_workflow.sh [--no-live] [--no-guards] + +Behavior: + 1) Applies profile via model_profile_switch.sh + 2) Reinstalls guard LaunchAgents (unless --no-guards) + 3) Prunes legacy models/providers/auth not referenced by config + 4) Prints models status + +Examples: + bash ./scripts/model_hygiene_workflow.sh paid + bash ./scripts/model_hygiene_workflow.sh free --no-live +USAGE +} + +in_array() { + local needle="$1" + shift + local item + for item in "$@"; do + if [[ "$item" == "$needle" ]]; then + return 0 + fi + done + return 1 +} + +profile="${1:-}" +if [[ -z "$profile" ]]; then + usage + exit 1 +fi +if [[ "$profile" == "-h" || "$profile" == "--help" ]]; then + usage + exit 0 +fi +shift || true + +apply_live="true" +install_guards="true" +while (( "$#" )); do + case "$1" in + --no-live) apply_live="false" ;; + --no-guards) install_guards="false" ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "[model-hygiene] unknown argument: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +if [[ ! -f "$PROFILES_CONFIG" ]]; then + echo "[model-hygiene] missing config: $PROFILES_CONFIG" >&2 + exit 1 +fi + +profile_exists="$(jq -r --arg p "$profile" '.profiles[$p] != null' "$PROFILES_CONFIG")" +if [[ "$profile_exists" != "true" ]]; then + echo "[model-hygiene] unknown profile '$profile'" >&2 + jq -r '.profiles | keys[]' "$PROFILES_CONFIG" | sed 's/^/ - /' + exit 1 +fi + +echo "[model-hygiene] collecting managed models/providers from config" +keep_models=() +while IFS= read -r model; do + [[ -z "$model" ]] && continue + if ! in_array "$model" "${keep_models[@]-}"; then + keep_models+=("$model") + fi +done < <(jq -r '.profiles | to_entries[] | .value.primary, (.value.fallbacks[]? // empty)' "$PROFILES_CONFIG") + +if [[ -f "$BUDGET_CONFIG" ]]; then + while IFS= read -r model; do + [[ -z "$model" ]] && continue + if ! in_array "$model" "${keep_models[@]-}"; then + keep_models+=("$model") + fi + done < <(jq -r '.lowModel // empty, (.highModels[]? // empty)' "$BUDGET_CONFIG") +fi + +if [[ ${#keep_models[@]} -eq 0 ]]; then + echo "[model-hygiene] no models discovered in config; aborting" >&2 + exit 1 +fi + +keep_providers=() +for model in "${keep_models[@]}"; do + provider="${model%%/*}" + [[ -z "$provider" || "$provider" == "$model" ]] && continue + if ! in_array "$provider" "${keep_providers[@]-}"; then + keep_providers+=("$provider") + fi +done + +echo "[model-hygiene] applying profile '$profile'" +switch_args=("$profile" "--no-status") +if [[ "$apply_live" != "true" ]]; then + switch_args+=("--no-live") +fi +bash "$SCRIPT_DIR/model_profile_switch.sh" "${switch_args[@]}" + +if [[ "$install_guards" == "true" ]]; then + echo "[model-hygiene] reinstalling guard LaunchAgents" + bash "$SCRIPT_DIR/install_local_model_guardrails.sh" >/dev/null +fi + +echo "[model-hygiene] pruning legacy configured models" +current_models_json="$(openclaw config get agents.defaults.models 2>/dev/null || echo '{}')" +while IFS= read -r model; do + [[ -z "$model" ]] && continue + if ! in_array "$model" "${keep_models[@]-}"; then + openclaw config unset "agents.defaults.models[$model]" >/dev/null || true + fi +done < <(jq -r 'keys[]? // empty' <<<"$current_models_json") + +for model in "${keep_models[@]}"; do + openclaw config set --json "agents.defaults.models[$model]" '{}' >/dev/null || true +done + +echo "[model-hygiene] pruning legacy aliases" +while IFS=$'\t' read -r alias target; do + [[ -z "$alias" || -z "$target" ]] && continue + if ! in_array "$target" "${keep_models[@]-}"; then + openclaw models aliases remove "$alias" >/dev/null || true + fi +done < <(openclaw models status --json 2>/dev/null | jq -r '.aliases // {} | to_entries[] | "\(.key)\t\(.value)"') + +echo "[model-hygiene] pruning legacy provider blocks" +providers_json="$(openclaw config get models.providers 2>/dev/null || echo '{}')" +while IFS= read -r provider; do + [[ -z "$provider" ]] && continue + if ! in_array "$provider" "${keep_providers[@]-}"; then + openclaw config unset "models.providers.$provider" >/dev/null || true + fi +done < <(jq -r 'keys[]? // empty' <<<"$providers_json") + +echo "[model-hygiene] pruning legacy auth profiles in openclaw config" +auth_profiles_json="$(openclaw config get auth.profiles 2>/dev/null || echo '{}')" +while IFS=$'\t' read -r profile_id provider; do + [[ -z "$profile_id" || -z "$provider" ]] && continue + if ! in_array "$provider" "${keep_providers[@]-}"; then + openclaw config unset "auth.profiles[$profile_id]" >/dev/null || true + fi +done < <(jq -r 'to_entries[]? | "\(.key)\t\(.value.provider // empty)"' <<<"$auth_profiles_json") + +if [[ -f "$AUTH_STORE" ]]; then + echo "[model-hygiene] pruning legacy auth profiles in auth store" + keep_providers_json="$(printf '%s\n' "${keep_providers[@]-}" | jq -R . | jq -s .)" + tmp_auth="$(mktemp)" + jq --argjson keep "$keep_providers_json" ' + .profiles |= with_entries(select((.value.provider // "") as $p | ($keep | index($p)) != null)) + | .lastGood |= with_entries(select(.key as $p | ($keep | index($p)) != null)) + | .usageStats |= with_entries(select((.key | split(":")[0]) as $p | ($keep | index($p)) != null)) + ' "$AUTH_STORE" > "$tmp_auth" + mv "$tmp_auth" "$AUTH_STORE" +fi + +echo "[model-hygiene] complete" +echo "[model-hygiene] providers kept: ${keep_providers[*]}" +echo "[model-hygiene] models kept: ${keep_models[*]}" +echo "" +openclaw models status