Add per-job run logs and separate vision/text model selection.
Capture pipeline stdout into job.log + API/UI so failed runs can be reviewed, and let users pick OpenRouter vision vs text models independently. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
models_catalog.py - OpenRouter model list for the UI dropdowns.
|
||||
|
||||
Fetches https://openrouter.ai/api/v1/models (cached ~1h) and splits into:
|
||||
- vision: accepts image input and returns text
|
||||
- text: chat models that return text (may also be multimodal)
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from backend import config
|
||||
|
||||
_TTL_SEC = 3600
|
||||
_cache: Dict[str, Any] = {"at": 0.0, "payload": None}
|
||||
|
||||
|
||||
def _entry(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
return {
|
||||
"id": m.get("id") or "",
|
||||
"name": m.get("name") or m.get("id") or "",
|
||||
}
|
||||
|
||||
|
||||
def _ensure_default(items: List[Dict[str, str]], model_id: str) -> List[Dict[str, str]]:
|
||||
"""Prepend the configured default if OpenRouter didn't return it."""
|
||||
if not model_id:
|
||||
return items
|
||||
if any(x["id"] == model_id for x in items):
|
||||
return items
|
||||
return [{"id": model_id, "name": model_id}] + items
|
||||
|
||||
|
||||
def _fetch_raw() -> List[Dict[str, Any]]:
|
||||
import httpx # local import so the app can start without httpx in odd envs
|
||||
headers = {"Accept": "application/json"}
|
||||
if config.AI_API_KEY:
|
||||
headers["Authorization"] = f"Bearer {config.AI_API_KEY}"
|
||||
url = f"{config.AI_BASE_URL.rstrip('/')}/models"
|
||||
# Ask for text-output chat models (includes multimodal). "all" is huge.
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
r = client.get(url, headers=headers, params={"output_modalities": "text"})
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
return data.get("data") or []
|
||||
|
||||
|
||||
def list_models() -> Dict[str, Any]:
|
||||
"""Return {vision, text, defaults} for the frontend selects."""
|
||||
now = time.time()
|
||||
if _cache["payload"] and (now - _cache["at"]) < _TTL_SEC:
|
||||
return _cache["payload"]
|
||||
|
||||
try:
|
||||
raw = _fetch_raw()
|
||||
except Exception as e:
|
||||
# Degrade to configured defaults so the UI still works offline.
|
||||
print(f"[Models] OpenRouter catalog fetch failed: {e}")
|
||||
vision = _ensure_default([], config.MODEL)
|
||||
text = _ensure_default([], config.TEXT_MODEL)
|
||||
payload = {
|
||||
"vision": vision,
|
||||
"text": text,
|
||||
"defaults": {"vision": config.MODEL, "text": config.TEXT_MODEL},
|
||||
"error": str(e),
|
||||
}
|
||||
return payload
|
||||
|
||||
vision: List[Dict[str, str]] = []
|
||||
text: List[Dict[str, str]] = []
|
||||
for m in raw:
|
||||
mid = m.get("id") or ""
|
||||
if not mid:
|
||||
continue
|
||||
arch = m.get("architecture") or {}
|
||||
inputs = arch.get("input_modalities") or []
|
||||
outputs = arch.get("output_modalities") or []
|
||||
# Legacy string form: "text+image->text"
|
||||
modality = (arch.get("modality") or "").lower()
|
||||
has_image_in = ("image" in inputs) or ("image" in modality.split("->")[0])
|
||||
has_text_out = ("text" in outputs) or ("->text" in modality) or (not outputs and not modality)
|
||||
has_text_in = ("text" in inputs) or ("text" in modality) or not inputs
|
||||
|
||||
if has_image_in and has_text_out:
|
||||
vision.append(_entry(m))
|
||||
if has_text_in and has_text_out:
|
||||
text.append(_entry(m))
|
||||
|
||||
vision.sort(key=lambda x: x["name"].lower())
|
||||
text.sort(key=lambda x: x["name"].lower())
|
||||
vision = _ensure_default(vision, config.MODEL)
|
||||
text = _ensure_default(text, config.TEXT_MODEL)
|
||||
|
||||
payload = {
|
||||
"vision": vision,
|
||||
"text": text,
|
||||
"defaults": {"vision": config.MODEL, "text": config.TEXT_MODEL},
|
||||
}
|
||||
_cache["at"] = now
|
||||
_cache["payload"] = payload
|
||||
return payload
|
||||
Reference in New Issue
Block a user