Source code for ralph.api.opencode
"""Fetch and cache the OpenCode model catalog from models.dev.
This module provides access to the OpenCode model catalog for
discovering available models and providers.
"""
from __future__ import annotations
import httpx
from loguru import logger
from ralph.api.model_entry import ModelEntry
CATALOG_URL = "https://models.dev/api.json"
TIMEOUT_SECS = 10
class _CatalogFetcher:
"""Callable cache around the OpenCode catalog."""
def __init__(self) -> None:
self._cache: list[ModelEntry] | None = None
def __call__(self) -> list[ModelEntry]:
if self._cache is not None:
return self._cache
logger.debug("Fetching model catalog from {}", CATALOG_URL)
payload: object
try:
with httpx.Client(timeout=TIMEOUT_SECS) as client:
response = client.get(CATALOG_URL)
response.raise_for_status()
payload = response.json()
except httpx.HTTPError as exc:
logger.error("Failed to fetch model catalog: {}", exc)
raise
raw = _parse_catalog_payload(payload)
models = [ModelEntry.model_validate(entry) for entry in raw]
logger.debug("Loaded {} models from catalog", len(models))
self._cache = models
return models
def cache_clear(self) -> None:
"""Clear any cached catalog."""
self._cache = None
def _parse_catalog_payload(payload: object) -> list[dict[str, object]]:
if isinstance(payload, list):
parsed: list[dict[str, object]] = []
for entry in payload:
if not isinstance(entry, dict):
msg = "Model catalog entries must be JSON objects"
logger.error(msg)
raise ValueError(msg)
parsed.append({str(key): value for key, value in entry.items()})
return parsed
if isinstance(payload, dict):
parsed = []
for provider_key, provider_entry in payload.items():
if not isinstance(provider_entry, dict):
msg = "Model catalog provider entries must be JSON objects"
logger.error(msg)
raise ValueError(msg)
models = provider_entry.get("models")
if not isinstance(models, dict):
msg = "Model catalog provider entries must contain a models object"
logger.error(msg)
raise ValueError(msg)
for model_key, model_entry in models.items():
if not isinstance(model_entry, dict):
msg = "Model catalog model entries must be JSON objects"
logger.error(msg)
raise ValueError(msg)
model_name = model_entry.get("name")
parsed.append(
{
"id": f"{provider_key}/{model_key}",
"provider": str(provider_key),
"name": str(model_name) if isinstance(model_name, str) else None,
}
)
return parsed
msg = "Model catalog JSON must be a list or provider map"
logger.error(msg)
raise ValueError(msg)
fetch_catalog = _CatalogFetcher()
[docs]
def get_model_by_id(model_id: str) -> ModelEntry | None:
"""Get a specific model by ID.
Args:
model_id: Model identifier to look up.
Returns:
ModelEntry if found, None otherwise.
"""
catalog = fetch_catalog()
for model in catalog:
if model.id == model_id:
return model
return None
[docs]
def search_models(query: str) -> list[ModelEntry]:
"""Search models by name or provider.
Args:
query: Search query (case-insensitive).
Returns:
List of matching ModelEntry instances.
"""
catalog = fetch_catalog()
query_lower = query.lower()
return [
model
for model in catalog
if query_lower in (model.name or "").lower()
or query_lower in (model.provider or "").lower()
or query_lower in model.id.lower()
]
[docs]
def list_providers() -> list[str]:
"""List all unique providers in the catalog.
Returns:
Sorted list of provider names.
"""
catalog = fetch_catalog()
providers = {model.provider for model in catalog if model.provider}
return sorted(providers)