mirror of
https://github.com/home-assistant/core.git
synced 2026-05-25 09:49:52 +01:00
Fix reasoning summary handling for OpenAI o-models (#168093)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
This commit is contained in:
@@ -46,6 +46,7 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_PROMPT,
|
||||
CONF_REASONING_EFFORT,
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
@@ -59,6 +60,7 @@ from .const import (
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_MAX_TOKENS,
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_REASONING_SUMMARY,
|
||||
RECOMMENDED_STORE_RESPONSES,
|
||||
RECOMMENDED_STT_OPTIONS,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
@@ -490,6 +492,25 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
||||
_add_stt_subentry(hass, entry)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=6)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 6:
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.subentry_type in ("conversation", "ai_task_data"):
|
||||
data = dict(subentry.data)
|
||||
updated = False
|
||||
if data.get(CONF_REASONING_SUMMARY) == "short":
|
||||
data[CONF_REASONING_SUMMARY] = "concise"
|
||||
updated = True
|
||||
if data.get(CONF_REASONING_SUMMARY) == "concise" and not data.get(
|
||||
CONF_CHAT_MODEL, ""
|
||||
).startswith("gpt-5"):
|
||||
data[CONF_REASONING_SUMMARY] = RECOMMENDED_REASONING_SUMMARY
|
||||
updated = True
|
||||
if updated:
|
||||
hass.config_entries.async_update_subentry(
|
||||
entry, subentry, data=data
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=7)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
@@ -127,7 +127,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for OpenAI Conversation."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 6
|
||||
MINOR_VERSION = 7
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -435,23 +435,37 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif CONF_VERBOSITY in options:
|
||||
options.pop(CONF_VERBOSITY)
|
||||
|
||||
if model.startswith(("o", "gpt-5")):
|
||||
reasoning_summary_options = ["off", "auto", "concise", "detailed"]
|
||||
if model.startswith("o"):
|
||||
reasoning_summary_options.remove("concise")
|
||||
stored_summary = options.get(
|
||||
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
|
||||
)
|
||||
if stored_summary not in reasoning_summary_options:
|
||||
stored_summary = RECOMMENDED_REASONING_SUMMARY
|
||||
options[CONF_REASONING_SUMMARY] = stored_summary
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_REASONING_SUMMARY,
|
||||
default=RECOMMENDED_REASONING_SUMMARY,
|
||||
default=stored_summary,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=["off", "auto", "short", "detailed"],
|
||||
options=reasoning_summary_options,
|
||||
translation_key=CONF_REASONING_SUMMARY,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif CONF_VERBOSITY in options:
|
||||
options.pop(CONF_VERBOSITY)
|
||||
if CONF_REASONING_SUMMARY in options:
|
||||
if not model.startswith("gpt-5"):
|
||||
options.pop(CONF_REASONING_SUMMARY)
|
||||
elif CONF_REASONING_SUMMARY in options:
|
||||
options.pop(CONF_REASONING_SUMMARY)
|
||||
|
||||
service_tiers = self._get_service_tiers(model)
|
||||
if "flex" in service_tiers or "priority" in service_tiers:
|
||||
|
||||
@@ -43,7 +43,10 @@ from openai.types.responses import (
|
||||
ToolParam,
|
||||
WebSearchToolParam,
|
||||
)
|
||||
from openai.types.responses.response_create_params import ResponseCreateParamsStreaming
|
||||
from openai.types.responses.response_create_params import (
|
||||
Reasoning,
|
||||
ResponseCreateParamsStreaming,
|
||||
)
|
||||
from openai.types.responses.response_input_param import (
|
||||
FunctionCallOutput,
|
||||
ImageGenerationCall as ImageGenerationCallParam,
|
||||
@@ -520,16 +523,19 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
)
|
||||
|
||||
if model_args["model"].startswith(("o", "gpt-5")):
|
||||
model_args["reasoning"] = {
|
||||
reasoning: Reasoning = {
|
||||
"effort": options.get(
|
||||
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
|
||||
)
|
||||
if not model_args["model"].startswith("gpt-5-pro")
|
||||
else "high", # GPT-5 pro only supports reasoning.effort: high
|
||||
"summary": options.get(
|
||||
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
|
||||
),
|
||||
}
|
||||
reasoning_summary = options.get(
|
||||
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
|
||||
)
|
||||
if reasoning_summary != "off":
|
||||
reasoning["summary"] = reasoning_summary
|
||||
model_args["reasoning"] = reasoning
|
||||
model_args["include"] = ["reasoning.encrypted_content"]
|
||||
|
||||
if (
|
||||
|
||||
@@ -242,9 +242,9 @@
|
||||
"reasoning_summary": {
|
||||
"options": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"concise": "Concise",
|
||||
"detailed": "Detailed",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"short": "Short"
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"search_context_size": {
|
||||
|
||||
@@ -58,7 +58,7 @@ def mock_config_entry(
|
||||
"api_key": "bla",
|
||||
},
|
||||
version=2,
|
||||
minor_version=6,
|
||||
minor_version=7,
|
||||
subentries_data=[
|
||||
ConfigSubentryData(
|
||||
data=mock_conversation_subentry_data,
|
||||
|
||||
@@ -119,7 +119,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
},
|
||||
]
|
||||
assert result2["version"] == 2
|
||||
assert result2["minor_version"] == 6
|
||||
assert result2["minor_version"] == 7
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -278,10 +278,10 @@ async def test_subentry_unsupported_model(
|
||||
)
|
||||
async def test_subentry_reasoning_effort_list(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry,
|
||||
mock_init_component,
|
||||
model,
|
||||
reasoning_effort_options,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component: None,
|
||||
model: str,
|
||||
reasoning_effort_options: list[str],
|
||||
) -> None:
|
||||
"""Test the list reasoning effort options."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
@@ -318,6 +318,152 @@ async def test_subentry_reasoning_effort_list(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "has_reasoning_summary"),
|
||||
[
|
||||
("o3", True),
|
||||
("o4-mini", True),
|
||||
("gpt-5", True),
|
||||
("gpt-5-mini", True),
|
||||
("gpt-5-pro", True),
|
||||
("gpt-4o", False),
|
||||
("gpt-4.1", False),
|
||||
],
|
||||
)
|
||||
async def test_subentry_reasoning_summary_visibility(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component: None,
|
||||
model: str,
|
||||
has_reasoning_summary: bool,
|
||||
) -> None:
|
||||
"""Test that reasoning_summary option is shown for all reasoning models."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "init"
|
||||
|
||||
# Configure initial step
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "advanced"
|
||||
|
||||
# Configure advanced step
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_CHAT_MODEL: model,
|
||||
},
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "model"
|
||||
assert (CONF_REASONING_SUMMARY in subentry_flow["data_schema"].schema) == (
|
||||
has_reasoning_summary
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "reasoning_summary_options"),
|
||||
[
|
||||
("o3", ["off", "auto", "detailed"]),
|
||||
("o4-mini", ["off", "auto", "detailed"]),
|
||||
("gpt-5", ["off", "auto", "concise", "detailed"]),
|
||||
("gpt-5-mini", ["off", "auto", "concise", "detailed"]),
|
||||
],
|
||||
)
|
||||
async def test_subentry_reasoning_summary_options(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component: None,
|
||||
model: str,
|
||||
reasoning_summary_options: list[str],
|
||||
) -> None:
|
||||
"""Test the list of reasoning summary options for reasoning models."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "init"
|
||||
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "advanced"
|
||||
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_CHAT_MODEL: model,
|
||||
},
|
||||
)
|
||||
assert subentry_flow["type"] is FlowResultType.FORM
|
||||
assert subentry_flow["step_id"] == "model"
|
||||
assert (
|
||||
subentry_flow["data_schema"].schema[CONF_REASONING_SUMMARY].config["options"]
|
||||
== reasoning_summary_options
|
||||
)
|
||||
|
||||
|
||||
async def test_subentry_reasoning_summary_default_sanitized_on_model_switch(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component: None,
|
||||
) -> None:
|
||||
"""Test that a stored 'concise' default is sanitized to 'auto' for o* models."""
|
||||
subentry = next(
|
||||
s
|
||||
for s in mock_config_entry.subentries.values()
|
||||
if s.subentry_type == "conversation"
|
||||
)
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={**subentry.data, CONF_REASONING_SUMMARY: "concise"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
assert subentry_flow["step_id"] == "init"
|
||||
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
)
|
||||
assert subentry_flow["step_id"] == "advanced"
|
||||
|
||||
subentry_flow = await hass.config_entries.subentries.async_configure(
|
||||
subentry_flow["flow_id"],
|
||||
{CONF_CHAT_MODEL: "o3"},
|
||||
)
|
||||
assert subentry_flow["step_id"] == "model"
|
||||
|
||||
schema = subentry_flow["data_schema"].schema
|
||||
summary_key = next(k for k in schema if k == CONF_REASONING_SUMMARY)
|
||||
assert summary_key.default() == RECOMMENDED_REASONING_SUMMARY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "service_tier_options"),
|
||||
[
|
||||
@@ -544,6 +690,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_MAX_TOKENS: 10000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_EFFORT: "high",
|
||||
CONF_REASONING_SUMMARY: RECOMMENDED_REASONING_SUMMARY,
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
},
|
||||
),
|
||||
@@ -810,6 +957,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_EFFORT: "low",
|
||||
CONF_REASONING_SUMMARY: RECOMMENDED_REASONING_SUMMARY,
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
},
|
||||
),
|
||||
@@ -823,6 +971,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_REASONING_EFFORT: "low",
|
||||
CONF_REASONING_SUMMARY: "auto",
|
||||
CONF_SERVICE_TIER: "flex",
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
CONF_VERBOSITY: "medium",
|
||||
|
||||
@@ -19,7 +19,9 @@ from homeassistant.components import conversation
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
from homeassistant.components.intent import async_register_timer_handler
|
||||
from homeassistant.components.openai_conversation.const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_CODE_INTERPRETER,
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_SERVICE_TIER,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_WEB_SEARCH,
|
||||
@@ -385,6 +387,46 @@ async def test_function_call_without_reasoning(
|
||||
assert mock_chat_log.content[1:] == snapshot
|
||||
|
||||
|
||||
@freeze_time("2025-10-31 18:00:00")
|
||||
async def test_reasoning_summary_off_omits_summary_key(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component: None,
|
||||
mock_create_stream: AsyncMock,
|
||||
mock_chat_log: MockChatLog, # noqa: F811
|
||||
) -> None:
|
||||
"""Test that reasoning summary 'off' omits the summary key from the API call."""
|
||||
conversation_subentry = next(
|
||||
subentry
|
||||
for subentry in mock_config_entry.subentries.values()
|
||||
if subentry.subentry_type == "conversation"
|
||||
)
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
conversation_subentry,
|
||||
data={
|
||||
CONF_CHAT_MODEL: "o4-mini",
|
||||
CONF_REASONING_SUMMARY: "off",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_create_stream.return_value = [
|
||||
create_message_item(id="msg_A", text="Hello", output_index=0),
|
||||
]
|
||||
|
||||
await conversation.async_converse(
|
||||
hass,
|
||||
"Hello",
|
||||
mock_chat_log.conversation_id,
|
||||
Context(),
|
||||
agent_id="conversation.openai_conversation",
|
||||
)
|
||||
|
||||
reasoning = mock_create_stream.call_args.kwargs["reasoning"]
|
||||
assert "summary" not in reasoning
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("description", "messages"),
|
||||
[
|
||||
|
||||
@@ -19,6 +19,7 @@ from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.openai_conversation import CONF_CHAT_MODEL
|
||||
from homeassistant.components.openai_conversation.const import (
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_STORE_RESPONSES,
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
@@ -680,7 +681,7 @@ async def test_migration_from_v1(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.version == 2
|
||||
assert mock_config_entry.minor_version == 6
|
||||
assert mock_config_entry.minor_version == 7
|
||||
assert mock_config_entry.data == {"api_key": "1234"}
|
||||
assert mock_config_entry.options == {}
|
||||
|
||||
@@ -825,7 +826,7 @@ async def test_migration_from_v1_with_multiple_keys(
|
||||
|
||||
for idx, entry in enumerate(entries):
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert len(entry.subentries) == 4
|
||||
|
||||
@@ -930,7 +931,7 @@ async def test_migration_from_v1_with_same_keys(
|
||||
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert (
|
||||
len(entry.subentries) == 5
|
||||
@@ -1142,7 +1143,7 @@ async def test_migration_from_v1_disabled(
|
||||
assert entry.disabled_by is merged_config_entry_disabled_by
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == (
|
||||
4 if merged_config_entry_disabled_by is not None else 6
|
||||
4 if merged_config_entry_disabled_by is not None else 7
|
||||
)
|
||||
assert not entry.options
|
||||
assert entry.title == "OpenAI Conversation"
|
||||
@@ -1315,7 +1316,7 @@ async def test_migration_from_v2_1(
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert entry.title == "ChatGPT"
|
||||
assert len(entry.subentries) == 5 # 2 conversation + 1 AI task + 1 STT + 1 TTS
|
||||
@@ -1408,15 +1409,18 @@ async def test_devices(
|
||||
)
|
||||
assert len(devices) == 4 # One for conversation, AI task, STT, and TTS
|
||||
|
||||
# Find the conversation device for snapshot comparison
|
||||
device = next(d for d in devices if d.name == "OpenAI Conversation")
|
||||
assert device == snapshot(exclude=props("identifiers"))
|
||||
# Verify the device has identifiers matching one of the subentries
|
||||
expected_identifiers = [
|
||||
{(DOMAIN, subentry.subentry_id)}
|
||||
# Find the conversation subentry device specifically, since device ordering
|
||||
# from concurrent platform setup is non-deterministic.
|
||||
conversation_subentry = next(
|
||||
subentry
|
||||
for subentry in mock_config_entry.subentries.values()
|
||||
]
|
||||
assert device.identifiers in expected_identifiers
|
||||
if subentry.subentry_type == "conversation"
|
||||
)
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, conversation_subentry.subentry_id)}
|
||||
)
|
||||
assert device is not None
|
||||
assert device == snapshot(exclude=props("identifiers"))
|
||||
|
||||
|
||||
async def test_migration_from_v2_2(
|
||||
@@ -1463,7 +1467,7 @@ async def test_migration_from_v2_2(
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert entry.title == "ChatGPT"
|
||||
assert len(entry.subentries) == 4
|
||||
@@ -1508,7 +1512,7 @@ async def test_migration_from_v2_2(
|
||||
DeviceEntryDisabler.CONFIG_ENTRY,
|
||||
RegistryEntryDisabler.CONFIG_ENTRY,
|
||||
True,
|
||||
6,
|
||||
7,
|
||||
None,
|
||||
DeviceEntryDisabler.USER,
|
||||
RegistryEntryDisabler.DEVICE,
|
||||
@@ -1518,7 +1522,7 @@ async def test_migration_from_v2_2(
|
||||
DeviceEntryDisabler.USER,
|
||||
RegistryEntryDisabler.DEVICE,
|
||||
True,
|
||||
6,
|
||||
7,
|
||||
None,
|
||||
DeviceEntryDisabler.USER,
|
||||
RegistryEntryDisabler.DEVICE,
|
||||
@@ -1528,7 +1532,7 @@ async def test_migration_from_v2_2(
|
||||
DeviceEntryDisabler.USER,
|
||||
RegistryEntryDisabler.USER,
|
||||
True,
|
||||
6,
|
||||
7,
|
||||
None,
|
||||
DeviceEntryDisabler.USER,
|
||||
RegistryEntryDisabler.USER,
|
||||
@@ -1538,7 +1542,7 @@ async def test_migration_from_v2_2(
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
6,
|
||||
7,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -1730,7 +1734,7 @@ async def test_migration_from_v2_4(
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert entry.title == "ChatGPT"
|
||||
assert len(entry.subentries) == 4
|
||||
@@ -1833,7 +1837,7 @@ async def test_migration_from_v2_5(
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 6
|
||||
assert entry.minor_version == 7
|
||||
assert not entry.options
|
||||
assert entry.title == "ChatGPT"
|
||||
assert len(entry.subentries) == 4
|
||||
@@ -1878,3 +1882,124 @@ async def test_migration_from_v2_5(
|
||||
stt_subentry = stt_subentries[0]
|
||||
assert stt_subentry.data == {}
|
||||
assert stt_subentry.title == "OpenAI STT"
|
||||
|
||||
|
||||
async def test_migration_from_v2_6(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test migration from version 2.6.
|
||||
|
||||
Ensures reasoning_summary "short" is renamed to "concise" for gpt-5 models,
|
||||
and that "concise" (whether from "short" or already stored) is reset to "auto"
|
||||
for o* models where it is unsupported. Other values and unrelated subentries
|
||||
are unchanged.
|
||||
"""
|
||||
conversation_options_short_o = {
|
||||
"chat_model": "o4-mini",
|
||||
CONF_REASONING_SUMMARY: "short",
|
||||
}
|
||||
conversation_options_auto = {
|
||||
"chat_model": "gpt-5-mini",
|
||||
CONF_REASONING_SUMMARY: "auto",
|
||||
}
|
||||
ai_task_options_short_o = {
|
||||
"chat_model": "o3",
|
||||
CONF_REASONING_SUMMARY: "short",
|
||||
}
|
||||
tts_options = {
|
||||
"chat_model": "gpt-4o-mini-tts",
|
||||
}
|
||||
conversation_options_short_gpt5 = {
|
||||
"chat_model": "gpt-5",
|
||||
CONF_REASONING_SUMMARY: "short",
|
||||
}
|
||||
conversation_options_concise_o = {
|
||||
"chat_model": "o3",
|
||||
CONF_REASONING_SUMMARY: "concise",
|
||||
}
|
||||
mock_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"api_key": "1234"},
|
||||
entry_id="mock_entry_id",
|
||||
version=2,
|
||||
minor_version=6,
|
||||
subentries_data=[
|
||||
ConfigSubentryData(
|
||||
data=conversation_options_short_o,
|
||||
subentry_id="mock_id_1",
|
||||
subentry_type="conversation",
|
||||
title="ChatGPT short o",
|
||||
unique_id=None,
|
||||
),
|
||||
ConfigSubentryData(
|
||||
data=conversation_options_auto,
|
||||
subentry_id="mock_id_2",
|
||||
subentry_type="conversation",
|
||||
title="ChatGPT auto",
|
||||
unique_id=None,
|
||||
),
|
||||
ConfigSubentryData(
|
||||
data=ai_task_options_short_o,
|
||||
subentry_id="mock_id_3",
|
||||
subentry_type="ai_task_data",
|
||||
title="OpenAI AI Task",
|
||||
unique_id=None,
|
||||
),
|
||||
ConfigSubentryData(
|
||||
data=tts_options,
|
||||
subentry_id="mock_id_4",
|
||||
subentry_type="tts",
|
||||
title="OpenAI TTS",
|
||||
unique_id=None,
|
||||
),
|
||||
ConfigSubentryData(
|
||||
data=conversation_options_short_gpt5,
|
||||
subentry_id="mock_id_5",
|
||||
subentry_type="conversation",
|
||||
title="ChatGPT short gpt5",
|
||||
unique_id=None,
|
||||
),
|
||||
ConfigSubentryData(
|
||||
data=conversation_options_concise_o,
|
||||
subentry_id="mock_id_6",
|
||||
subentry_type="conversation",
|
||||
title="ChatGPT concise o",
|
||||
unique_id=None,
|
||||
),
|
||||
],
|
||||
title="ChatGPT",
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.openai_conversation.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 7
|
||||
|
||||
subentries_by_id = entry.subentries
|
||||
|
||||
# "short" on an o* model: short→concise→auto (concise unsupported on o*)
|
||||
assert subentries_by_id["mock_id_1"].data[CONF_REASONING_SUMMARY] == "auto"
|
||||
|
||||
# "auto" on a gpt-5 model: unchanged
|
||||
assert subentries_by_id["mock_id_2"].data[CONF_REASONING_SUMMARY] == "auto"
|
||||
|
||||
# "short" on an o* ai_task_data subentry: short→concise→auto
|
||||
assert subentries_by_id["mock_id_3"].data[CONF_REASONING_SUMMARY] == "auto"
|
||||
|
||||
# TTS subentry is unaffected
|
||||
assert subentries_by_id["mock_id_4"].data == tts_options
|
||||
|
||||
# "short" on a gpt-5 model: short→concise (concise is valid for gpt-5)
|
||||
assert subentries_by_id["mock_id_5"].data[CONF_REASONING_SUMMARY] == "concise"
|
||||
|
||||
# "concise" already stored on an o* model: concise→auto
|
||||
assert subentries_by_id["mock_id_6"].data[CONF_REASONING_SUMMARY] == "auto"
|
||||
|
||||
Reference in New Issue
Block a user