Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 119 additions & 15 deletions .agents/scripts/model-availability-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ readonly DEFAULT_HEALTH_TTL=300 # 5 minutes for health checks
readonly DEFAULT_RATELIMIT_TTL=60 # 1 minute for rate limit data
readonly PROBE_TIMEOUT=10 # HTTP request timeout in seconds

# Known providers list
readonly KNOWN_PROVIDERS="anthropic openai google openrouter groq deepseek"
# Known providers list (opencode is a meta-provider routing through its gateway)
readonly KNOWN_PROVIDERS="anthropic openai google openrouter groq deepseek opencode"

# OpenCode models cache (from models.dev, refreshed by opencode CLI)
readonly OPENCODE_MODELS_CACHE="${HOME}/.cache/opencode/models.json"

# Provider API endpoints for lightweight probes
# These endpoints are chosen for minimal cost: /models endpoints are free
Expand All @@ -71,6 +74,7 @@ get_provider_endpoint() {
openrouter) echo "https://openrouter.ai/api/v1/models" ;;
groq) echo "https://api.groq.com/openai/v1/models" ;;
deepseek) echo "https://api.deepseek.com/v1/models" ;;
opencode) echo "https://opencode.ai/zen/v1/models" ;;
*) return 1 ;;
esac
return 0
Expand All @@ -86,6 +90,7 @@ get_provider_key_vars() {
openrouter) echo "OPENROUTER_API_KEY" ;;
groq) echo "GROQ_API_KEY" ;;
deepseek) echo "DEEPSEEK_API_KEY" ;;
opencode) echo "OPENCODE_API_KEY" ;;
*) return 1 ;;
esac
return 0
Expand All @@ -95,26 +100,44 @@ get_provider_key_vars() {
is_known_provider() {
local provider="$1"
case "$provider" in
anthropic|openai|google|openrouter|groq|deepseek) return 0 ;;
anthropic|openai|google|openrouter|groq|deepseek|opencode) return 0 ;;
*) return 1 ;;
esac
}

# Tier to primary/fallback model mapping
# Format: primary_provider/model|fallback_provider/model
# When OpenCode is available, prefer opencode/* model IDs (routed through
# OpenCode's gateway, no direct API keys needed for these).
get_tier_models() {
local tier="$1"
case "$tier" in
haiku) echo "anthropic/claude-3-5-haiku-20241022|google/gemini-2.5-flash" ;;
flash) echo "google/gemini-2.5-flash|openai/gpt-4.1-mini" ;;
sonnet) echo "anthropic/claude-sonnet-4-20250514|openai/gpt-4.1" ;;
pro) echo "google/gemini-2.5-pro|anthropic/claude-sonnet-4-20250514" ;;
opus) echo "anthropic/claude-opus-4-6|openai/o3" ;;
health) echo "anthropic/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
eval) echo "anthropic/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
coding) echo "anthropic/claude-opus-4-6|openai/o3" ;;
*) return 1 ;;
esac

# Check if OpenCode is available (CLI installed and models cache exists)
if _is_opencode_available; then
case "$tier" in
haiku) echo "opencode/claude-3-5-haiku|opencode/gemini-3-flash" ;;
flash) echo "google/gemini-2.5-flash|opencode/gemini-3-flash" ;;
sonnet) echo "opencode/claude-sonnet-4|anthropic/claude-sonnet-4-20250514" ;;
pro) echo "google/gemini-2.5-pro|opencode/gemini-3-pro" ;;
opus) echo "opencode/claude-opus-4-6|anthropic/claude-opus-4-6" ;;
health) echo "opencode/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
eval) echo "opencode/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
coding) echo "opencode/claude-opus-4-6|anthropic/claude-opus-4-6" ;;
*) return 1 ;;
esac
else
case "$tier" in
haiku) echo "anthropic/claude-3-5-haiku-20241022|google/gemini-2.5-flash" ;;
flash) echo "google/gemini-2.5-flash|openai/gpt-4.1-mini" ;;
sonnet) echo "anthropic/claude-sonnet-4-20250514|openai/gpt-4.1" ;;
pro) echo "google/gemini-2.5-pro|anthropic/claude-sonnet-4-20250514" ;;
opus) echo "anthropic/claude-opus-4-6|openai/o3" ;;
health) echo "anthropic/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
eval) echo "anthropic/claude-sonnet-4-5|google/gemini-2.5-flash" ;;
coding) echo "anthropic/claude-opus-4-6|openai/o3" ;;
*) return 1 ;;
esac
fi
return 0
}

Expand All @@ -127,6 +150,52 @@ is_known_tier() {
esac
}

# =============================================================================
# OpenCode Integration
# =============================================================================
# OpenCode maintains a model registry from models.dev cached at
# ~/.cache/opencode/models.json. This provides instant model discovery
# without needing direct API keys for each provider.

_is_opencode_available() {
# Check if opencode CLI exists and models cache is present
if command -v opencode &>/dev/null && [[ -f "$OPENCODE_MODELS_CACHE" && -s "$OPENCODE_MODELS_CACHE" ]]; then
return 0
fi
return 1
}

