Remove support for Anthropic retired models (#182979)

This commit is contained in:
Denis Shulyaka
2026-09-26 22:44:55 +03:00
committed by GitHub
parent f5e582f485
commit c9f01eb001
11 changed files with 235 additions and 765 deletions
@@ -56,9 +56,7 @@ class AnthropicTaskEntity(
chat_log: conversation.ChatLog,
) -> ai_task.GenDataTaskResult:
"""Handle a generate data task."""
await self._async_handle_chat_log(
chat_log, task.name, task.structure, max_iterations=1000
)
await self._async_handle_chat_log(chat_log, task.structure, max_iterations=1000)
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
raise HomeAssistantError(
@@ -37,10 +37,9 @@ async def async_create_client(
@callback
def model_alias(model_id: str) -> str:
"""Resolve alias from versioned model name."""
if model_id[-2:-1] != "-" and not model_id.endswith("-preview"):
model_id = model_id[:-9]
if model_id.endswith("-4"):
return model_id + "-0"
model, _, version = model_id.rpartition("-")
if len(version) == 8 and version.isdecimal():
return model
return model_id
+15 -103
View File
@@ -57,9 +57,6 @@ from anthropic.types import (
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolChoiceAnyParam,
ToolChoiceAutoParam,
ToolChoiceToolParam,
ToolParam,
ToolSearchToolBm25_20251119Param,
ToolSearchToolResultBlock,
@@ -111,7 +108,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util, slugify
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonArrayType, JsonObjectType
from .const import (
@@ -528,12 +525,10 @@ class AnthropicDeltaStream:
self,
chat_log: conversation.ChatLog,
stream: AsyncStream[MessageStreamEvent],
output_tool: str | None = None,
) -> None:
"""Initialize the delta stream."""
self._chat_log: conversation.ChatLog = chat_log
self._stream: AsyncStream[MessageStreamEvent] = stream
self._output_tool: str | None = output_tool
self._buffer: deque[
conversation.AssistantContentDeltaDict
@@ -666,15 +661,6 @@ class AnthropicDeltaStream:
input=input,
)
self._current_tool_args = ""
if name == self._output_tool:
if self._first_block or self._content_details.has_content():
if self._content_details:
self._content_details.delete_empty()
self._buffer.append({"native": self._content_details})
self._content_details = ContentDetails()
self._content_details.add_citation_detail()
self._buffer.append({"role": "assistant"})
self._first_block = False
def on_text_block(self, text: str, citations: list[TextCitation] | None) -> None:
"""Handle TextBlock."""
@@ -814,14 +800,7 @@ class AnthropicDeltaStream:
def on_input_json_delta(self, partial_json: str) -> None:
"""Handle InputJSONDelta."""
if (
self._current_tool_block is not None
and self._current_tool_block["name"] == self._output_tool
):
self._content_details.citation_details[-1].length += len(partial_json)
self._buffer.append({"content": partial_json})
else:
self._current_tool_args += partial_json
self._current_tool_args += partial_json
def on_text_delta(self, text: str) -> None:
"""Handle TextDelta."""
@@ -845,9 +824,6 @@ class AnthropicDeltaStream:
def on_content_block_stop_event(self, index: int) -> None:
"""Handle RawContentBlockStopEvent."""
if self._current_tool_block is not None:
if self._current_tool_block["name"] == self._output_tool:
self._current_tool_block = None
return
tool_args = (
json.loads(self._current_tool_args) if self._current_tool_args else {}
)
@@ -928,12 +904,11 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
entry_type=dr.DeviceEntryType.SERVICE,
)
async def _get_model_args( # noqa: C901
async def _get_model_args(
self,
chat_log: conversation.ChatLog,
structure_name: str | None = None,
structure: probatio.Schema | None = None,
) -> tuple[MessageCreateParamsStreaming, str | None]:
) -> MessageCreateParamsStreaming:
"""Get the model arguments."""
options: dict[str, Any] = DEFAULT | self.subentry.data
@@ -1110,77 +1085,21 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
)
)
if structure and structure_name:
if (
self.model_info.capabilities
and self.model_info.capabilities.structured_outputs.supported
):
# Native structured output for those models who support it.
structure_name = None
model_args.setdefault("output_config", OutputConfigParam())[
"format"
] = JSONOutputFormatParam(
if structure:
model_args.setdefault("output_config", OutputConfigParam())["format"] = (
JSONOutputFormatParam(
type="json_schema",
schema={
**probatio.to_openapi(
schema=anthropic.transform_schema(
probatio.to_openapi(
structure,
custom_serializer=chat_log.llm_api.custom_serializer
if chat_log.llm_api
else llm.selector_serializer,
openapi_version="3.1.0",
),
"additionalProperties": False,
},
)
),
)
elif model_args["thinking"]["type"] == "disabled":
structure_name = slugify(structure_name)
if not tools:
# Simplest case: no tools and no extended thinking
# Add a tool and force its use
model_args["tool_choice"] = ToolChoiceToolParam(
type="tool",
name=structure_name,
)
else:
# Second case: tools present but no extended thinking
# Allow the model to use any tool but not text response
# The model should know to use the right tool by its description
model_args["tool_choice"] = ToolChoiceAnyParam(
type="any",
)
else:
# Extended thinking is enabled. With extended thinking, we cannot
# force tool use or disable text responses, so we add a hint to the
# system prompt instead. With extended thinking, the model should be
# smart enough to use the tool.
structure_name = slugify(structure_name)
model_args["tool_choice"] = ToolChoiceAutoParam(
type="auto",
)
model_args["system"].append( # type: ignore[union-attr]
TextBlockParam(
type="text",
text=f"Claude MUST use the '{structure_name}' tool to provide "
"the final answer instead of plain text.",
)
)
if structure_name:
tools.append(
ToolParam(
name=structure_name,
description="Use this tool to reply to the user",
input_schema=probatio.to_openapi(
structure,
custom_serializer=chat_log.llm_api.custom_serializer
if chat_log.llm_api
else llm.selector_serializer,
openapi_version="3.1.0",
),
)
)
preloaded_tools.append(structure_name)
)
if tools:
if options[CONF_TOOL_SEARCH] and len(tools) > len(preloaded_tools) + 1:
@@ -1196,19 +1115,16 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
model_args["tools"] = tools
return model_args, structure_name
return model_args
async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
structure_name: str | None = None,
structure: probatio.Schema | None = None,
max_iterations: int = MAX_TOOL_ITERATIONS,
) -> None:
"""Generate an answer for the chat log."""
model_args, structure_name = await self._get_model_args(
chat_log, structure_name, structure
)
model_args = await self._get_model_args(chat_log, structure)
coordinator = self.entry.runtime_data
client = coordinator.client
@@ -1222,11 +1138,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
AnthropicDeltaStream(
chat_log,
stream,
output_tool=structure_name or None,
),
AnthropicDeltaStream(chat_log, stream),
)
]
)
+148 -111
View File
@@ -53,6 +53,154 @@ from anthropic.types.web_fetch_tool_result_block import (
)
model_list = [
ModelInfo(
id="claude-opus-5-5",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=True),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=True),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=True),
low=CapabilitySupport(supported=True),
max=CapabilitySupport(supported=True),
medium=CapabilitySupport(supported=True),
supported=True,
xhigh=CapabilitySupport(supported=True),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=True),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=True),
enabled=CapabilitySupport(supported=False),
),
),
),
created_at=datetime.datetime(2026, 9, 21, 16, 24, tzinfo=datetime.UTC),
display_name="Claude Opus 5.5",
max_input_tokens=1000000,
max_tokens=128000,
type="model",
),
ModelInfo(
id="claude-fable-5-1",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=True),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=True),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=True),
low=CapabilitySupport(supported=True),
max=CapabilitySupport(supported=True),
medium=CapabilitySupport(supported=True),
supported=True,
xhigh=CapabilitySupport(supported=True),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=True),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=True),
enabled=CapabilitySupport(supported=False),
),
),
),
created_at=datetime.datetime(2026, 8, 28, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Fable 5.1",
max_input_tokens=1000000,
max_tokens=128000,
type="model",
),
ModelInfo(
id="claude-opus-5",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=True),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=True),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=True),
low=CapabilitySupport(supported=True),
max=CapabilitySupport(supported=True),
medium=CapabilitySupport(supported=True),
supported=True,
xhigh=CapabilitySupport(supported=True),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=True),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=True),
enabled=CapabilitySupport(supported=False),
),
),
),
created_at=datetime.datetime(2026, 7, 24, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Opus 5",
max_input_tokens=1000000,
max_tokens=128000,
type="model",
),
ModelInfo(
id="claude-sonnet-5",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=True),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=True),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=True),
low=CapabilitySupport(supported=True),
max=CapabilitySupport(supported=True),
medium=CapabilitySupport(supported=True),
supported=True,
xhigh=CapabilitySupport(supported=True),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=True),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=True),
enabled=CapabilitySupport(supported=False),
),
),
),
created_at=datetime.datetime(2026, 6, 29, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Sonnet 5",
max_input_tokens=1000000,
max_tokens=128000,
type="model",
),
ModelInfo(
id="claude-fable-5",
capabilities=ModelCapabilities(
@@ -349,117 +497,6 @@ model_list = [
max_tokens=64000,
type="model",
),
ModelInfo(
id="claude-opus-4-1-20250805",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=False),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=False),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=False),
low=CapabilitySupport(supported=False),
max=CapabilitySupport(supported=False),
medium=CapabilitySupport(supported=False),
supported=False,
xhigh=CapabilitySupport(supported=False),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=True),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=False),
enabled=CapabilitySupport(supported=True),
),
),
),
created_at=datetime.datetime(2025, 8, 5, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Opus 4.1",
max_input_tokens=200000,
max_tokens=32000,
type="model",
),
ModelInfo(
id="claude-opus-4-20250514",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=False),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=False),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=False),
low=CapabilitySupport(supported=False),
max=CapabilitySupport(supported=False),
medium=CapabilitySupport(supported=False),
supported=False,
xhigh=CapabilitySupport(supported=False),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=False),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=False),
enabled=CapabilitySupport(supported=True),
),
),
),
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Opus 4",
max_input_tokens=200000,
max_tokens=32000,
type="model",
),
ModelInfo(
id="claude-sonnet-4-20250514",
capabilities=ModelCapabilities(
batch=CapabilitySupport(supported=True),
citations=CapabilitySupport(supported=True),
code_execution=CapabilitySupport(supported=False),
context_management=ContextManagementCapability(
clear_thinking_20251015=CapabilitySupport(supported=True),
clear_tool_uses_20250919=CapabilitySupport(supported=True),
compact_20260112=CapabilitySupport(supported=False),
supported=True,
),
effort=EffortCapability(
high=CapabilitySupport(supported=False),
low=CapabilitySupport(supported=False),
max=CapabilitySupport(supported=False),
medium=CapabilitySupport(supported=False),
supported=False,
xhigh=CapabilitySupport(supported=False),
),
image_input=CapabilitySupport(supported=True),
pdf_input=CapabilitySupport(supported=True),
structured_outputs=CapabilitySupport(supported=False),
thinking=ThinkingCapability(
supported=True,
types=ThinkingTypes(
adaptive=CapabilitySupport(supported=False),
enabled=CapabilitySupport(supported=True),
),
),
),
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Sonnet 4",
max_input_tokens=1000000,
max_tokens=64000,
type="model",
),
]
@@ -54,273 +54,3 @@
}),
})
# ---
# name: test_generate_structured_data_legacy
dict({
'container': None,
'max_tokens': 3000,
'messages': list([
dict({
'content': 'Generate test data',
'role': 'user',
}),
dict({
'content': '{"characters": ["Mario", "Luigi"]}',
'role': 'assistant',
}),
]),
'model': 'claude-sonnet-4-0',
'stream': True,
'system': list([
dict({
'cache_control': dict({
'type': 'ephemeral',
}),
'text': '''
You are a Home Assistant expert and help users with their tasks.
Current time is 04:00:00. Today's date is 2026-01-01.
''',
'type': 'text',
}),
]),
'thinking': dict({
'type': 'disabled',
}),
'tool_choice': dict({
'name': 'test_task',
'type': 'tool',
}),
'tools': list([
dict({
'description': 'Use this tool to reply to the user',
'input_schema': dict({
'additionalProperties': False,
'properties': dict({
'characters': dict({
'items': dict({
'type': 'string',
}),
'type': 'array',
}),
}),
'required': list([
'characters',
]),
'type': 'object',
}),
'name': 'test_task',
}),
]),
})
# ---
# name: test_generate_structured_data_legacy_extended_thinking
dict({
'container': None,
'max_tokens': 3000,
'messages': list([
dict({
'content': 'Generate test data',
'role': 'user',
}),
dict({
'content': list([
dict({
'signature': 'ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==',
'thinking': "Let's use the tool to respond",
'type': 'thinking',
}),
dict({
'text': '{"characters": ["Mario", "Luigi"]}',
'type': 'text',
}),
]),
'role': 'assistant',
}),
]),
'model': 'claude-sonnet-4-0',
'stream': True,
'system': list([
dict({
'cache_control': dict({
'type': 'ephemeral',
}),
'text': '''
You are a Home Assistant expert and help users with their tasks.
Current time is 04:00:00. Today's date is 2026-01-01.
''',
'type': 'text',
}),
dict({
'text': "Claude MUST use the 'test_task' tool to provide the final answer instead of plain text.",
'type': 'text',
}),
]),
'thinking': dict({
'budget_tokens': 1500,
'display': 'summarized',
'type': 'enabled',
}),
'tool_choice': dict({
'type': 'auto',
}),
'tools': list([
dict({
'description': 'Use this tool to reply to the user',
'input_schema': dict({
'additionalProperties': False,
'properties': dict({
'characters': dict({
'items': dict({
'type': 'string',
}),
'type': 'array',
}),
}),
'required': list([
'characters',
]),
'type': 'object',
}),
'name': 'test_task',
}),
]),
})
# ---
# name: test_generate_structured_data_legacy_extra_text_block
dict({
'container': None,
'max_tokens': 3000,
'messages': list([
dict({
'content': 'Generate test data',
'role': 'user',
}),
dict({
'content': list([
dict({
'signature': 'ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==',
'thinking': "Let's use the tool to respond",
'type': 'thinking',
}),
dict({
'text': 'Sure!',
'type': 'text',
}),
dict({
'text': '{"characters": ["Mario", "Luigi"]}',
'type': 'text',
}),
]),
'role': 'assistant',
}),
]),
'model': 'claude-sonnet-4-0',
'stream': True,
'system': list([
dict({
'cache_control': dict({
'type': 'ephemeral',
}),
'text': '''
You are a Home Assistant expert and help users with their tasks.
Current time is 04:00:00. Today's date is 2026-01-01.
''',
'type': 'text',
}),
dict({
'text': "Claude MUST use the 'test_task' tool to provide the final answer instead of plain text.",
'type': 'text',
}),
]),
'thinking': dict({
'budget_tokens': 1500,
'display': 'summarized',
'type': 'enabled',
}),
'tool_choice': dict({
'type': 'auto',
}),
'tools': list([
dict({
'description': 'Use this tool to reply to the user',
'input_schema': dict({
'additionalProperties': False,
'properties': dict({
'characters': dict({
'items': dict({
'type': 'string',
}),
'type': 'array',
}),
}),
'required': list([
'characters',
]),
'type': 'object',
}),
'name': 'test_task',
}),
]),
})
# ---
# name: test_generate_structured_data_legacy_tools
dict({
'container': None,
'max_tokens': 3000,
'messages': list([
dict({
'content': 'Generate test data',
'role': 'user',
}),
dict({
'content': '{"characters": ["Mario", "Luigi"]}',
'role': 'assistant',
}),
]),
'model': 'claude-sonnet-4-0',
'stream': True,
'system': list([
dict({
'cache_control': dict({
'type': 'ephemeral',
}),
'text': '''
You are a Home Assistant expert and help users with their tasks.
Current time is 04:00:00. Today's date is 2026-01-01.
''',
'type': 'text',
}),
]),
'thinking': dict({
'type': 'disabled',
}),
'tool_choice': dict({
'type': 'any',
}),
'tools': list([
dict({
'max_uses': 5,
'name': 'web_search',
'type': 'web_search_20250305',
}),
dict({
'description': 'Use this tool to reply to the user',
'input_schema': dict({
'additionalProperties': False,
'properties': dict({
'characters': dict({
'items': dict({
'type': 'string',
}),
'type': 'array',
}),
}),
'required': list([
'characters',
]),
'type': 'object',
}),
'name': 'test_task',
}),
]),
})
# ---
@@ -1,6 +1,22 @@
# serializer version: 1
# name: test_model_list
list([
dict({
'label': 'Claude Opus 5.5',
'value': 'claude-opus-5-5',
}),
dict({
'label': 'Claude Fable 5.1',
'value': 'claude-fable-5-1',
}),
dict({
'label': 'Claude Opus 5',
'value': 'claude-opus-5',
}),
dict({
'label': 'Claude Sonnet 5',
'value': 'claude-sonnet-5',
}),
dict({
'label': 'Claude Fable 5',
'value': 'claude-fable-5',
@@ -33,17 +49,5 @@
'label': 'Claude Sonnet 4.5',
'value': 'claude-sonnet-4-5',
}),
dict({
'label': 'Claude Opus 4.1',
'value': 'claude-opus-4-1',
}),
dict({
'label': 'Claude Opus 4',
'value': 'claude-opus-4-0',
}),
dict({
'label': 'Claude Sonnet 4',
'value': 'claude-sonnet-4-0',
}),
])
# ---
@@ -1831,7 +1831,9 @@
'result': dict({
'data': dict({
'content': dict({
'citations': None,
'citations': dict({
'enabled': True,
}),
'source': dict({
'data': '''
Home Assistant new version is out!
@@ -1889,8 +1891,8 @@
'cited_text': 'Anthropic integration now supports web fetch tool.',
'document_index': 0,
'document_title': 'Latest Home Assistant Release Notes',
'end_char_index': 105,
'start_char_index': 56,
'end_char_index': 104,
'start_char_index': 54,
'type': 'char_location',
}),
]),
@@ -1936,7 +1938,9 @@
dict({
'content': dict({
'content': dict({
'citations': None,
'citations': dict({
'enabled': True,
}),
'source': dict({
'data': '''
Home Assistant new version is out!
@@ -1982,8 +1986,8 @@
'cited_text': 'Anthropic integration now supports web fetch tool.',
'document_index': 0,
'document_title': 'Latest Home Assistant Release Notes',
'end_char_index': 105,
'start_char_index': 56,
'end_char_index': 104,
'start_char_index': 54,
'type': 'char_location',
}),
]),
+4 -248
View File
@@ -11,15 +11,11 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import ai_task, media_source
from homeassistant.components.anthropic.const import (
CONF_CHAT_MODEL,
CONF_THINKING_BUDGET,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector
from . import create_content_block, create_thinking_block, create_tool_use_block
from . import create_content_block
from tests.common import MockConfigEntry
@@ -112,254 +108,14 @@ async def test_stream_wrong_type(
)
@freeze_time("2026-01-01 12:00:00")
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_structured_data_legacy(
async def test_generate_invalid_structured_data(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_stream: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test AI Task structured data generation with legacy method."""
for subentry in mock_config_entry.subentries.values():
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
CONF_CHAT_MODEL: "claude-sonnet-4-0",
CONF_THINKING_BUDGET: 0,
},
)
await hass.async_block_till_done()
mock_create_stream.return_value = [
create_tool_use_block(
0,
"toolu_0123456789AbCdEfGhIjKlM",
"test_task",
['{"charac', 'ters": ["Mario', '", "Luigi"]}'],
),
]
result = await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id="ai_task.claude_ai_task",
instructions="Generate test data",
structure=probatio.Schema(
{
probatio.Required("characters"): selector.selector(
{
"text": {
"multiple": True,
}
}
)
},
),
)
assert result.data == {"characters": ["Mario", "Luigi"]}
assert mock_create_stream.call_args.kwargs.copy() == snapshot
@freeze_time("2026-01-01 12:00:00")
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_structured_data_legacy_tools(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_stream: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test AI Task structured data generation with legacy method and tools enabled."""
mock_create_stream.return_value = [
create_tool_use_block(
0,
"toolu_0123456789AbCdEfGhIjKlM",
"test_task",
['{"charac', 'ters": ["Mario', '", "Luigi"]}'],
),
]
for subentry in mock_config_entry.subentries.values():
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
"chat_model": "claude-sonnet-4-0",
"web_search": True,
"thinking_budget": 0,
},
)
await hass.async_block_till_done()
result = await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id="ai_task.claude_ai_task",
instructions="Generate test data",
structure=probatio.Schema(
{
probatio.Required("characters"): selector.selector(
{
"text": {
"multiple": True,
}
}
)
},
),
)
assert result.data == {"characters": ["Mario", "Luigi"]}
assert mock_create_stream.call_args.kwargs.copy() == snapshot
@freeze_time("2026-01-01 12:00:00")
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_structured_data_legacy_extended_thinking(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_stream: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test AI Task structured data generation.
Uses legacy method with extended_thinking.
"""
mock_create_stream.return_value = [
(
*create_thinking_block(
0,
["Let's use the tool to respond"],
),
*create_tool_use_block(
1,
"toolu_0123456789AbCdEfGhIjKlM",
"test_task",
['{"charac', 'ters": ["Mario', '", "Luigi"]}'],
),
),
]
for subentry in mock_config_entry.subentries.values():
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
"chat_model": "claude-sonnet-4-0",
"thinking_budget": 1500,
},
)
await hass.async_block_till_done()
result = await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id="ai_task.claude_ai_task",
instructions="Generate test data",
structure=probatio.Schema(
{
probatio.Required("characters"): selector.selector(
{
"text": {
"multiple": True,
}
}
)
},
),
)
assert result.data == {"characters": ["Mario", "Luigi"]}
assert mock_create_stream.call_args.kwargs.copy() == snapshot
@freeze_time("2026-01-01 12:00:00")
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_structured_data_legacy_extra_text_block(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_stream: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test AI Task structured data generation.
Uses legacy method with extra text block.
"""
mock_create_stream.return_value = [
(
*create_thinking_block(
0,
["Let's use the tool to respond"],
),
*create_content_block(1, ["Sure!"]),
*create_tool_use_block(
2,
"toolu_0123456789AbCdEfGhIjKlM",
"test_task",
['{"charac', 'ters": ["Mario', '", "Luigi"]}'],
),
),
]
for subentry in mock_config_entry.subentries.values():
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
"chat_model": "claude-sonnet-4-0",
"thinking_budget": 1500,
},
)
await hass.async_block_till_done()
result = await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id="ai_task.claude_ai_task",
instructions="Generate test data",
structure=probatio.Schema(
{
probatio.Required("characters"): selector.selector(
{
"text": {
"multiple": True,
}
}
)
},
),
)
assert result.data == {"characters": ["Mario", "Luigi"]}
assert mock_create_stream.call_args.kwargs.copy() == snapshot
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_invalid_structured_data_legacy(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_stream: AsyncMock,
) -> None:
"""Test AI Task with invalid JSON response with legacy method."""
for subentry in mock_config_entry.subentries.values():
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
CONF_CHAT_MODEL: "claude-sonnet-4-0",
},
)
await hass.async_block_till_done()
"""Test AI Task with an invalid JSON response."""
mock_create_stream.return_value = [
create_tool_use_block(
0,
"toolu_0123456789AbCdEfGhIjKlM",
"test_task",
"INVALID JSON RESPONSE",
)
create_content_block(0, ["INVALID JSON RESPONSE"])
]
with pytest.raises(
@@ -378,7 +378,7 @@ async def test_subentry_web_search_user_location(
type="message",
id="mock_message_id",
role="assistant",
model="claude-sonnet-4-0",
model="claude-sonnet-4-5",
usage=types.Usage(input_tokens=100, output_tokens=100),
content=[
types.TextBlock(
@@ -9,6 +9,7 @@ from anthropic import RateLimitError
from anthropic.types import (
CitationCharLocation,
CitationCharLocationParam,
CitationsConfig,
CitationsWebSearchResultLocation,
CitationWebSearchResultLocationParam,
DocumentBlock,
@@ -1018,7 +1019,7 @@ async def test_web_search(
next(iter(mock_config_entry.subentries.values())),
data={
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_CHAT_MODEL: "claude-sonnet-4-0",
CONF_CHAT_MODEL: "claude-sonnet-4-5",
CONF_WEB_SEARCH: True,
CONF_WEB_SEARCH_MAX_USES: 5,
CONF_WEB_SEARCH_USER_LOCATION: True,
@@ -1164,7 +1165,7 @@ async def test_web_search_error(
next(iter(mock_config_entry.subentries.values())),
data={
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_CHAT_MODEL: "claude-sonnet-4-0",
CONF_CHAT_MODEL: "claude-sonnet-4-5",
CONF_WEB_SEARCH: True,
CONF_WEB_SEARCH_MAX_USES: 5,
CONF_WEB_SEARCH_USER_LOCATION: True,
@@ -1878,13 +1879,13 @@ async def test_web_fetch(
mock_create_stream: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test web fetch."""
"""Test web fetch with interleaved thinking and citation parsing."""
hass.config_entries.async_update_subentry(
mock_config_entry,
next(iter(mock_config_entry.subentries.values())),
data={
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_CHAT_MODEL: "claude-sonnet-4-6",
CONF_WEB_FETCH: True,
CONF_WEB_FETCH_MAX_USES: 5,
},
@@ -1896,7 +1897,7 @@ async def test_web_fetch(
url="https://www.home-assistant.io/latest-release-notes/",
content=DocumentBlock(
type="document",
citations=None,
citations=CitationsConfig(enabled=True),
source=PlainTextSource(
type="text",
data="Home Assistant new version is out!\nMany new features.\n"
@@ -1950,8 +1951,8 @@ async def test_web_fetch(
type="char_location",
document_index=0,
document_title="Latest Home Assistant Release Notes",
start_char_index=56,
end_char_index=105,
start_char_index=54,
end_char_index=104,
cited_text="Anthropic integration now supports web fetch tool.",
),
],
@@ -1969,6 +1970,15 @@ async def test_web_fetch(
agent_id="conversation.claude_conversation",
)
request = mock_create_stream.call_args.kwargs
assert request["model"] == "claude-sonnet-4-6"
assert request["thinking"] == {"type": "adaptive", "display": "summarized"}
assert {
"name": "web_fetch",
"type": "web_fetch_20250910",
"max_uses": 5,
} in request["tools"]
chat_log = hass.data.get(conversation.chat_log.DATA_CHAT_LOGS).get(
result.conversation_id
)
@@ -12,6 +12,7 @@ from homeassistant.components.anthropic.const import DOMAIN
from homeassistant.components.anthropic.coordinator import (
UPDATE_INTERVAL_CONNECTED,
UPDATE_INTERVAL_DISCONNECTED,
model_alias,
)
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.core import Context, HomeAssistant
@@ -21,6 +22,25 @@ from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.parametrize(
("model_id", "expected_alias"),
[
pytest.param("claude-opus-4-5-20251101", "claude-opus-4-5", id="dated_model"),
pytest.param("claude-opus-4-7", "claude-opus-4-7", id="version_alias"),
pytest.param("claude-opus-5", "claude-opus-5", id="major_version_alias"),
pytest.param(
"claude-opus-4-10", "claude-opus-4-10", id="multi_digit_version_alias"
),
pytest.param(
"claude-mythos-preview", "claude-mythos-preview", id="preview_alias"
),
],
)
def test_model_alias(model_id: str, expected_alias: str) -> None:
"""Test model aliases preserve versions and remove date suffixes."""
assert model_alias(model_id) == expected_alias
@patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock)
@pytest.mark.usefixtures("mock_init_component")
async def test_auth_error_handling(