Skip to content

Instantly share code, notes, and snippets.

@jnaskali
Created November 22, 2025 20:24
Show Gist options
  • Select an option

  • Save jnaskali/87d35a3b62200f02b54670a004000ec6 to your computer and use it in GitHub Desktop.

Select an option

Save jnaskali/87d35a3b62200f02b54670a004000ec6 to your computer and use it in GitHub Desktop.
OpenRouter ZDR Model Pricing Checker - bash script for querying Zero-Data-Retention models on OpenRouter
#!/bin/bash
#
# OOOOOOOOO RRRRRRRRRRRRRRRRR ZZZZZZZZZZZZZZZZZZZDDDDDDDDDDDDD RRRRRRRRRRRRRRRRR
# OO:::::::::OO R::::::::::::::::R Z:::::::::::::::::ZD::::::::::::DDD R::::::::::::::::R
# OO:::::::::::::OO R::::::RRRRRR:::::R Z:::::::::::::::::ZD:::::::::::::::DD R::::::RRRRRR:::::R
# O:::::::OOO:::::::ORR:::::R R:::::R Z:::ZZZZZZZZ:::::Z DDD:::::DDDDD:::::DRR:::::R R:::::R
# O::::::O O::::::O R::::R R:::::R ZZZZZ Z:::::Z D:::::D D:::::D R::::R R:::::R
# O:::::O O:::::O R::::R R:::::R Z:::::Z D:::::D D:::::DR::::R R:::::R
# O:::::O O:::::O R::::RRRRRR:::::R Z:::::Z D:::::D D:::::DR::::RRRRRR:::::R
# O:::::O O:::::O R:::::::::::::RR -------- Z:::::Z D:::::D D:::::DR:::::::::::::RR
# O:::::O O:::::O R::::RRRRRR:::::R -::::::- Z:::::Z D:::::D D:::::DR::::RRRRRR:::::R
# O:::::O O:::::O R::::R R:::::R -------- Z:::::Z D:::::D D:::::DR::::R R:::::R
# O:::::O O:::::O R::::R R:::::R Z:::::Z D:::::D D:::::DR::::R R:::::R
# O::::::O O::::::O R::::R R:::::R ZZZ:::::Z ZZZZZ D:::::D D:::::D R::::R R:::::R
# O:::::::OOO:::::::ORR:::::R R:::::R Z::::::ZZZZZZZZ:::ZDDD:::::DDDDD:::::DRR:::::R R:::::R
# OO:::::::::::::OO R::::::R R:::::R Z:::::::::::::::::ZD:::::::::::::::DD R::::::R R:::::R
# OO:::::::::OO R::::::R R:::::R Z:::::::::::::::::ZD::::::::::::DDD R::::::R R:::::R
# OOOOOOOOO RRRRRRRR RRRRRRR ZZZZZZZZZZZZZZZZZZZDDDDDDDDDDDDD RRRRRRRR RRRRRRR
#
# OpenRouter ZDR Model Pricing Checker
#
# Maintainer: Juhani Naskali
# License: MIT <https://opensource.org/licenses/MIT>
set -euo pipefail
# --- Configuration ---
readonly API_URL="https://openrouter.ai/api/v1/endpoints/zdr"
readonly CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/openrouter-zdr"
readonly CACHE_FILE="$CACHE_DIR/models.json"
readonly CACHE_OLD_FILE="$CACHE_DIR/models.old.json"
readonly CACHE_TTL=3600
# Default Curated List (Regex)
# Default regex to return, along with all Free models.
readonly CURATED_PATTERNS="claude.*4\.|gemini-2\.5-flash$|gemini-3|grok|perplexity-sonar|(?<!nous)research"
# Colors
readonly BOLD='\033[1m'
readonly GREEN='\033[0;32m'
readonly BLUE='\033[0;34m'
readonly YELLOW='\033[0;33m'
readonly CYAN='\033[0;36m'
readonly RED='\033[0;31m'
readonly NC='\033[0m'
# Defaults
SHOW_ALL=false
SHOW_ID=false
SHOW_FREE=false
WIDE_MODE=false
FORCE_REFRESH=false
LIMIT=0
SORT_BY="default"
USE_CURATED=true # default mode
# Filter Defaults (Values that result in 'true' in logic if unset)
MIN_CTX=0
FILTER_PROV=""
FILTER_TOOLS=false
CUSTOM_REGEX=""
# Functions
check_deps() {
for cmd in jq curl; do
command -v "$cmd" >/dev/null 2>&1 || { echo -e "${RED}Error: '$cmd' is required.${NC}"; exit 1; }
done
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
echo -e "${RED}Error: OPENROUTER_API_KEY is not set.${NC}"; exit 1
fi
}
fetch_data() {
mkdir -p "$CACHE_DIR"
local fetch=true
if [[ "$FORCE_REFRESH" == "false" && -f "$CACHE_FILE" ]]; then
local age
age=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE")))
if (( age < CACHE_TTL )); then
fetch=false
echo -e "${GREEN}Using cached data (Updated $((age / 60)) min ago)${NC}"
fi
fi
if [[ "$fetch" == "true" ]]; then
echo -e "${YELLOW}Fetching ZDR models...${NC}"
local response
response=$(curl -s "$API_URL")
if echo "$response" | jq -e .data >/dev/null 2>&1; then
[[ -f "$CACHE_FILE" ]] && cp "$CACHE_FILE" "$CACHE_OLD_FILE"
echo "$response" > "$CACHE_FILE"
echo -e "${GREEN}Cache updated.${NC}"
else
echo -e "${RED}API Error. Using cache if available.${NC}"
[[ ! -f "$CACHE_FILE" ]] && exit 1
fi
fi
}
get_new_models() {
if [[ -f "$CACHE_OLD_FILE" && -f "$CACHE_FILE" ]]; then
jq -n \
--slurpfile old "$CACHE_OLD_FILE" \
--slurpfile new "$CACHE_FILE" \
'($old[0].data | map(.model_name)) as $o | ($new[0].data | map(.model_name)) as $n | ($n - $o) | unique'
else
echo "[]"
fi
}
render_table() {
local new_models_json="$1"
# 1. Define Formatting String based on Wide Mode
local name_fmt="%-48.48s"
local table_width="118"
if [[ "$WIDE_MODE" == "true" ]]; then
name_fmt="%-60s" # Flexible width
table_width="130"
fi
# 2. Header
echo -e "${BLUE}$(printf '=%.0s' $(seq 1 "$table_width"))${NC}"
printf "${BOLD}${name_fmt} %-14s %-9s %-9s %-4s %-12s %s${NC}\n" "Model" "Provider" "Prompt" "Output" "TRW" "Ctx" "Uptime"
echo -e "${BLUE}$(printf '=%.0s' $(seq 1 "$table_width"))${NC}"
# 3. Construct JQ Filter
local jq_filter='
.data
| map(select(
# A. Scope Logic (Curated vs All vs Search)
(
if ($use_curated == "true") then
# Match Curated Regex OR be a Free model
(.model_name | test($curated_pattern; "i")) or
(.name | test($curated_pattern; "i")) or
((.pricing.prompt|tonumber) == 0 and (.pricing.completion|tonumber) == 0)
else
true # If any filter is set, we start with ALL models
end
)
and
# B. Custom Regex Search (Matches ID or Name based on display mode)
(
if ($regex != "") then
if ($show_id == "true") then
(.name | test($regex; "i"))
else
(.model_name | test($regex; "i"))
end
else true end
)
and
# C. Hard Filters
(if ($show_free == "true") then ((.pricing.prompt|tonumber) == 0 and (.pricing.completion|tonumber) == 0) else true end)
and
(if ($tools_only == "true") then (.supported_parameters | index("tools")) else true end)
and
(.context_length >= ($min_ctx|tonumber))
and
(if ($prov_filter != "") then (.provider_name | test($prov_filter; "i")) else true end)
))
# Deduplication (Group by model name, take cheapest provider)
| group_by(.model_name)
| map(sort_by(.pricing.completion|tonumber) | .[0])
| flatten
'
# 4. Append Sorting Logic
case "$SORT_BY" in
price) jq_filter+=" | sort_by(.pricing.completion | tonumber)" ;;
prompt) jq_filter+=" | sort_by(.pricing.prompt | tonumber)" ;;
context) jq_filter+=" | sort_by(.context_length | tonumber)" ;;
uptime) jq_filter+=" | sort_by(.uptime_last_30m // 0 | tonumber) | reverse" ;; # Highest uptime first
name) jq_filter+=" | sort_by(.model_name)" ;;
default) jq_filter+=" | sort_by(.model_name, (.pricing.completion | tonumber))" ;;
esac
# 5. Append Limit Logic
if [[ "$LIMIT" -gt 0 ]]; then
jq_filter+=" | .[0:$LIMIT]"
fi
# 6. Final Output Formatting
jq_filter+='
| .[]
| . as $row
| (if (.pricing.prompt|tonumber) == 0 then "FREE" else "$" + ((.pricing.prompt|tonumber * 1000000 * 100 | round / 100)|tostring) + "/M" end) as $p
| (if (.pricing.completion|tonumber) == 0 then "FREE" else "$" + ((.pricing.completion|tonumber * 1000000 * 100 | round / 100)|tostring) + "/M" end) as $c
| (if .context_length >= 1000000 then (.context_length/1000000|tostring)+"M" elif .context_length >= 1000 then (.context_length/1000|floor|tostring)+"K" else (.context_length|tostring) end) as $ctx
# Uptime Logic: Round to 2 decimals if NOT wide mode
| (if .uptime_last_30m then
(if $wide_mode == "true" then
(.uptime_last_30m|tostring)
else
((.uptime_last_30m * 100 | round) / 100 | tostring)
end) + "%"
else "N/A" end) as $up
| (if ($show_id == "true") then ($row.name | split(" | ") | last) else ($row.model_name + (if ($row.quantization and $row.quantization != "unknown") then " ["+$row.quantization+"]" else "" end)) end) as $name
| (
(if (.supported_parameters | index("tools")) then "T" else "-" end) +
(if (.pricing.internal_reasoning != "0" or (.supported_parameters | any(. == "reasoning" or . == "thinking"))) then "R" else "-" end) +
(if (.pricing.web_search != "0" or (.model_name | test("sonar|search";"i"))) then "W" else "-" end)
) as $caps
| (if ($new_ids | index($row.model_name)) then "NEW" else "OLD" end) as $status
| [$name, $row.provider_name[0:14], $p, $c, $caps, $ctx, $up, $status] | @tsv
'
# Execute JQ
jq -r --arg show_id "$SHOW_ID" \
--arg show_free "$SHOW_FREE" \
--arg tools_only "$FILTER_TOOLS" \
--arg prov_filter "$FILTER_PROV" \
--arg min_ctx "$MIN_CTX" \
--arg use_curated "$USE_CURATED" \
--arg wide_mode "$WIDE_MODE" \
--arg curated_pattern "$CURATED_PATTERNS" \
--arg regex "$CUSTOM_REGEX" \
--argjson new_ids "$new_models_json" \
"$jq_filter" "$CACHE_FILE" | \
while IFS=$'\t' read -r name prov prompt comp caps ctx uptime status; do
if [[ "$status" == "NEW" ]]; then
# If New: Truncate name slightly less to fit the *
if [[ "$WIDE_MODE" == "true" ]]; then
printf "${name_fmt} ${GREEN}*${NC} " "$name"
else
printf "%-46.46s ${GREEN}*${NC} " "$name"
fi
else
printf "${name_fmt} " "$name"
fi
printf "%-14s %-9s %-9s %-4s %-12s %s\n" "$prov" "$prompt" "$comp" "$caps" "$ctx" "$uptime"
done
echo -e "${BLUE}$(printf '=%.0s' $(seq 1 "$table_width"))${NC}"
}
# --- Argument Parsing ---
while [[ $# -gt 0 ]]; do
case $1 in
# Display Modes
-a|--all) SHOW_ALL=true; USE_CURATED=false; shift ;;
-i|--id) SHOW_ID=true; shift ;;
-w|--wide) WIDE_MODE=true; shift ;;
-r|--refresh) FORCE_REFRESH=true; shift ;;
# Filters (All filters disable USE_CURATED)
-f|--free) SHOW_FREE=true; USE_CURATED=false; shift ;;
-t|--tools) FILTER_TOOLS=true; USE_CURATED=false; shift ;;
-P|--provider) FILTER_PROV="$2"; USE_CURATED=false; shift 2 ;;
-c|--context) MIN_CTX="$2"; USE_CURATED=false; shift 2 ;;
# Sorting & Limits
-n|--limit) LIMIT="$2"; shift 2 ;;
-s|--sort) SORT_BY="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 [OPTIONS] [REGEX]"
echo "Arguments:"
echo " REGEX Custom regex search (matches Name, or ID if -i is used)"
echo ""
echo "Display Options:"
echo " -a, --all Show all ZDR models (overrides curated list)"
echo " -i, --id Show Model IDs instead of display names"
echo " -w, --wide Don't truncate model names (for wide screens). Shows raw Uptime."
echo " -n, --limit N Show only top N results"
echo ""
echo "Filtering (Automatically searches ALL models, ignoring curated list):"
echo " -f, --free Show only free models"
echo " -t, --tools Show only models supporting Tools/Function Calling"
echo " -P, --provider Filter by Provider name (regex)"
echo " -c, --context Minimum context length (e.g. 128000)"
echo ""
echo "Sorting:"
echo " -s, --sort KEY Sort by: price, context, uptime, prompt, name"
echo ""
echo "System:"
echo " -r, --refresh Force API cache refresh"
exit 0
;;
*)
# Handle positional argument as Custom Regex
if [[ "$1" =~ ^- ]]; then
echo "Unknown option: $1"; exit 1
else
CUSTOM_REGEX="$1"
USE_CURATED=false
shift
fi
;;
esac
done
# Execution
check_deps
echo -e "${BOLD}${BLUE}OpenRouter ZDR Model Pricing${NC}"
# Info Line showing active filters
INFO_MSG="${CYAN}Mode:${NC} "
if [[ "$USE_CURATED" == "true" ]]; then INFO_MSG+="Curated List"; else INFO_MSG+="All/Filtered"; fi
[[ "$SHOW_FREE" == "true" ]] && INFO_MSG+=", Free Only"
[[ "$FILTER_TOOLS" == "true" ]] && INFO_MSG+=", Tools Support"
[[ "$MIN_CTX" -gt 0 ]] && INFO_MSG+=", Ctx > $MIN_CTX"
[[ -n "$CUSTOM_REGEX" ]] && INFO_MSG+=", Search: '$CUSTOM_REGEX'"
echo -e "$INFO_MSG"
fetch_data
NEW_MODELS=$(get_new_models)
NEW_COUNT=$(echo "$NEW_MODELS" | jq length)
if (( NEW_COUNT > 0 )); then
echo -e "${GREEN}Found $NEW_COUNT new model(s)${NC}"
fi
render_table "$NEW_MODELS"
# Final Legend
if (( NEW_COUNT > 0 )); then
echo -e "${CYAN}Legend:${NC} TRW (T=Tools, R=Reasoning, W=Web) | ${GREEN}* New Model${NC}"
else
echo -e "${CYAN}Legend:${NC} TRW (T=Tools, R=Reasoning, W=Web)"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment