|
97 | 97 | _LLM_PROVIDER_ANTHROPIC: "claude-opus-4-6", |
98 | 98 | } |
99 | 99 |
|
| 100 | +# HF Inference Providers catalogue. The public router /v1/models endpoint |
| 101 | +# lists every model the HF Router can serve along with per-provider tool |
| 102 | +# support; we surface the tool-capable subset as a dropdown on the submit / |
| 103 | +# admin forms so users don't have to hand-type model ids. Cached in-process |
| 104 | +# (the list is large and changes slowly) and refreshed lazily on expiry. |
| 105 | +_HF_MODELS_URL = f"{_LLM_PROVIDER_BASES[_LLM_PROVIDER_HF]}/models" |
| 106 | +_HF_MODELS_TTL_SECONDS = 15 * 60 |
| 107 | +_hf_models_lock = threading.Lock() |
| 108 | +_hf_models_cache: dict[str, Any] = {"fetched_at": 0.0, "models": []} |
| 109 | + |
100 | 110 |
|
101 | 111 | # --------------------------------------------------------------------------- |
102 | 112 | # Session handling: signed cookies via itsdangerous (no DB). |
@@ -1343,6 +1353,72 @@ def _provider_entry(pid: str, label: str, base_url: str) -> dict: |
1343 | 1353 | ) |
1344 | 1354 |
|
1345 | 1355 |
|
| 1356 | +def _tool_capable_hf_models(payload: Any) -> list[str]: |
| 1357 | + """Extract the ids of HF Router models that can drive an agentic |
| 1358 | + review — at least one ``live`` provider advertising ``supports_tools``. |
| 1359 | + Serge's review loop relies on browse tools, so models that can't call |
| 1360 | + tools would degrade silently; we keep them out of the dropdown. Sorted |
| 1361 | + case-insensitively and de-duplicated.""" |
| 1362 | + data = payload.get("data") if isinstance(payload, dict) else None |
| 1363 | + if not isinstance(data, list): |
| 1364 | + return [] |
| 1365 | + ids: set[str] = set() |
| 1366 | + for entry in data: |
| 1367 | + if not isinstance(entry, dict): |
| 1368 | + continue |
| 1369 | + model_id = entry.get("id") |
| 1370 | + if not isinstance(model_id, str) or not model_id: |
| 1371 | + continue |
| 1372 | + providers = entry.get("providers") |
| 1373 | + if not isinstance(providers, list): |
| 1374 | + continue |
| 1375 | + if any( |
| 1376 | + isinstance(p, dict) |
| 1377 | + and p.get("status") == "live" |
| 1378 | + and p.get("supports_tools") is True |
| 1379 | + for p in providers |
| 1380 | + ): |
| 1381 | + ids.add(model_id) |
| 1382 | + return sorted(ids, key=str.lower) |
| 1383 | + |
| 1384 | + |
| 1385 | +async def _fetch_hf_router_models() -> list[str]: |
| 1386 | + """Tool-capable HF Router model ids, cached for ``_HF_MODELS_TTL_SECONDS``. |
| 1387 | + On a fetch error the last good list (possibly empty) is returned so a |
| 1388 | + transient router blip doesn't blank out the dropdown — the form falls |
| 1389 | + back to a free-text box when the list is empty.""" |
| 1390 | + now = time.monotonic() |
| 1391 | + with _hf_models_lock: |
| 1392 | + # Use fetched_at (0.0 until the first successful fetch) rather than |
| 1393 | + # the list's truthiness to gauge freshness — a valid fetch that |
| 1394 | + # yields no tool-capable models is still a fetch worth caching, and |
| 1395 | + # keying on truthiness would re-hit the router on every call. |
| 1396 | + fetched_at = _hf_models_cache["fetched_at"] |
| 1397 | + if fetched_at > 0 and now - fetched_at < _HF_MODELS_TTL_SECONDS: |
| 1398 | + return _hf_models_cache["models"] |
| 1399 | + try: |
| 1400 | + async with httpx.AsyncClient(timeout=10) as client: |
| 1401 | + resp = await client.get(_HF_MODELS_URL) |
| 1402 | + resp.raise_for_status() |
| 1403 | + models = _tool_capable_hf_models(resp.json()) |
| 1404 | + except (httpx.HTTPError, ValueError) as exc: |
| 1405 | + log.warning("Failed to fetch HF router models from %s: %s", _HF_MODELS_URL, exc) |
| 1406 | + with _hf_models_lock: |
| 1407 | + return _hf_models_cache["models"] |
| 1408 | + with _hf_models_lock: |
| 1409 | + _hf_models_cache["models"] = models |
| 1410 | + _hf_models_cache["fetched_at"] = now |
| 1411 | + return models |
| 1412 | + |
| 1413 | + |
| 1414 | +@app.get("/llm-options/hf-models") |
| 1415 | +async def hf_models(request: Request) -> JSONResponse: |
| 1416 | + """Tool-capable models served by the HF Router, for the provider |
| 1417 | + dropdown that appears when ``hf`` is the selected provider.""" |
| 1418 | + _require_user(request) |
| 1419 | + return JSONResponse({"models": await _fetch_hf_router_models()}) |
| 1420 | + |
| 1421 | + |
1346 | 1422 | @app.get("/reviews/lookup-provider") |
1347 | 1423 | def lookup_provider(request: Request, owner: str, repo: str) -> JSONResponse: |
1348 | 1424 | """Pre-flight match for the submit form: given a (owner, repo) |
|
0 commit comments