diff --git a/custom_components/openai_compatible/config_flow.py b/custom_components/openai_compatible/config_flow.py index cd91d94..1bf7525 100644 --- a/custom_components/openai_compatible/config_flow.py +++ b/custom_components/openai_compatible/config_flow.py @@ -708,9 +708,11 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow): try: response = await client.responses.create( model=self.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), + # A user message, not a system one: endpoints in front of + # Anthropic models reject a request without user content. input=[ { - "role": "system", + "role": "user", "content": "Where are the following coordinates located: " f"({zone_home.attributes[EntityStateAttribute.LATITUDE]}," f" {zone_home.attributes[EntityStateAttribute.LONGITUDE]})?", diff --git a/custom_components/openai_compatible/entity.py b/custom_components/openai_compatible/entity.py index 2f86269..bfbf344 100644 --- a/custom_components/openai_compatible/entity.py +++ b/custom_components/openai_compatible/entity.py @@ -560,12 +560,20 @@ class OpenAIBaseLLMEntity(Entity): ), ) if options.get(CONF_WEB_SEARCH_USER_LOCATION): + # Leave out unknown fields: some endpoints reject empty strings, + # and the location lookup may have found no city or region. + location = { + key: value + for key, option in ( + ("city", CONF_WEB_SEARCH_CITY), + ("region", CONF_WEB_SEARCH_REGION), + ("country", CONF_WEB_SEARCH_COUNTRY), + ("timezone", CONF_WEB_SEARCH_TIMEZONE), + ) + if (value := options.get(option)) + } web_search["user_location"] = UserLocation( - type="approximate", - city=options.get(CONF_WEB_SEARCH_CITY, ""), - region=options.get(CONF_WEB_SEARCH_REGION, ""), - country=options.get(CONF_WEB_SEARCH_COUNTRY, ""), - timezone=options.get(CONF_WEB_SEARCH_TIMEZONE, ""), + type="approximate", **location ) if not options.get( CONF_WEB_SEARCH_INLINE_CITATIONS, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 4bd1ffc..949c6db 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1334,6 +1334,7 @@ async def test_subentry_web_search_user_location_lookup_fails( ) await hass.async_block_till_done() assert mock_create.call_args.kwargs["model"] == "claude-sonnet-5-5" + assert mock_create.call_args.kwargs["input"][0]["role"] == "user" assert subentry_flow["type"] is FlowResultType.ABORT assert subentry_flow["reason"] == "reconfigure_successful" assert CONF_WEB_SEARCH_CITY not in subentry.data diff --git a/tests/test_entity.py b/tests/test_entity.py index c3b1673..7a8a718 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -3,14 +3,21 @@ from unittest.mock import MagicMock from homeassistant.components import conversation +from homeassistant.core import Context from homeassistant.helpers import llm +from custom_components.openai_compatible.const import ( + CONF_WEB_SEARCH, + CONF_WEB_SEARCH_COUNTRY, + CONF_WEB_SEARCH_TIMEZONE, + CONF_WEB_SEARCH_USER_LOCATION, +) from custom_components.openai_compatible.entity import ( _convert_content_to_param, _transform_stream, ) -from . import create_function_tool_call_item +from . import create_function_tool_call_item, create_message_item async def _stream(events): @@ -90,3 +97,41 @@ def test_web_search_not_replayed() -> None: ] assert messages[1]["content"] == "It ended 2 to 2." assert messages[2]["call_id"] == "call_1" + + +async def test_web_search_location_leaves_out_empty_fields( + hass, mock_config_entry, mock_init_component, mock_create_stream +) -> None: + """Test unknown location fields are left out of the web search tool. + + A failed location lookup stores no city or region; endpoints in front of + Anthropic models reject empty strings there. + """ + subentry = next(iter(mock_config_entry.subentries.values())) + hass.config_entries.async_update_subentry( + mock_config_entry, + subentry, + data={ + **subentry.data, + CONF_WEB_SEARCH: True, + CONF_WEB_SEARCH_USER_LOCATION: True, + CONF_WEB_SEARCH_COUNTRY: "DE", + CONF_WEB_SEARCH_TIMEZONE: "Europe/Berlin", + }, + ) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + mock_create_stream.return_value = [ + create_message_item(id="msg_A", text="Hi", output_index=0) + ] + + await conversation.async_converse( + hass, "hello", None, Context(), agent_id="conversation.openai_conversation" + ) + + tools = mock_create_stream.mock_calls[0][2]["tools"] + web_search = next(tool for tool in tools if tool["type"] == "web_search") + assert web_search["user_location"] == { + "type": "approximate", + "country": "DE", + "timezone": "Europe/Berlin", + }