137 lines
4.1 KiB
Bash
Executable File
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"
|