diff --git a/.agents/scripts/model-availability-helper.sh b/.agents/scripts/model-availability-helper.sh index 1ae3963c0..a5e6c4d82 100755 --- a/.agents/scripts/model-availability-helper.sh +++ b/.agents/scripts/model-availability-helper.sh @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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 # ============================================================================= @@ -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 @@ -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 @@ -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" @@ -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 diff --git a/.agents/scripts/model-registry-helper.sh b/.agents/scripts/model-registry-helper.sh index 8bd117ccd..d4f64770c 100755 --- a/.agents/scripts/model-registry-helper.sh +++ b/.agents/scripts/model-registry-helper.sh @@ -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 @@ -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 @@ -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 } diff --git a/tests/test-model-availability.sh b/tests/test-model-availability.sh index 500a35ebd..e18a22d56 100755 --- a/tests/test-model-availability.sh +++ b/tests/test-model-availability.sh @@ -188,12 +188,12 @@ fi # Check with known provider (may succeed or fail depending on keys) # Use || true to prevent set -e from aborting on non-zero exit -for provider in anthropic openai google; do +for provider in anthropic openai google opencode; do check_exit=0 run_with_timeout 15 bash "$HELPER" check "$provider" --quiet >/dev/null 2>&1 || check_exit=$? case "$check_exit" in 0) pass "check $provider: healthy" ;; - 1) pass "check $provider: unhealthy (expected without key)" ;; + 1) pass "check $provider: unhealthy (expected without key or CLI)" ;; 2) pass "check $provider: rate limited" ;; 3) pass "check $provider: no key (expected in CI)" ;; *) fail "check $provider: unexpected exit code $check_exit" ;; @@ -255,7 +255,41 @@ else fi # ============================================================ -# SECTION 7: JSON output +# SECTION 7: OpenCode Integration +# ============================================================ +section "OpenCode Integration" + +# Verify opencode is a known provider +if bash "$HELPER" help 2>&1 | grep -q "opencode"; then + pass "help mentions opencode provider" +else + fail "help mentions opencode provider" +fi + +# Check opencode provider (should succeed if CLI installed, fail gracefully otherwise) +check_oc_exit=0 +run_with_timeout 10 bash "$HELPER" check opencode --quiet >/dev/null 2>&1 || check_oc_exit=$? +case "$check_oc_exit" in + 0) pass "check opencode: healthy (CLI and cache available)" ;; + 1) pass "check opencode: unhealthy (CLI or cache not available)" ;; + *) fail "check opencode: unexpected exit code $check_oc_exit" ;; +esac + +# Verify opencode model check (if opencode is available) +if command -v opencode &>/dev/null && [[ -f "$HOME/.cache/opencode/models.json" ]]; then + oc_model_exit=0 + run_with_timeout 10 bash "$HELPER" check "opencode/claude-sonnet-4" --quiet >/dev/null 2>&1 || oc_model_exit=$? + case "$oc_model_exit" in + 0) pass "check opencode/claude-sonnet-4: available" ;; + 1) pass "check opencode/claude-sonnet-4: not available (provider unhealthy)" ;; + *) fail "check opencode/claude-sonnet-4: unexpected exit code $oc_model_exit" ;; + esac +else + skip "opencode model check (opencode CLI not installed)" +fi + +# ============================================================ +# SECTION 8: JSON output # ============================================================ section "JSON Output"