Coverage for src / osiris_cli / provider_registry.py: 0%
124 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 16:44 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 16:44 +0200
1import json
2from datetime import datetime, timezone
3from pathlib import Path
4from typing import Dict, Any, Optional
6import httpx
7from openai import OpenAI
9from .config import CONFIG_DIR, settings, KNOWN_PROVIDERS
11REGISTRY_FILE = CONFIG_DIR / "providers.json"
12OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"
15def _now_iso() -> str:
16 return datetime.now(timezone.utc).isoformat()
19def load_registry() -> Dict[str, Any]:
20 if REGISTRY_FILE.exists():
21 try:
22 return json.loads(REGISTRY_FILE.read_text())
23 except Exception:
24 return {}
25 return {}
28def save_registry(data: Dict[str, Any]) -> None:
29 REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
30 REGISTRY_FILE.write_text(json.dumps(data, indent=2))
33def refresh_registry(fetch_openrouter: bool = True, provider: Optional[str] = None) -> Dict[str, Any]:
34 """
35 Refresh provider metadata (primarily models) and persist cache.
36 - OpenRouter: public model listing with pricing (no API key required)
37 - Others: use OpenAI-compatible /models with configured API key
38 """
39 registry = load_registry()
40 registry.setdefault("sources", {})
42 # OpenRouter public listing
43 if fetch_openrouter and (provider is None or provider == "openrouter"):
44 try:
45 with httpx.Client(timeout=15) as client:
46 resp = client.get(OPENROUTER_MODELS_URL)
47 resp.raise_for_status()
48 data = resp.json().get("data", [])
49 models = []
50 for item in data:
51 mid = item.get("id")
52 pricing = item.get("pricing", {})
53 models.append(
54 {
55 "id": mid,
56 "prompt": pricing.get("prompt"),
57 "completion": pricing.get("completion"),
58 "context_length": item.get("context_length"),
59 "tags": ["FREE"] if (mid and mid.endswith(":free")) or (pricing.get("prompt") == 0 and pricing.get("completion") == 0) else [],
60 }
61 )
62 registry["sources"]["openrouter"] = {
63 "fetched_at": _now_iso(),
64 "count": len(models),
65 "models": models,
66 }
67 except Exception as e:
68 registry["sources"]["openrouter"] = {
69 "fetched_at": _now_iso(),
70 "error": str(e),
71 }
73 # Specific provider refresh (OpenAI-compatible)
74 if provider and provider != "openrouter":
75 meta = KNOWN_PROVIDERS.get(provider)
76 if meta:
77 key_name = meta.get("api_key_name")
78 api_key = None
79 if key_name:
80 val = getattr(settings, key_name, None)
81 if val:
82 api_key = val.get_secret_value() if hasattr(val, "get_secret_value") else val
83 else:
84 api_key = "ollama"
85 try:
86 client = OpenAI(base_url=meta["base_url"], api_key=api_key or "missing")
87 models = client.models.list()
88 items = [{"id": m.id, "tags": []} for m in models.data]
89 registry["sources"][provider] = {
90 "fetched_at": _now_iso(),
91 "count": len(items),
92 "models": items,
93 }
94 except Exception as e:
95 registry["sources"][provider] = {
96 "fetched_at": _now_iso(),
97 "error": str(e),
98 }
100 registry["last_refreshed"] = _now_iso()
101 save_registry(registry)
102 return registry
106def sync_all_providers() -> Dict[str, Any]:
107 """Fetch and cache models for all known providers using configured keys."""
108 registry = load_registry()
109 for prov in KNOWN_PROVIDERS.keys():
110 try:
111 refresh_registry(provider=prov)
112 except Exception as e:
113 # Store per-provider error without failing the whole sync
114 if "sources" not in registry:
115 registry["sources"] = {}
116 registry["sources"][prov] = {"fetched_at": _now_iso(), "error": str(e)}
117 registry["last_synced"] = _now_iso()
118 save_registry(registry)
119 return registry
122def validate_model_cached(provider: str, model_id: str) -> Dict[str, Any]:
123 """Check whether a model id exists in cached registry for provider."""
124 cached = list_cached_models(provider)
125 models = cached.get("models") or []
126 present = False
127 for m in models:
128 mid = m.get("id") if isinstance(m, dict) else str(m)
129 if mid == model_id:
130 present = True
131 break
132 return {"provider": provider, "model": model_id, "present": present}
136def list_cached_models(source: str = "openrouter") -> Dict[str, Any]:
137 """Return cached models for a source if present."""
138 data = load_registry()
139 src = data.get("sources", {}).get(source)
140 if not src:
141 return {"source": source, "error": "no cached data"}
142 return {"source": source, "count": src.get("count", 0), "models": src.get("models", [])}
145def list_free_models(source: str = "openrouter") -> Dict[str, Any]:
146 """Return only models tagged FREE from the cache for a source."""
147 cached = list_cached_models(source)
148 models = cached.get("models") or []
149 free = [m for m in models if isinstance(m, dict) and "FREE" in (m.get("tags") or [])]
150 return {"source": source, "count": len(free), "models": free}
154def list_paid_models(source: str = "openrouter") -> Dict[str, Any]:
155 """Return only models not tagged FREE from the cache for a source."""
156 cached = list_cached_models(source)
157 models = cached.get("models") or []
158 paid = [m for m in models if not (isinstance(m, dict) and "FREE" in (m.get("tags") or []))]
159 return {"source": source, "count": len(paid), "models": paid}
162def search_cached_models(source: str, pattern: str) -> Dict[str, Any]:
163 """Search cached models by substring match on id."""
164 cached = list_cached_models(source)
165 models = cached.get("models") or []
166 pat = (pattern or "").lower()
167 matches = []
168 for m in models:
169 mid = m.get("id") if isinstance(m, dict) else str(m)
170 if pat in mid.lower():
171 matches.append(m)
172 return {"source": source, "count": len(matches), "models": matches}
175def health_check(provider: Optional[str] = None) -> Dict[str, Any]:
176 """
177 Quick health check by listing models via provider base_url + API key.
178 """
179 prov = provider or settings.provider
180 meta = KNOWN_PROVIDERS.get(prov)
181 if not meta:
182 return {"provider": prov, "status": "unknown-provider"}
184 key_name = meta.get("api_key_name")
185 api_key = None
186 if key_name:
187 val = getattr(settings, key_name, None)
188 if val:
189 api_key = val.get_secret_value() if hasattr(val, "get_secret_value") else val
190 else:
191 api_key = "ollama"
193 client = OpenAI(base_url=meta["base_url"], api_key=api_key)
194 try:
195 models = client.models.list()
196 return {
197 "provider": prov,
198 "status": "ok",
199 "model_count": len(models.data),
200 }
201 except Exception as e:
202 return {"provider": prov, "status": "error", "error": str(e)}