feat(engines): list LiteLLM router aliases in the model dropdown

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-20 16:23:25 +02:00
parent 5852d7a2d5
commit 46967ade68
3 changed files with 63 additions and 1 deletions

View File

@ -7,6 +7,9 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased]
### Added
- **LiteLLM model-group aliases in the model list.** The Active Language Model / OCR model dropdown now also lists the gateway's router aliases (ocr, default, cheap, vision, local-stt …), which LiteLLM's `/models` omits; they are read from `/router/settings` and shown first. Gateways without that endpoint are unaffected.
### Changed
- **Searchable model dropdown** for the Active Language Model and Active OCR Model pickers (Engines page): the long native model list is now the same type-to-search dropdown used for voices, with model-specific wording and no avatars.

View File

@ -5030,6 +5030,28 @@ async def mcp_sse(request: Request):
# ── Conversation playground ───────────────────────────────────────────────────
def _litellm_model_aliases(base: str, key: str) -> list[str]:
"""Names of a LiteLLM gateway's router aliases (Model Group Alias), which its /models list omits.
Best effort: the settings endpoint needs an admin-capable key, and other gateways do not have it.
"""
root = base[:-3] if base.endswith("/v1") else base
try:
r = requests.get(f"{root}/router/settings", timeout=5, headers={"Authorization": f"Bearer {key}"})
if r.status_code != 200:
return []
data = r.json()
except Exception:
return []
if not isinstance(data, dict):
return []
for holder in (data.get("current_values"), data.get("router_settings"), data):
aliases = holder.get("model_group_alias") if isinstance(holder, dict) else None
if isinstance(aliases, dict):
return [str(name) for name in aliases if name]
return []
@router.get("/api/conversation/llm-models")
async def conversation_llm_models(
url: str = "",
@ -5058,7 +5080,8 @@ async def conversation_llm_models(
str(item["id"]) if isinstance(item, dict) and item.get("id") else str(item)
for item in data if item
]
return {"models": models, "url": base}
aliases = [a for a in _litellm_model_aliases(base, key_to_use) if a not in models]
return {"models": aliases + models, "url": base, "aliases": aliases}
else:
return {"models": [], "url": base, "error": r.text}
except Exception as e:

View File

@ -290,3 +290,39 @@ class CharacterSheetsEmptyAnswerTests(unittest.TestCase):
with mock.patch.object(conv, "_post_llm_chat_completion", return_value=self._resp(good)) as post:
client.post("/api/character-sheets", json={"text": "A kam.", "known_characters": ["A"]})
self.assertEqual(post.call_count, 1)
class LlmModelAliasTests(unittest.TestCase):
def _get(self, responses):
from unittest import mock
from fastapi import FastAPI
from fastapi.testclient import TestClient
import routes.conversation as conv
app = FastAPI(); app.include_router(conv.router)
def fake_get(url, **kw):
r = mock.Mock()
status, body = responses.get(url.split("://", 1)[1].split("/", 1)[1], (404, {}))
r.status_code = status; r.json.return_value = body; r.text = str(body)
return r
with mock.patch.object(conv.requests, "get", side_effect=fake_get):
return TestClient(app).get("/api/conversation/llm-models", params={"url": "http://192.168.1.5:4000"}).json()
def test_router_aliases_are_listed_before_the_models(self):
out = self._get({
"models": (200, {"data": [{"id": "gpt-x"}]}),
"router/settings": (200, {"current_values": {"model_group_alias": {"ocr": "a", "default": "b"}}}),
})
self.assertEqual(out["models"], ["ocr", "default", "gpt-x"])
self.assertEqual(out["aliases"], ["ocr", "default"])
def test_a_gateway_without_alias_settings_still_lists_its_models(self):
out = self._get({"models": (200, {"data": [{"id": "m1"}, {"id": "m2"}]})})
self.assertEqual(out["models"], ["m1", "m2"])
def test_an_alias_that_is_already_a_model_is_not_repeated(self):
out = self._get({
"models": (200, {"data": [{"id": "reasoning"}]}),
"router/settings": (200, {"model_group_alias": {"reasoning": "x", "cheap": "y"}}),
})
self.assertEqual(out["models"], ["cheap", "reasoning"])