# Check if a model exists in the OpenCode models cache.
# Returns 0 if found, 1 if not.
_opencode_model_exists() {
local model_spec="$1"
local provider model_id

if [[ "$model_spec" == *"/"* ]]; then
provider="${model_spec%%/*}"
model_id="${model_spec#*/}"
else
model_id="$model_spec"
provider=""
fi

if [[ ! -f "$OPENCODE_MODELS_CACHE" || ! -s "$OPENCODE_MODELS_CACHE" ]]; then
return 1
fi

# Check the cache JSON: providers are top-level keys, models are nested
if [[ -n "$provider" ]]; then
jq -e --arg p "$provider" --arg m "$model_id" \
'.[$p].models[$m] // empty' "$OPENCODE_MODELS_CACHE" >/dev/null 2>&1
return $?
else
# Search all providers for this model ID
jq -e --arg m "$model_id" \
'[.[] | .models[$m] // empty] | length > 0' "$OPENCODE_MODELS_CACHE" >/dev/null 2>&1
return $?
fi
}

# =============================================================================
# Database Setup
# =============================================================================
Expand Down Expand Up @@ -392,6 +461,25 @@ probe_provider() {
fi
fi

# OpenCode provider: check via models cache (no API key needed)
if [[ "$provider" == "opencode" ]]; then
if _is_opencode_available; then
local oc_models_count=0
oc_models_count=$(jq -r '.opencode.models | length' "$OPENCODE_MODELS_CACHE" 2>/dev/null || echo "0")
_record_health "opencode" "healthy" 200 0 "" "$oc_models_count"
[[ "$quiet" != "true" ]] && print_success "opencode: healthy ($oc_models_count models in cache)"
db_query "
INSERT INTO probe_log (provider, action, result, duration_ms, details)
VALUES ('opencode', 'cache_check', 'healthy', 0, '$oc_models_count models from cache');
" || true
return 0
else
_record_health "opencode" "unhealthy" 0 0 "OpenCode CLI or models cache not found" 0
[[ "$quiet" != "true" ]] && print_warning "opencode: CLI or models cache not available"
return 1
fi
fi

# Resolve API key
local key_var
if ! key_var=$(resolve_api_key "$provider"); then
Expand Down Expand Up @@ -703,7 +791,14 @@ check_model_available() {
fi
fi

# Model-level check: query the model-registry if available
# Model-level check 1: OpenCode models cache (instant, preferred)
if _opencode_model_exists "$model_spec"; then
_record_model_availability "$model_id" "$provider" 1
[[ "$quiet" != "true" ]] && print_success "$model_spec: available (OpenCode cache confirmed)"
return 0
fi

# Model-level check 2: query the model-registry SQLite if available
local registry_db="${AVAILABILITY_DIR}/model-registry.db"
if [[ -f "$registry_db" ]]; then
local in_registry
Expand Down Expand Up @@ -1275,8 +1370,16 @@ cmd_help() {
echo " eval - Cheap evaluation model"
echo " coding - Best SOTA coding model"
echo ""
echo "Providers:"
echo " anthropic, openai, google, openrouter, groq, deepseek, opencode"
echo " The 'opencode' provider uses the OpenCode models cache (~/.cache/opencode/models.json)"
echo " instead of direct API probing. When OpenCode is available, tier resolution"
echo " prefers opencode/* model IDs (routed through OpenCode's gateway)."
echo ""
echo "Examples:"
echo " model-availability-helper.sh check anthropic"
echo " model-availability-helper.sh check opencode"
echo " model-availability-helper.sh check opencode/claude-sonnet-4"
echo " model-availability-helper.sh check anthropic/claude-sonnet-4-20250514"
echo " model-availability-helper.sh check sonnet"
echo " model-availability-helper.sh probe --all"
Expand Down Expand Up @@ -1304,6 +1407,7 @@ cmd_help() {
echo " 3 - API key invalid or missing"
echo ""
echo "Cache: $AVAILABILITY_DB"
echo "OpenCode models: $OPENCODE_MODELS_CACHE"
echo "TTL: ${DEFAULT_HEALTH_TTL}s (health), ${DEFAULT_RATELIMIT_TTL}s (rate limits)"
echo ""
return 0
Expand Down
127 changes: 123 additions & 4 deletions .agents/scripts/model-registry-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,127 @@ ${line}"
}

# =============================================================================
# Sync: Provider APIs (live model discovery)
# Sync: OpenCode Models (preferred — uses opencode CLI model registry)
# =============================================================================
# OpenCode maintains a model registry sourced from models.dev that includes
# all providers the user has configured. This is faster and more reliable than
# probing individual provider APIs directly, and works even without direct
# API keys (OpenCode routes through its gateway for opencode/* models).

sync_opencode() {
local added=0

# Check if opencode CLI is available
if ! command -v opencode &>/dev/null; then
print_info "OpenCode CLI not found, skipping opencode model sync"
return 0
fi

# Try the cached models file first (instant, no network)
local cache_file="${HOME}/.cache/opencode/models.json"
local model_ids=""

# Only extract models from providers we track (avoids processing 2500+ models
# from the full models.dev registry — we only need ~100 from relevant providers)
local relevant_providers='["anthropic","openai","google","openrouter","groq","deepseek","opencode"]'

if [[ -f "$cache_file" && -s "$cache_file" ]]; then
model_ids=$(jq -r --argjson providers "$relevant_providers" '
to_entries[] |
select(.key as $k | $providers | index($k)) |
.key as $provider |
.value.models // {} |
keys[] |
"\($provider)/\(.)"
' "$cache_file" 2>/dev/null) || true
fi

# Fallback: use opencode models CLI if cache is empty or missing
if [[ -z "$model_ids" ]]; then
print_info " Cache miss, querying opencode models CLI..."
model_ids=$(timeout "$DEFAULT_TIMEOUT" opencode models 2>/dev/null \
| grep -E '^(anthropic|openai|google|openrouter|groq|deepseek|opencode)/' | sort) || true
fi

if [[ -z "$model_ids" ]]; then
print_warning "No models discovered from OpenCode"
return 0
fi

# Build batch SQL for all models (much faster than individual queries)
local sql_batch="BEGIN TRANSACTION;"
while IFS= read -r full_id; do
[[ -z "$full_id" ]] && continue
# Skip non-text models (embeddings, tts, whisper, image-only)
case "$full_id" in
*embed*|*tts*|*whisper*|*dall-e*|*moderation*|*image*) continue ;;
esac

local provider
provider="${full_id%%/*}"

# Normalize provider name to match our conventions
local norm_provider
case "$provider" in
anthropic) norm_provider="Anthropic" ;;
openai) norm_provider="OpenAI" ;;
google) norm_provider="Google" ;;
openrouter) norm_provider="OpenRouter" ;;
groq) norm_provider="Groq" ;;
deepseek) norm_provider="DeepSeek" ;;
opencode) norm_provider="OpenCode" ;;
*) norm_provider="$provider" ;;
esac

