Add websocket command to list LLM APIs (#177903)

This commit is contained in:
Magnus Øverli
2026-08-04 08:41:06 -04:00
committed by GitHub
parent 88c77c74ab
commit bb113f832c
5 changed files with 121 additions and 0 deletions
+1
View File
@@ -354,6 +354,7 @@ homeassistant.components.litejet.*
homeassistant.components.litellm.*
homeassistant.components.litterrobot.*
homeassistant.components.llama_cpp.*
homeassistant.components.llm.*
homeassistant.components.local_ip.*
homeassistant.components.local_todo.*
homeassistant.components.lock.*
+2
View File
@@ -20,6 +20,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
from .websocket_api import async_setup as async_setup_ws_api
_LOGGER = logging.getLogger(__name__)
@@ -57,6 +58,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, DOMAIN, _process_llm_tools_platform
)
async_register_api(hass, AssistAPI(hass))
async_setup_ws_api(hass)
return True
@@ -0,0 +1,34 @@
"""Websocket API for the LLM integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.llm import async_get_apis
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the LLM websocket API."""
websocket_api.async_register_command(hass, websocket_list_apis)
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "llm/api/list"})
@callback
def websocket_list_apis(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""List the registered LLM APIs.
Each API is described by the ID used to select it and the name shown to
the user. APIs are listed in registration order.
"""
connection.send_result(
msg["id"],
{"apis": [{"id": api.id, "name": api.name} for api in async_get_apis(hass)]},
)
Generated
+10
View File
@@ -3297,6 +3297,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.llm.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.local_ip.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -0,0 +1,74 @@
"""Tests for the LLM integration websocket API."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm
from homeassistant.setup import async_setup_component
from tests.typing import WebSocketGenerator
class _StubAPI(llm.API):
"""Minimal LLM API used to populate the registry."""
async def async_get_api_instance(
self, llm_context: llm.LLMContext
) -> llm.APIInstance:
"""Return the instance of the API."""
return llm.APIInstance(
api=self, api_prompt="", llm_context=llm_context, tools=[]
)
@pytest.fixture(autouse=True)
async def setup_llm(hass: HomeAssistant) -> None:
"""Set up the LLM integration."""
assert await async_setup_component(hass, "llm", {})
@pytest.mark.parametrize(
("registered_apis", "expected_apis"),
[
pytest.param([], [{"id": "assist", "name": "Assist"}], id="assist_only"),
pytest.param(
[("test-api", "Test API")],
[
{"id": "assist", "name": "Assist"},
{"id": "test-api", "name": "Test API"},
],
id="registered_api",
),
],
)
async def test_list_apis(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
registered_apis: list[tuple[str, str]],
expected_apis: list[dict[str, str]],
) -> None:
"""Test listing the registered LLM APIs."""
for api_id, name in registered_apis:
llm.async_register_api(hass, _StubAPI(hass=hass, id=api_id, name=name))
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "llm/api/list"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"apis": expected_apis}
async def test_list_apis_requires_admin(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
hass_read_only_access_token: str,
) -> None:
"""Test listing the LLM APIs is only allowed for admins."""
client = await hass_ws_client(hass, hass_read_only_access_token)
await client.send_json_auto_id({"type": "llm/api/list"})
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "unauthorized"