Add per-job run logs and separate vision/text model selection.
Docker Release / build-and-push (push) Successful in 1m27s
Docker Release / release (push) Skipped

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:
2026-07-31 14:56:48 -05:00
co-authored by Cursor
parent e30522af9a
commit a6b0c8fdfa
8 changed files with 586 additions and 44 deletions
+101
View File
@@ -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