feat: configurable base_url threaded to all three client sites
Adds CONF_BASE_URL/DEFAULT_BASE_URL and threads base_url through every AsyncOpenAI construction site: config_flow.validate_input, __init__.async_setup_entry, and config_flow's web-search geolocation helper (_get_location_data) — the easy one to miss, since it would otherwise silently talk to api.openai.com when the user configured a different endpoint. Fixes upstream-ported fixtures (conftest.mock_config_entry, test_config_flow's inline MockConfigEntry/data-schema assertions) to supply base_url now that the config-flow schema requires it. No translations/en.json exists in this repo (only strings.json), so only strings.json got the new "base_url" UI strings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
This commit is contained in:
1 parent
8d2326d435
commit
564c52159f
7 files changed
+50
-4
No files matched your search
@@ -39,6 +39,7 @@ from homeassistant.helpers.httpx_client import get_async_client
|
|||||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_BASE_URL,
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
CONF_FILENAMES,
|
CONF_FILENAMES,
|
||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
@@ -282,6 +283,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bo
|
|||||||
"""Set up OpenAI Conversation from a config entry."""
|
"""Set up OpenAI Conversation from a config entry."""
|
||||||
client = openai.AsyncOpenAI(
|
client = openai.AsyncOpenAI(
|
||||||
api_key=entry.data[CONF_API_KEY],
|
api_key=entry.data[CONF_API_KEY],
|
||||||
|
base_url=entry.data[CONF_BASE_URL],
|
||||||
http_client=get_async_client(hass),
|
http_client=get_async_client(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from homeassistant.helpers.selector import (
|
|||||||
from homeassistant.helpers.typing import VolDictType
|
from homeassistant.helpers.typing import VolDictType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_BASE_URL,
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
CONF_CODE_INTERPRETER,
|
CONF_CODE_INTERPRETER,
|
||||||
CONF_IMAGE_MODEL,
|
CONF_IMAGE_MODEL,
|
||||||
@@ -67,6 +68,7 @@ from .const import (
|
|||||||
CONF_WEB_SEARCH_TIMEZONE,
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
CONF_WEB_SEARCH_USER_LOCATION,
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
DEFAULT_AI_TASK_NAME,
|
DEFAULT_AI_TASK_NAME,
|
||||||
|
DEFAULT_BASE_URL,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_STT_NAME,
|
DEFAULT_STT_NAME,
|
||||||
DEFAULT_STT_PROMPT,
|
DEFAULT_STT_PROMPT,
|
||||||
@@ -106,6 +108,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Required(CONF_BASE_URL, default=DEFAULT_BASE_URL): str,
|
||||||
vol.Required(CONF_API_KEY): str,
|
vol.Required(CONF_API_KEY): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -117,7 +120,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
|||||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
"""
|
"""
|
||||||
client = openai.AsyncOpenAI(
|
client = openai.AsyncOpenAI(
|
||||||
api_key=data[CONF_API_KEY], http_client=get_async_client(hass)
|
api_key=data[CONF_API_KEY],
|
||||||
|
base_url=data[CONF_BASE_URL],
|
||||||
|
http_client=get_async_client(hass),
|
||||||
)
|
)
|
||||||
await client.models.list(timeout=10.0)
|
await client.models.list(timeout=10.0)
|
||||||
|
|
||||||
@@ -646,6 +651,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
if zone_home is not None:
|
if zone_home is not None:
|
||||||
client = openai.AsyncOpenAI(
|
client = openai.AsyncOpenAI(
|
||||||
api_key=self._get_entry().data[CONF_API_KEY],
|
api_key=self._get_entry().data[CONF_API_KEY],
|
||||||
|
base_url=self._get_entry().data[CONF_BASE_URL],
|
||||||
http_client=get_async_client(self.hass),
|
http_client=get_async_client(self.hass),
|
||||||
)
|
)
|
||||||
location_schema = vol.Schema(
|
location_schema = vol.Schema(
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ from homeassistant.helpers import llm
|
|||||||
DOMAIN = "openai_compatible"
|
DOMAIN = "openai_compatible"
|
||||||
LOGGER: logging.Logger = logging.getLogger(__package__)
|
LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
CONF_BASE_URL = "base_url"
|
||||||
|
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
|
||||||
DEFAULT_CONVERSATION_NAME = "OpenAI Conversation"
|
DEFAULT_CONVERSATION_NAME = "OpenAI Conversation"
|
||||||
DEFAULT_AI_TASK_NAME = "OpenAI AI Task"
|
DEFAULT_AI_TASK_NAME = "OpenAI AI Task"
|
||||||
DEFAULT_STT_NAME = "OpenAI STT"
|
DEFAULT_STT_NAME = "OpenAI STT"
|
||||||
|
|||||||
@@ -21,10 +21,12 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
|
"base_url": "API base URL"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"api_key": "Your OpenAI API key."
|
"api_key": "Your OpenAI API key.",
|
||||||
|
"base_url": "The OpenAI-compatible endpoint, e.g. http://192.0.2.10:4000/v1 for LiteLLM. Must support the Responses API."
|
||||||
},
|
},
|
||||||
"description": "Set up OpenAI Conversation integration by providing your OpenAI API key. Instructions to obtain an API key can be found [here]({instructions_url})."
|
"description": "Set up OpenAI Conversation integration by providing your OpenAI API key. Instructions to obtain an API key can be found [here]({instructions_url})."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ from openai.types.responses.response import IncompleteDetails
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.openai_compatible.const import (
|
from custom_components.openai_compatible.const import (
|
||||||
|
CONF_BASE_URL,
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
DEFAULT_AI_TASK_NAME,
|
DEFAULT_AI_TASK_NAME,
|
||||||
|
DEFAULT_BASE_URL,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_STT_NAME,
|
DEFAULT_STT_NAME,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
@@ -65,6 +67,7 @@ def mock_config_entry(
|
|||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={
|
data={
|
||||||
"api_key": "bla",
|
"api_key": "bla",
|
||||||
|
CONF_BASE_URL: DEFAULT_BASE_URL,
|
||||||
},
|
},
|
||||||
version=2,
|
version=2,
|
||||||
minor_version=7,
|
minor_version=7,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""base_url must reach the OpenAI client, not just the config entry."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from custom_components.openai_compatible.const import CONF_BASE_URL, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_client_built_with_base_url(hass: HomeAssistant) -> None:
|
||||||
|
"""async_setup_entry must pass base_url to AsyncOpenAI."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={"api_key": "sk-test", CONF_BASE_URL: "http://192.0.2.10:4000/v1"},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.openai_compatible.openai.AsyncOpenAI"
|
||||||
|
) as mock_client:
|
||||||
|
mock_client.return_value.platform_headers = lambda: {}
|
||||||
|
mock_client.return_value.with_options.return_value.models.list = AsyncMock()
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_client.call_args.kwargs["base_url"] == "http://192.0.2.10:4000/v1"
|
||||||
@@ -12,6 +12,7 @@ from custom_components.openai_compatible.config_flow import (
|
|||||||
RECOMMENDED_CONVERSATION_OPTIONS,
|
RECOMMENDED_CONVERSATION_OPTIONS,
|
||||||
)
|
)
|
||||||
from custom_components.openai_compatible.const import (
|
from custom_components.openai_compatible.const import (
|
||||||
|
CONF_BASE_URL,
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
CONF_CODE_INTERPRETER,
|
CONF_CODE_INTERPRETER,
|
||||||
CONF_IMAGE_MODEL,
|
CONF_IMAGE_MODEL,
|
||||||
@@ -35,6 +36,7 @@ from custom_components.openai_compatible.const import (
|
|||||||
CONF_WEB_SEARCH_TIMEZONE,
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
CONF_WEB_SEARCH_USER_LOCATION,
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
DEFAULT_AI_TASK_NAME,
|
DEFAULT_AI_TASK_NAME,
|
||||||
|
DEFAULT_BASE_URL,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_STT_NAME,
|
DEFAULT_STT_NAME,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
@@ -90,6 +92,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"api_key": "bla",
|
"api_key": "bla",
|
||||||
|
"base_url": DEFAULT_BASE_URL,
|
||||||
}
|
}
|
||||||
assert result2["options"] == {}
|
assert result2["options"] == {}
|
||||||
assert result2["subentries"] == [
|
assert result2["subentries"] == [
|
||||||
@@ -127,7 +130,7 @@ async def test_duplicate_entry(hass: HomeAssistant) -> None:
|
|||||||
"""Test we abort on duplicate config entry."""
|
"""Test we abort on duplicate config entry."""
|
||||||
MockConfigEntry(
|
MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={CONF_API_KEY: "bla"},
|
data={CONF_API_KEY: "bla", CONF_BASE_URL: DEFAULT_BASE_URL},
|
||||||
).add_to_hass(hass)
|
).add_to_hass(hass)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
|||||||
Reference in new issue
Block a user