sql_batch="${sql_batch}
INSERT INTO provider_models (model_id, provider, in_registry, in_subagents, discovered_at)
VALUES (
'$(sql_escape "$full_id")',
'$(sql_escape "$norm_provider")',
0, 0,
strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
)
ON CONFLICT(model_id, provider) DO UPDATE SET
discovered_at = excluded.discovered_at;"
added=$((added + 1))
done <<< "$model_ids"
sql_batch="${sql_batch} COMMIT;"

# Execute batch insert
db_query "$sql_batch"

db_query "
INSERT INTO sync_log (sync_type, source, models_added, models_updated, details)
VALUES ('opencode', 'opencode-models', $added, 0, 'Discovered from OpenCode model registry');
"

print_info "OpenCode sync: $added models discovered"
return 0
}

# =============================================================================
# Sync: Provider APIs (fallback — direct API key probing)
# =============================================================================
# Falls back to direct API probing when OpenCode CLI is not available.
# Requires individual provider API keys in environment.

sync_providers() {
local added=0
local json_flag="${1:-false}"

# Skip if opencode sync already ran successfully (check sync_log)
local opencode_synced
opencode_synced=$(db_query "
SELECT models_added FROM sync_log
WHERE sync_type = 'opencode'
AND (julianday('now') - julianday(timestamp)) * 86400 < 60
ORDER BY id DESC LIMIT 1;
" 2>/dev/null || echo "")
if [[ -n "$opencode_synced" && "$opencode_synced" -gt 0 ]]; then
print_info "Skipping direct API probing (OpenCode sync already discovered $opencode_synced models)"
return 0
fi

# Reuse provider key detection from compare-models-helper.sh
local provider_env_keys="Anthropic|ANTHROPIC_API_KEY
OpenAI|OPENAI_API_KEY
Expand Down Expand Up @@ -559,8 +673,12 @@ cmd_sync() {
[[ "$quiet" != "true" ]] && print_info "Phase 2: Syncing embedded model data..."
sync_embedded

# Phase 3: Sync from provider APIs (if keys available)
[[ "$quiet" != "true" ]] && print_info "Phase 3: Discovering models from provider APIs..."
# Phase 3: Sync from OpenCode model registry (preferred — fast, no API keys needed)
[[ "$quiet" != "true" ]] && print_info "Phase 3: Discovering models from OpenCode registry..."
sync_opencode

# Phase 4: Fallback to direct provider API probing (skipped if Phase 3 succeeded)
[[ "$quiet" != "true" ]] && print_info "Phase 4: Direct provider API discovery (fallback)..."
sync_providers

# Cleanup old backups
Expand Down Expand Up @@ -1154,7 +1272,8 @@ cmd_help() {
echo "Data Sources:"
echo " 1. Subagent frontmatter ($MODELS_DIR/*.md)"
echo " 2. Embedded data (compare-models-helper.sh MODEL_DATA)"
echo " 3. Provider APIs (Anthropic, OpenAI, Google, OpenRouter, Groq, DeepSeek)"
echo " 3. OpenCode model registry (opencode models — preferred, from models.dev)"
echo " 4. Provider APIs (Anthropic, OpenAI, Google, OpenRouter, Groq, DeepSeek)"
echo ""
return 0
}
Expand Down
Loading