Spaces:
Sleeping
Sleeping
| """Model registry — discovery, validation, and provider detection. | |
| Delegates entirely to LiteLLM for provider detection, environment | |
| validation, and model discovery. No external API calls for discovery — | |
| only ``validate_connection`` makes a real (tiny) LLM call. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import time | |
| from dataclasses import dataclass | |
| from typing import Any | |
| logger = logging.getLogger(__name__) | |
| def _litellm(): | |
| """Return the litellm module, importing it on first call.""" | |
| global _litellm_mod | |
| try: | |
| return _litellm_mod # type: ignore[name-defined] | |
| except NameError: | |
| pass | |
| try: | |
| import litellm as _mod | |
| _litellm_mod = _mod | |
| return _mod | |
| except ImportError: | |
| _litellm_mod = None | |
| return None | |
| # Example model strings per provider (for user guidance in the CLI) | |
| PROVIDER_MODEL_EXAMPLES: dict[str, str] = { | |
| "openai": "gpt-4o-mini", | |
| "anthropic": "claude-sonnet-4-20250514", | |
| "gemini": "gemini/gemini-2.0-flash", | |
| "deepseek": "deepseek/deepseek-chat", | |
| "groq": "groq/llama-3.1-70b", | |
| "bedrock": "us.anthropic.claude-haiku-4-5-20251001-v1:0", | |
| "ollama": "ollama/llama2", | |
| "azure": "azure/gpt-4", | |
| "openrouter": "openrouter/anthropic/claude-3.5-sonnet", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Data types | |
| # --------------------------------------------------------------------------- | |
| class ValidationResult: | |
| """Result of a model + key validation.""" | |
| success: bool | |
| model: str = "" | |
| provider: str = "" | |
| latency_ms: int = 0 | |
| error: str = "" | |
| class ModelInfo: | |
| """Metadata about a model from LiteLLM's registry.""" | |
| model: str | |
| provider: str | |
| max_input_tokens: int | None = None | |
| max_output_tokens: int | None = None | |
| input_cost_per_m: float | None = None # per million tokens | |
| output_cost_per_m: float | None = None | |
| key_found: bool = False | |
| # --------------------------------------------------------------------------- | |
| # Provider detection (delegated to LiteLLM) | |
| # --------------------------------------------------------------------------- | |
| def get_provider(model: str) -> str: | |
| """Return the provider name for a model string, or 'unknown'.""" | |
| ll = _litellm() | |
| if ll is None: | |
| raise ImportError("LiteLLM is required for model validation.") | |
| try: | |
| _, provider, _, _ = ll.get_llm_provider(model) | |
| except Exception as e: | |
| logger.debug( | |
| "Could not detect provider for %r (%s): %s", model, type(e).__name__, e | |
| ) | |
| provider = "unknown" | |
| return provider | |
| def get_missing_keys(model: str) -> list[str]: | |
| """Return env var names that LiteLLM says are missing for *model*.""" | |
| ll = _litellm() | |
| if ll is None: | |
| return [] | |
| try: | |
| result = ll.validate_environment(model=model) | |
| return result.get("missing_keys", []) | |
| except Exception as e: | |
| logger.debug( | |
| "Could not validate environment for %r (%s): %s", model, type(e).__name__, e | |
| ) | |
| return [] | |
| def keys_are_set(model: str) -> bool: | |
| """Check whether the required keys for *model* are in the environment.""" | |
| return len(get_missing_keys(model)) == 0 | |
| # --------------------------------------------------------------------------- | |
| # Connection validation | |
| # --------------------------------------------------------------------------- | |
| def validate_connection(model: str, api_key: str | None = None) -> ValidationResult: | |
| """Make a minimal LLM call to verify model + key work. | |
| Sends a 3-token request ("Say 'ok'") to confirm authentication, | |
| model availability, and network connectivity. | |
| Args: | |
| model: LiteLLM model string. | |
| api_key: Explicit key, or None to use environment. | |
| """ | |
| ll = _litellm() | |
| if ll is None: | |
| return ValidationResult( | |
| success=False, model=model, error="LiteLLM is not installed." | |
| ) | |
| call_params: dict[str, Any] = { | |
| "model": model, | |
| "messages": [{"role": "user", "content": "Say 'ok'"}], | |
| "max_tokens": 3, | |
| "temperature": 0.0, | |
| "timeout": 15, | |
| } | |
| if api_key: | |
| call_params["api_key"] = api_key | |
| # Suppress LiteLLM's noisy debug output during validation | |
| prev_verbose = getattr(ll, "suppress_debug_info", False) | |
| ll.suppress_debug_info = True | |
| start = time.monotonic() | |
| try: | |
| response = ll.completion(**call_params) | |
| elapsed_ms = int((time.monotonic() - start) * 1000) | |
| provider = "unknown" | |
| if hasattr(response, "_hidden_params"): | |
| provider = response._hidden_params.get("custom_llm_provider", "unknown") | |
| return ValidationResult( | |
| success=True, | |
| model=model, | |
| provider=provider, | |
| latency_ms=elapsed_ms, | |
| ) | |
| except ll.AuthenticationError: | |
| return ValidationResult(success=False, model=model, error="Invalid API key.") | |
| except ll.NotFoundError: | |
| return ValidationResult( | |
| success=False, | |
| model=model, | |
| error=f"Model '{model}' not found at the provider.", | |
| ) | |
| except ll.APIConnectionError: | |
| return ValidationResult( | |
| success=False, | |
| model=model, | |
| error="Could not connect to the provider.", | |
| ) | |
| except Exception as e: | |
| return ValidationResult(success=False, model=model, error=str(e)) | |
| finally: | |
| ll.suppress_debug_info = prev_verbose | |
| # --------------------------------------------------------------------------- | |
| # Model search / discovery | |
| # --------------------------------------------------------------------------- | |
| PROVIDER_KEY_ENV: dict[str, str | list[str]] = { | |
| "openai": "OPENAI_API_KEY", | |
| "anthropic": "ANTHROPIC_API_KEY", | |
| "azure": "AZURE_API_KEY", | |
| "gemini": "GEMINI_API_KEY", | |
| "deepseek": "DEEPSEEK_API_KEY", | |
| "groq": "GROQ_API_KEY", | |
| "bedrock": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION_NAME"], | |
| "bedrock_converse": [ | |
| "AWS_ACCESS_KEY_ID", | |
| "AWS_SECRET_ACCESS_KEY", | |
| "AWS_REGION_NAME", | |
| ], | |
| "vertex_ai": "GOOGLE_APPLICATION_CREDENTIALS", | |
| "cohere": "COHERE_API_KEY", | |
| "mistral": "MISTRAL_API_KEY", | |
| "openrouter": "OPENROUTER_API_KEY", | |
| "together_ai": "TOGETHERAI_API_KEY", | |
| "fireworks_ai": "FIREWORKS_AI_API_KEY", | |
| "replicate": "REPLICATE_API_KEY", | |
| "huggingface": "HUGGINGFACE_API_KEY", | |
| "perplexity": "PERPLEXITYAI_API_KEY", | |
| "anyscale": "ANYSCALE_API_KEY", | |
| } | |
| _PROVIDER_ALT_KEYS: dict[str, list[str]] = { | |
| "bedrock_converse": ["AWS_BEARER_TOKEN_BEDROCK"], | |
| } | |
| def _quick_key_check(provider: str) -> bool: | |
| """Fast check: are the required env vars set for this provider?""" | |
| env_var = PROVIDER_KEY_ENV.get(provider) | |
| if env_var is not None: | |
| if isinstance(env_var, list): | |
| if all(bool(os.environ.get(v)) for v in env_var): | |
| return True | |
| elif bool(os.environ.get(env_var)): | |
| return True | |
| # Alternative auth (e.g. bearer token for Bedrock) | |
| alt_vars = _PROVIDER_ALT_KEYS.get(provider) | |
| if alt_vars: | |
| return any(bool(os.environ.get(v)) for v in alt_vars) | |
| return False | |
| def search_models( | |
| query: str = "", | |
| provider: str | None = None, | |
| chat_only: bool = True, | |
| limit: int = 20, | |
| ) -> tuple[list[ModelInfo], int]: | |
| """Search LiteLLM's model registry. | |
| Args: | |
| query: Substring to match against model names. | |
| provider: Filter to a specific provider. | |
| chat_only: Only return chat/completion models. | |
| limit: Maximum results. | |
| Returns: | |
| (results, total_matches) — results capped at *limit*, | |
| total_matches is the full count of matching models. | |
| """ | |
| ll = _litellm() | |
| if ll is None: | |
| return [], 0 | |
| results: list[ModelInfo] = [] | |
| total = 0 | |
| terms = query.lower().split() if query else [] | |
| for model_id, info in ll.model_cost.items(): | |
| if chat_only and info.get("mode") != "chat": | |
| continue | |
| if provider and info.get("litellm_provider") != provider: | |
| continue | |
| model_lower = model_id.lower() | |
| if terms and not all(t in model_lower for t in terms): | |
| continue | |
| total += 1 | |
| if len(results) >= limit: | |
| continue # keep counting total | |
| prov = info.get("litellm_provider", "unknown") | |
| input_cost = info.get("input_cost_per_token") | |
| output_cost = info.get("output_cost_per_token") | |
| # Fast key check — just look for the provider's standard env var. | |
| # We avoid litellm.validate_environment() here because it's slow | |
| # and some providers (e.g. GitHub Copilot) trigger interactive auth. | |
| key_found = _quick_key_check(prov) | |
| results.append( | |
| ModelInfo( | |
| model=model_id, | |
| provider=prov, | |
| max_input_tokens=info.get("max_input_tokens"), | |
| max_output_tokens=info.get("max_output_tokens"), | |
| input_cost_per_m=input_cost * 1_000_000 if input_cost else None, | |
| output_cost_per_m=output_cost * 1_000_000 if output_cost else None, | |
| key_found=key_found, | |
| ) | |
| ) | |
| return results, total | |
| def suggest_models(typo: str, limit: int = 5) -> list[str]: | |
| """Return model names similar to *typo* (simple substring matching).""" | |
| ll = _litellm() | |
| if ll is None: | |
| return [] | |
| candidates: list[str] = [] | |
| typo_lower = typo.lower() | |
| for model_id, info in ll.model_cost.items(): | |
| if info.get("mode") != "chat": | |
| continue | |
| if model_id.lower().startswith(typo_lower): | |
| candidates.append(model_id) | |
| elif typo_lower in model_id.lower(): | |
| candidates.append(model_id) | |
| if len(candidates) >= limit: | |
| break | |
| return candidates | |