feat(engines): list LiteLLM router aliases in the model dropdown
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
5852d7a2d5
commit
46967ade68
@ -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.
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user