OpenClaw-Setup/openclaw-setup-max/scripts/model_budget_guard.sh

137 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_PATH="${MODEL_GUARD_CONFIG:-$ROOT_DIR/config/model-budget-guard.config.json}"
if ! command -v jq >/dev/null 2>&1; then
echo "[model-guard] jq is required" >&2
exit 1
fi
if ! command -v openclaw >/dev/null 2>&1; then
echo "[model-guard] openclaw CLI is required" >&2
exit 1
fi
expand_tilde() {
local p="$1"
if [[ "$p" == "~" ]]; then
echo "$HOME"
elif [[ "$p" == "~/"* ]]; then
echo "$HOME/${p#~/}"
else
echo "$p"
fi
}
safe_now_ms() {
python3 - <<'PY'
import time
print(int(time.time()*1000))
PY
}
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "[model-guard] missing config: $CONFIG_PATH" >&2
exit 1
fi
enabled="$(jq -r '.enabled // true' "$CONFIG_PATH")"
if [[ "$enabled" != "true" ]]; then
exit 0
fi
session_key="$(jq -r '.sessionKey // "agent:main:main"' "$CONFIG_PATH")"
low_model="$(jq -r '.lowModel // empty' "$CONFIG_PATH")"
warn_after_min="$(jq -r '.warnAfterMinutes // 2' "$CONFIG_PATH")"
revert_after_min="$(jq -r '.revertAfterMinutes // 45' "$CONFIG_PATH")"
min_warn_interval_min="$(jq -r '.minWarnIntervalMinutes // 20' "$CONFIG_PATH")"
state_file_raw="$(jq -r '.stateFile // "~/.openclaw/model-budget-guard-state.json"' "$CONFIG_PATH")"
state_file="$(expand_tilde "$state_file_raw")"
if [[ -z "$low_model" ]]; then
echo "[model-guard] lowModel must be set" >&2
exit 1
fi
current_model="$(openclaw models status --json 2>/dev/null | jq -r '.defaultModel // empty')"
if [[ -z "$current_model" ]]; then
echo "[model-guard] unable to determine current default model" >&2
exit 0
fi
high_match="$(jq -r --arg m "$current_model" '.highModels // [] | index($m) != null' "$CONFIG_PATH")"
if [[ "$high_match" != "true" ]]; then
# Reset timer when leaving high-cost models.
if [[ -f "$state_file" ]]; then
tmp_file="$state_file.tmp"
jq --arg key "$session_key" 'del(.[$key].highSinceMs)' "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
fi
exit 0
fi
now_ms="$(safe_now_ms)"
warn_after_ms=$((warn_after_min * 60 * 1000))
revert_after_ms=$((revert_after_min * 60 * 1000))
min_warn_interval_ms=$((min_warn_interval_min * 60 * 1000))
mkdir -p "$(dirname "$state_file")"
if [[ ! -f "$state_file" ]]; then
printf '{}\n' > "$state_file"
fi
state_json="$(cat "$state_file")"
high_since_ms="$(jq -r --arg key "$session_key" '.[$key].highSinceMs // 0' <<<"$state_json")"
if ! [[ "$high_since_ms" =~ ^[0-9]+$ ]] || (( high_since_ms <= 0 )); then
high_since_ms="$now_ms"
state_json="$(jq \
--arg key "$session_key" \
--argjson now "$now_ms" \
'.[$key].highSinceMs=$now' \
<<<"$state_json")"
fi
age_ms=$((now_ms - high_since_ms))
if (( age_ms < 0 )); then age_ms=0; fi
last_warn_at="$(jq -r --arg key "$session_key" '.[$key].lastWarnAt // 0' <<<"$state_json")"
last_revert_at="$(jq -r --arg key "$session_key" '.[$key].lastRevertAt // 0' <<<"$state_json")"
should_warn="false"
if (( age_ms >= warn_after_ms )); then
if [[ ! "$last_warn_at" =~ ^[0-9]+$ ]] || (( last_warn_at == 0 )); then
should_warn="true"
elif [[ "$last_warn_at" =~ ^[0-9]+$ ]] && (( now_ms - last_warn_at >= min_warn_interval_ms )); then
should_warn="true"
fi
fi
if [[ "$should_warn" == "true" ]]; then
echo "[model-guard] high-cost model active ($current_model). Consider switching to: $low_model"
state_json="$(jq \
--arg key "$session_key" \
--argjson now "$now_ms" \
'.[$key].lastWarnAt=$now' \
<<<"$state_json")"
fi
if (( age_ms >= revert_after_ms )); then
can_revert="true"
if [[ "$last_revert_at" =~ ^[0-9]+$ ]] && (( now_ms - last_revert_at < min_warn_interval_ms )); then
can_revert="false"
fi
if [[ "$can_revert" == "true" ]]; then
openclaw models set "$low_model" >/dev/null 2>&1 || true
echo "[model-guard] auto-switched default model to $low_model"
state_json="$(jq \
--arg key "$session_key" \
--argjson now "$now_ms" \
'.[$key].lastRevertAt=$now | .[$key].lastWarnAt=$now | .[$key].highSinceMs=$now' \
<<<"$state_json")"
fi
fi
printf '%s\n' "$state_json" > "$state_file"