diff --git a/agents/coding.json b/agents/coding.json index eda6369..a6cb80c 100644 --- a/agents/coding.json +++ b/agents/coding.json @@ -29,6 +29,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/default.json b/agents/default.json index c02c77c..20ee69d 100644 --- a/agents/default.json +++ b/agents/default.json @@ -17,6 +17,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/devops.json b/agents/devops.json index d1e7ba3..664b5e3 100644 --- a/agents/devops.json +++ b/agents/devops.json @@ -19,6 +19,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/qa.json b/agents/qa.json index 299dc9d..91c9800 100644 --- a/agents/qa.json +++ b/agents/qa.json @@ -18,6 +18,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/reviewer.json b/agents/reviewer.json index 0e077ce..d9a410a 100644 --- a/agents/reviewer.json +++ b/agents/reviewer.json @@ -18,6 +18,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/sysarchitect.json b/agents/sysarchitect.json index 8719699..0a304ba 100644 --- a/agents/sysarchitect.json +++ b/agents/sysarchitect.json @@ -18,6 +18,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/agents/writer.json b/agents/writer.json index a089b1b..ebfc822 100644 --- a/agents/writer.json +++ b/agents/writer.json @@ -18,6 +18,9 @@ ], "postTurn": [ "task list 2>/dev/null || true" + ], + "turnError": [ + "$OLLIE/x/freeloader $OLLIE_SESSION_ID 2>/dev/null || true" ] } } diff --git a/core b/core index 2c9f853..78e5416 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 2c9f853a4aa6bce14b5788d97c1987edf35e9aaf +Subproject commit 78e54165500f2b6136d9fc2e7422b7ae984dca71 diff --git a/scripts/x/freeloader b/scripts/x/freeloader new file mode 100755 index 0000000..e33df03 --- /dev/null +++ b/scripts/x/freeloader @@ -0,0 +1,80 @@ +#!/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, switches +# to the next viable free model, and resubmits the previous prompt. +# +# Configure in an agent .json: +# { "hooks": { "turnError": "$OLLIE/x/freeloader $OLLIE_SESSION_ID" } } +# +# The hook runner sets OLLIE_ERROR_TYPE, OLLIE_MODEL, and OLLIE_SESSION_ID +# from the payload before executing this script. +# +# Exit codes: +# 0 — error was intercepted and handled (loop will not retry) +# 1 — error not handled; normal retry/error behaviour proceeds + +set -euo pipefail + +OLLIE="${OLLIE:-$HOME/mnt/ollie}" +BLACKLIST="${XDG_CONFIG_HOME:-$HOME/.config}/ollie/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" + +# Read context injected by the hook runner. +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: blacklist the current model so future sessions skip it. +if [ "$error_type" = "tool_unsupported" ]; then + mkdir -p "$(dirname "$BLACKLIST")" + 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 + +# Build candidate list: :free models not in the blacklist, excluding current. +mapfile -t candidates < <( + cat "$B/models" 2>/dev/null \ + | grep ':free' \ + | sort \ + | grep -vxF "$current_model" \ + | { [ -f "$BLACKLIST" ] && grep -vxFf "$BLACKLIST" || cat; } +) + +if [ ${#candidates[@]} -eq 0 ]; then + printf '%s: no viable free models remaining\n' "$cmd" >&2 + exit 1 +fi + +next_model="${candidates[0]}" + +# 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/prompt" || die "failed to resubmit prompt" + +printf '%s: %s → %s\n' "$cmd" "$current_model" "$next_model" >&2 +exit 0