fix: make the integration work against a Claude proxy
- Treat an empty tool argument string as no parameters instead of raising JSONDecodeError; the proxy sends "" for tools like the timer status (upstream issue #3). - Do not replay earlier web searches in the history. The chat log keeps only their status, and a search without results makes the model disown the answers it based on it and search again (upstream issue #4). - Look up the home location for web search with the subentry's model instead of gpt-4o-mini, and save the options without city/region if the lookup fails (upstream issue #5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8737e5d4ec
commit
235c536cab
6 files changed
+224
-70
No files matched your search
@@ -1274,6 +1274,74 @@ async def test_subentry_web_search_user_location(
|
||||
}
|
||||
|
||||
|
||||
async def test_subentry_web_search_user_location_lookup_fails(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry,
|
||||
mock_init_component,
|
||||
) -> None:
|
||||
"""Test the location lookup uses the subentry's model and may fail.
|
||||
|
||||
An endpoint that only serves other providers' models rejects the OpenAI
|
||||
default; saving must still succeed with country and time zone only.
|
||||
"""
|
||||
subentry = next(
|
||||
sub
|
||||
for sub in mock_config_entry.subentries.values()
|
||||
if sub.subentry_type == "conversation"
|
||||
)
|
||||
subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_CHAT_MODEL: "claude-sonnet-5-5",
|
||||
},
|
||||
)
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert subentry_flow["step_id"] == "model"
|
||||
|
||||
hass.config.country = "DE"
|
||||
hass.config.time_zone = "Europe/Berlin"
|
||||
hass.states.async_set("zone.home", "0", {"latitude": 50.9, "longitude": 12.1})
|
||||
with patch(
|
||||
"openai.resources.responses.AsyncResponses.create",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=BadRequestError(
|
||||
response=httpx.Response(status_code=400, request=httpx.Request("POST", "")),
|
||||
body=None,
|
||||
message="unknown provider for model",
|
||||
),
|
||||
) as mock_create:
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "medium",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: True,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_create.call_args.kwargs["model"] == "claude-sonnet-5-5"
|
||||
assert subentry_flow["type"] is FlowResultType.ABORT
|
||||
assert subentry_flow["reason"] == "reconfigure_successful"
|
||||
assert CONF_WEB_SEARCH_CITY not in subentry.data
|
||||
assert CONF_WEB_SEARCH_REGION not in subentry.data
|
||||
assert subentry.data[CONF_WEB_SEARCH_COUNTRY] == "DE"
|
||||
assert subentry.data[CONF_WEB_SEARCH_TIMEZONE] == "Europe/Berlin"
|
||||
|
||||
|
||||
async def test_creating_ai_task_subentry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -231,19 +231,6 @@ async def test_conversation_agent(
|
||||
@pytest.mark.parametrize(
|
||||
("description", "messages"),
|
||||
[
|
||||
(
|
||||
"Test function call started with missing arguments",
|
||||
(
|
||||
*create_function_tool_call_item(
|
||||
id="fc_1",
|
||||
arguments=[],
|
||||
call_id="call_call_1",
|
||||
name="test_tool",
|
||||
output_index=0,
|
||||
),
|
||||
*create_message_item(id="msg_A", text="Cool", output_index=1),
|
||||
),
|
||||
),
|
||||
(
|
||||
"Test invalid JSON",
|
||||
(
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Tests for the fork's chat log conversion fixes in entity.py."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.helpers import llm
|
||||
|
||||
from custom_components.openai_compatible.entity import (
|
||||
_convert_content_to_param,
|
||||
_transform_stream,
|
||||
)
|
||||
|
||||
from . import create_function_tool_call_item
|
||||
|
||||
|
||||
async def _stream(events):
|
||||
for event in events:
|
||||
yield event
|
||||
|
||||
|
||||
async def test_tool_call_without_arguments() -> None:
|
||||
"""Test an empty argument string means a call without parameters.
|
||||
|
||||
Some OpenAI-compatible endpoints send "" instead of "{}" for tools that
|
||||
take no parameters, e.g. the timer status intent.
|
||||
"""
|
||||
events = create_function_tool_call_item(
|
||||
id="fc_1",
|
||||
arguments=[],
|
||||
call_id="call_1",
|
||||
name="HassTimerStatus",
|
||||
output_index=0,
|
||||
)
|
||||
|
||||
deltas = [delta async for delta in _transform_stream(MagicMock(), _stream(events))]
|
||||
|
||||
tool_calls = [call for delta in deltas for call in delta.get("tool_calls", [])]
|
||||
assert len(tool_calls) == 1
|
||||
assert tool_calls[0].tool_name == "HassTimerStatus"
|
||||
assert tool_calls[0].tool_args == {}
|
||||
|
||||
|
||||
def test_web_search_not_replayed() -> None:
|
||||
"""Test earlier web searches are left out of the history.
|
||||
|
||||
The chat log keeps only a search's status, not its results. Replaying the
|
||||
bare search makes the model disown the answer it based on that search.
|
||||
"""
|
||||
content = [
|
||||
conversation.UserContent(content="How did the match go?"),
|
||||
conversation.AssistantContent(
|
||||
agent_id="conversation.test",
|
||||
tool_calls=[
|
||||
llm.ToolInput(
|
||||
id="ws_1",
|
||||
tool_name="web_search_call",
|
||||
tool_args={"action": {"type": "search", "query": "match"}},
|
||||
external=True,
|
||||
)
|
||||
],
|
||||
),
|
||||
conversation.ToolResultContent(
|
||||
agent_id="conversation.test",
|
||||
tool_call_id="ws_1",
|
||||
tool_name="web_search_call",
|
||||
result=llm.ToolResult(data={"status": "completed"}),
|
||||
),
|
||||
conversation.AssistantContent(
|
||||
agent_id="conversation.test",
|
||||
content="It ended 2 to 2.",
|
||||
tool_calls=[
|
||||
llm.ToolInput(id="call_1", tool_name="GetLiveContext", tool_args={})
|
||||
],
|
||||
),
|
||||
conversation.ToolResultContent(
|
||||
agent_id="conversation.test",
|
||||
tool_call_id="call_1",
|
||||
tool_name="GetLiveContext",
|
||||
result=llm.ToolResult(data={"success": True}),
|
||||
),
|
||||
]
|
||||
|
||||
messages = _convert_content_to_param(content)
|
||||
|
||||
assert [message["type"] for message in messages] == [
|
||||
"message",
|
||||
"message",
|
||||
"function_call",
|
||||
"function_call_output",
|
||||
]
|
||||
assert messages[1]["content"] == "It ended 2 to 2."
|
||||
assert messages[2]["call_id"] == "call_1"
|
||||
Reference in new issue
Block a user