fix: make the home location lookup work against Claude
validate / hassfest (push) Failing after 11s
validate / hacs (push) Failing after 23s
test / pytest (push) Successful in 46s

- Send the location question as a user message. A request with only a
  system message has no user content, which endpoints in front of Anthropic
  models reject ("text content blocks must be non-empty").
- Leave unknown fields out of the web search user_location. After a failed
  lookup there is no city or region, and the empty strings made every
  request fail ("user_location.city: String should have at least 1
  character").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pc-heiniandClaude Opus 5.5 committed 2026-10-22 08:52:14 +02:00
1 parent 235c536cab
commit 84f4bcda55
4 files changed
+63 -7

No files matched your search

@@ -708,9 +708,11 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
try: try:
response = await client.responses.create( response = await client.responses.create(
model=self.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), 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=[ input=[
{ {
"role": "system", "role": "user",
"content": "Where are the following coordinates located: " "content": "Where are the following coordinates located: "
f"({zone_home.attributes[EntityStateAttribute.LATITUDE]}," f"({zone_home.attributes[EntityStateAttribute.LATITUDE]},"
f" {zone_home.attributes[EntityStateAttribute.LONGITUDE]})?", f" {zone_home.attributes[EntityStateAttribute.LONGITUDE]})?",
+13 -5
View File
@@ -560,12 +560,20 @@ class OpenAIBaseLLMEntity(Entity):
), ),
) )
if options.get(CONF_WEB_SEARCH_USER_LOCATION): 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( web_search["user_location"] = UserLocation(
type="approximate", type="approximate", **location
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, ""),
) )
if not options.get( if not options.get(
CONF_WEB_SEARCH_INLINE_CITATIONS, CONF_WEB_SEARCH_INLINE_CITATIONS,
+1
View File
@@ -1334,6 +1334,7 @@ async def test_subentry_web_search_user_location_lookup_fails(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_create.call_args.kwargs["model"] == "claude-sonnet-5-5" 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["type"] is FlowResultType.ABORT
assert subentry_flow["reason"] == "reconfigure_successful" assert subentry_flow["reason"] == "reconfigure_successful"
assert CONF_WEB_SEARCH_CITY not in subentry.data assert CONF_WEB_SEARCH_CITY not in subentry.data
+46 -1
View File
@@ -3,14 +3,21 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from homeassistant.components import conversation from homeassistant.components import conversation
from homeassistant.core import Context
from homeassistant.helpers import llm 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 ( from custom_components.openai_compatible.entity import (
_convert_content_to_param, _convert_content_to_param,
_transform_stream, _transform_stream,
) )
from . import create_function_tool_call_item from . import create_function_tool_call_item, create_message_item
async def _stream(events): 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[1]["content"] == "It ended 2 to 2."
assert messages[2]["call_id"] == "call_1" 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",
}