#!/usr/bin/env bash # x/freeloader: turnError hook handler for free-model sessions on OpenRouter. # # Intercepts rate_limit and tool_unsupported errors on :free models, walks # through every viable free model once per round, and resubmits the previous # prompt on each switch. When the pool is exhausted, clears the tried list # and exits 1 so normal error handling takes over. # # Configure in an agent .json: # { "hooks": { "turnError": "$OLLIE/x/freeloader $OLLIE_SESSION_ID" } } # # Exit codes: # 0 — error intercepted, switched model and resubmitted # 1 — pool exhausted or error not applicable; normal handling proceeds set -euo pipefail OLLIE="${OLLIE:-$HOME/mnt/ollie}" CFG="${XDG_CONFIG_HOME:-$HOME/.config}/ollie" BLACKLIST="$CFG/freeloader-blacklist" cmd=${0##*/} die() { printf '%s: %s\n' "$cmd" "$*" >&2; exit 1; } [ $# -ne 1 ] && die "usage: freeloader " SID="$1" B="$OLLIE/s/$SID" [ -d "$B" ] || die "session not found: $SID" error_type="${OLLIE_ERROR_TYPE:-}" current_model="${OLLIE_MODEL:-$(grep '^model=' "$B/cfg" 2>/dev/null | cut -d= -f2-)}" # Only handle rate_limit and tool_unsupported. case "$error_type" in rate_limit|tool_unsupported) ;; *) exit 1 ;; esac # Only act on :free models. case "$current_model" in *:free) ;; *) exit 1 ;; esac # For tool_unsupported: permanently blacklist the current model. if [ "$error_type" = "tool_unsupported" ]; then mkdir -p "$CFG" if ! grep -qxF "$current_model" "$BLACKLIST" 2>/dev/null; then printf '%s\n' "$current_model" >> "$BLACKLIST" printf '%s: blacklisted %s (tool_unsupported)\n' "$cmd" "$current_model" >&2 fi fi # Per-session tried list: tracks models attempted in the current round. TRIED="$CFG/freeloader-tried-$SID" mkdir -p "$CFG" # Mark current model as tried. if ! grep -qxF "$current_model" "$TRIED" 2>/dev/null; then printf '%s\n' "$current_model" >> "$TRIED" fi # Build the full sorted :free pool (blacklist excluded). mapfile -t pool < <( cat "$B/models" 2>/dev/null \ | grep ':free' \ | sort \ | { [ -f "$BLACKLIST" ] && grep -vxFf "$BLACKLIST" || cat; } ) # Find first model not yet tried this round. next_model="" for m in "${pool[@]}"; do if ! grep -qxF "$m" "$TRIED" 2>/dev/null; then next_model="$m" break fi done if [ -z "$next_model" ]; then printf '%s: all free models exhausted, giving up\n' "$cmd" >&2 rm -f "$TRIED" exit 1 fi # Switch model and resubmit the previous prompt. printf 'model %s\n' "$next_model" > "$B/ctl" || die "failed to switch model" prev=$(cat "$B/prompt.prev" 2>/dev/null) || die "prompt.prev not available" [ -z "$prev" ] && die "prompt.prev is empty" printf '%s\n' "$prev" > "$B/fifo.in" || die "failed to resubmit prompt" printf '%s: %s → %s\n' "$cmd" "$current_model" "$next_model" >&2 exit 0