From a07af843fd06658413027e7d40d362e7da1db142 Mon Sep 17 00:00:00 2001 From: Aiman Farooq <3269808+AimanFarooq@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:49:37 -0800 Subject: [PATCH] Allow xhigh, max and custom reasoning effort values Providers define their own reasoning_effort levels beyond low/medium/high: Meta's Model API documents xhigh and max (on some models), and other OpenAI-compatible servers have their own. Add xhigh and max as presets and let the selector accept a typed custom value, as the model picker already does, so new levels do not need a release. Custom values are free text, so the provider trims whitespace and treats a blank value like the none default (parameter omitted). Follow-up to #7. --- .../configurable_llm/config_flow.py | 1 + custom_components/configurable_llm/const.py | 11 ++++++++- .../providers/openai_chat_provider.py | 3 ++- .../configurable_llm/strings.json | 8 ++++--- .../configurable_llm/translations/en.json | 8 ++++--- tests/providers/test_openai_chat_provider.py | 24 +++++++++++++++++++ tests/test_config_flow.py | 10 ++++++++ 7 files changed, 57 insertions(+), 8 deletions(-) diff --git a/custom_components/configurable_llm/config_flow.py b/custom_components/configurable_llm/config_flow.py index b61d719..9699de6 100644 --- a/custom_components/configurable_llm/config_flow.py +++ b/custom_components/configurable_llm/config_flow.py @@ -689,6 +689,7 @@ def _openai_model_schema(self) -> VolDictType: options=REASONING_EFFORT_OPTIONS, translation_key=CONF_REASONING_EFFORT, mode=SelectSelectorMode.DROPDOWN, + custom_value=True, ) ), } diff --git a/custom_components/configurable_llm/const.py b/custom_components/configurable_llm/const.py index 10207c6..9dd3e49 100644 --- a/custom_components/configurable_llm/const.py +++ b/custom_components/configurable_llm/const.py @@ -26,7 +26,16 @@ # `reasoning_effort` choices offered for OpenAI-compatible providers. Servers and # models vary in what they accept; "none" means "do not send the parameter". -REASONING_EFFORT_OPTIONS = ["none", "minimal", "low", "medium", "high"] +# The selector also accepts a typed custom value for provider-specific levels. +REASONING_EFFORT_OPTIONS = [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] CONF_WEB_FETCH_MAX_USES = "web_fetch_max_uses" CONF_WEB_SEARCH = "web_search" diff --git a/custom_components/configurable_llm/providers/openai_chat_provider.py b/custom_components/configurable_llm/providers/openai_chat_provider.py index dc07bf2..efc1664 100644 --- a/custom_components/configurable_llm/providers/openai_chat_provider.py +++ b/custom_components/configurable_llm/providers/openai_chat_provider.py @@ -439,7 +439,8 @@ async def build_request( request["top_p"] = top_p # reasoning_effort is model-dependent; only send when explicitly set. - reasoning_effort = options.get(CONF_REASONING_EFFORT) + # Custom values are typed free text, so trim stray whitespace. + reasoning_effort = (options.get(CONF_REASONING_EFFORT) or "").strip() if reasoning_effort and reasoning_effort != "none": request["reasoning_effort"] = reasoning_effort diff --git a/custom_components/configurable_llm/strings.json b/custom_components/configurable_llm/strings.json index 3727e94..fca2266 100644 --- a/custom_components/configurable_llm/strings.json +++ b/custom_components/configurable_llm/strings.json @@ -115,7 +115,7 @@ "web_fetch": "Allow the model to retrieve full content from specified web pages and PDF documents to augment its context with live web content.", "web_fetch_max_uses": "Limit the number of web fetches the model can perform in a single response.", "tool_search": "Discover Home Assistant tools on demand instead of loading them all upfront. Useful when many tools are exposed.", - "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it." + "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it. You can also type a value your provider documents that is not listed." } } }, @@ -191,7 +191,7 @@ "web_fetch": "Allow the model to retrieve full content from specified web pages and PDF documents to augment its context with live web content.", "web_fetch_max_uses": "Limit the number of web fetches the model can perform in a single response.", "tool_search": "Discover Home Assistant tools on demand instead of loading them all upfront. Useful when many tools are exposed.", - "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it." + "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it. You can also type a value your provider documents that is not listed." } } }, @@ -248,7 +248,9 @@ "minimal": "Minimal", "low": "Low", "medium": "Medium", - "high": "High" + "high": "High", + "xhigh": "Extra high", + "max": "Max" } } }, diff --git a/custom_components/configurable_llm/translations/en.json b/custom_components/configurable_llm/translations/en.json index 3727e94..fca2266 100644 --- a/custom_components/configurable_llm/translations/en.json +++ b/custom_components/configurable_llm/translations/en.json @@ -115,7 +115,7 @@ "web_fetch": "Allow the model to retrieve full content from specified web pages and PDF documents to augment its context with live web content.", "web_fetch_max_uses": "Limit the number of web fetches the model can perform in a single response.", "tool_search": "Discover Home Assistant tools on demand instead of loading them all upfront. Useful when many tools are exposed.", - "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it." + "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it. You can also type a value your provider documents that is not listed." } } }, @@ -191,7 +191,7 @@ "web_fetch": "Allow the model to retrieve full content from specified web pages and PDF documents to augment its context with live web content.", "web_fetch_max_uses": "Limit the number of web fetches the model can perform in a single response.", "tool_search": "Discover Home Assistant tools on demand instead of loading them all upfront. Useful when many tools are exposed.", - "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it." + "reasoning_effort": "Reasoning effort for OpenAI-compatible reasoning models. Leave at Default if your model or server does not support it. You can also type a value your provider documents that is not listed." } } }, @@ -248,7 +248,9 @@ "minimal": "Minimal", "low": "Low", "medium": "Medium", - "high": "High" + "high": "High", + "xhigh": "Extra high", + "max": "Max" } } }, diff --git a/tests/providers/test_openai_chat_provider.py b/tests/providers/test_openai_chat_provider.py index 66eba1b..c559a1f 100644 --- a/tests/providers/test_openai_chat_provider.py +++ b/tests/providers/test_openai_chat_provider.py @@ -417,6 +417,30 @@ async def test_build_request_reasoning_effort_minimal(hass: HomeAssistant) -> No assert request["reasoning_effort"] == "minimal" +@pytest.mark.parametrize( + ("configured", "expected"), + [ + ("xhigh", "xhigh"), + ("max", "max"), + # Typed custom value for a provider-specific level. + ("ultra", "ultra"), + # Stray whitespace from free-text entry is trimmed. + (" low ", "low"), + # Blank custom entries behave like the "none" default. + (" ", None), + ("", None), + ], +) +async def test_build_request_reasoning_effort_values( + hass: HomeAssistant, configured: str, expected: str | None +) -> None: + """Preset and custom effort values are forwarded; blanks are omitted.""" + options = {**DEFAULT_OPENAI, CONF_REASONING_EFFORT: configured} + request, _ = await provider.build_request(_ctx(options=options)) + + assert request.get("reasoning_effort") == expected + + async def test_build_request_tools(hass: HomeAssistant) -> None: """HA tools become Chat Completions function tools.""" request, _ = await provider.build_request(_ctx(with_tools=True)) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index f2b18af..077c050 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -6,6 +6,7 @@ import anthropic import httpx import pytest +import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigEntryState, @@ -30,6 +31,7 @@ CONF_PRESET, CONF_PROTOCOL, CONF_PROMPT_CACHING, + CONF_REASONING_EFFORT, CONF_RECOMMENDED, CONF_THINKING_BUDGET, CONF_WEB_SEARCH, @@ -861,3 +863,11 @@ async def test_flow_subentry_openai_model_schema( result = await flow.async_step_model({}) assert result["type"] == FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize("effort", ["none", "minimal", "xhigh", "max", "ultra"]) +async def test_flow_subentry_openai_reasoning_effort_values(effort: str) -> None: + """The reasoning effort selector accepts presets and typed custom values.""" + schema = vol.Schema(ConversationSubentryFlowHandler()._openai_model_schema()) + + assert schema({CONF_REASONING_EFFORT: effort})[CONF_REASONING_EFFORT] == effort