feat: ask for the model on the first screen of the endpoint flow
The model field was one screen too late. Reported directly: "this is where I would have expected to see the model dropdown", on the AI Task init step that showed only Name and the recommended checkbox. - CONF_CHAT_MODEL moves from async_step_additional to async_step_init for conversation and ai_task_data, so all four endpoint types now ask on the first screen. stt and tts already did. - UNSUPPORTED_MODELS is validated there too, since the recommended path creates the subentry from that step and would otherwise skip the check. - model_list.default_model() picks what to preselect: RECOMMENDED_CHAT_MODEL only when the provider actually serves it (or discovery returned nothing), otherwise the first advertised model. Offering a stranger's gpt-4o-mini to a LiteLLM user preselects a model that fails on first use. This is the trade I got wrong last commit. I kept the field on the second step to avoid touching 71 ported tests, having judged them unfixable mechanically. They were not: the 71 were ~13 functions under parametrize, and test_subentry_switching's data is already a list of per-step dicts, so moving a model between steps is moving a key between two dicts. Two scripted passes moved 14 of them; 5 needed reasoning about (an error that now surfaces on init rather than the next step, and four cases whose expected data gains the now-always-recorded model). async_fetch_model_ids now also catches non-OpenAIError exceptions. Found via a ported test whose client is a MagicMock: the TypeError from awaiting it escaped and broke the whole form. A real proxy returning a payload the SDK cannot parse would do the same, and this runs inside a dialog with a working free-text fallback, so nothing it raises justifies taking the form down. Also fixes a string that was simply false on a third-party provider, again reported from the UI: "Store requests and responses in OpenAI" / "stored by OpenAI and visible in your OpenAI dashboard logs". It now describes the Responses API store flag and says support varies. Swept the other 43 OpenAI-mentioning strings while there -- the rest are [%key:...%] self-references or this fork's own action names, and no other option text makes a claim about who stores the data. Local: 170 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
This commit is contained in:
1 parent
030822c845
commit
6a599e092a
7 files changed
+128
-91
No files matched your search
@@ -104,7 +104,7 @@ from .const import (
|
||||
UNSUPPORTED_PRIORITY_SERVICE_TIERS_MODELS,
|
||||
UNSUPPORTED_WEB_SEARCH_MODELS,
|
||||
)
|
||||
from .model_list import async_fetch_model_ids, model_field
|
||||
from .model_list import async_fetch_model_ids, default_model, model_field
|
||||
from .url_util import (
|
||||
CredentialsInBaseURL,
|
||||
InvalidBaseURL,
|
||||
@@ -325,36 +325,58 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
}
|
||||
)
|
||||
|
||||
# The model belongs on the first screen. Picking one is the entire
|
||||
# point of adding an endpoint, and behind the "recommended" checkbox
|
||||
# it was unreachable: ticked, this step creates the subentry and the
|
||||
# model silently stays RECOMMENDED_CHAT_MODEL -- an OpenAI name a
|
||||
# third-party provider will not serve. entity.py reads
|
||||
# CONF_CHAT_MODEL regardless of the recommended flag, so a model
|
||||
# chosen here takes effect on either path.
|
||||
model_ids = await async_fetch_model_ids(self._get_entry().runtime_data)
|
||||
step_schema[
|
||||
vol.Optional(CONF_CHAT_MODEL, default=default_model(model_ids))
|
||||
] = model_field(model_ids)
|
||||
|
||||
step_schema[
|
||||
vol.Required(CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False))
|
||||
] = bool
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input.get(CONF_LLM_HASS_API) is None:
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
|
||||
if user_input[CONF_RECOMMENDED]:
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input.pop(CONF_NAME),
|
||||
# Checked here rather than on the next step, because the
|
||||
# recommended path creates the subentry from this one and would
|
||||
# otherwise skip the check entirely.
|
||||
if user_input.get(CONF_CHAT_MODEL) in UNSUPPORTED_MODELS:
|
||||
errors[CONF_CHAT_MODEL] = "model_not_supported"
|
||||
|
||||
if not errors:
|
||||
if user_input[CONF_RECOMMENDED]:
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input.pop(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
options.update(user_input)
|
||||
if CONF_LLM_HASS_API in options and CONF_LLM_HASS_API not in user_input:
|
||||
options.pop(CONF_LLM_HASS_API)
|
||||
return await self.async_step_additional()
|
||||
options.update(user_input)
|
||||
if CONF_LLM_HASS_API in options and CONF_LLM_HASS_API not in user_input:
|
||||
options.pop(CONF_LLM_HASS_API)
|
||||
return await self.async_step_additional()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(step_schema), options
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_additional(
|
||||
@@ -365,14 +387,8 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {
|
||||
# Upstream makes this a free-text box defaulting to an OpenAI
|
||||
# model name. Offer what the provider actually serves instead.
|
||||
vol.Optional(
|
||||
CONF_CHAT_MODEL,
|
||||
default=RECOMMENDED_CHAT_MODEL,
|
||||
): model_field(
|
||||
await async_fetch_model_ids(self._get_entry().runtime_data)
|
||||
),
|
||||
# CONF_CHAT_MODEL is asked on the init step, so that it is
|
||||
# reachable without unticking "recommended" first.
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=RECOMMENDED_MAX_TOKENS,
|
||||
@@ -393,8 +409,6 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
if user_input is not None:
|
||||
options.update(user_input)
|
||||
if user_input.get(CONF_CHAT_MODEL) in UNSUPPORTED_MODELS:
|
||||
errors[CONF_CHAT_MODEL] = "model_not_supported"
|
||||
|
||||
if not errors:
|
||||
return await self.async_step_model()
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import LOGGER, RECOMMENDED_CHAT_MODEL
|
||||
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
@@ -48,6 +48,14 @@ async def async_fetch_model_ids(
|
||||
except openai.OpenAIError as err:
|
||||
LOGGER.debug("Could not list models: %s", err)
|
||||
return []
|
||||
except Exception: # noqa: BLE001 - see below
|
||||
# Deliberately broad. This runs inside a config-flow form and has a
|
||||
# working fallback (a free-text model field), so nothing it can raise
|
||||
# justifies taking the dialog down: a proxy may return a payload the
|
||||
# SDK cannot parse, and the failure modes of arbitrary
|
||||
# OpenAI-compatible servers are not enumerable in advance.
|
||||
LOGGER.debug("Unexpected error listing models", exc_info=True)
|
||||
return []
|
||||
|
||||
return sorted({model.id for model in page.data if getattr(model, "id", None)})
|
||||
|
||||
@@ -71,3 +79,17 @@ def model_field(model_ids: list[str]) -> Any:
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def default_model(model_ids: list[str]) -> str:
|
||||
"""Return the model to preselect for a new endpoint.
|
||||
|
||||
Upstream's default is RECOMMENDED_CHAT_MODEL, an OpenAI name. Offering it
|
||||
to someone configuring a third-party provider preselects a model that will
|
||||
fail on first use, so prefer it only when the provider actually serves it
|
||||
(or when discovery told us nothing) and otherwise start from what is
|
||||
advertised.
|
||||
"""
|
||||
if not model_ids or RECOMMENDED_CHAT_MODEL in model_ids:
|
||||
return RECOMMENDED_CHAT_MODEL
|
||||
return model_ids[0]
|
||||
@@ -123,12 +123,12 @@
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"store_responses": "Store requests and responses in OpenAI",
|
||||
"store_responses": "Store requests and responses on the provider",
|
||||
"temperature": "Temperature",
|
||||
"top_p": "Top P"
|
||||
},
|
||||
"data_description": {
|
||||
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
|
||||
"store_responses": "Sets the Responses API store flag, asking the provider to keep requests and responses in its own logs. Support varies: OpenAI shows them in its dashboard, while many compatible servers ignore the flag entirely."
|
||||
},
|
||||
"title": "Additional settings"
|
||||
},
|
||||
|
||||
@@ -59,12 +59,12 @@
|
||||
"data": {
|
||||
"chat_model": "Model",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"store_responses": "Store requests and responses in OpenAI",
|
||||
"store_responses": "Store requests and responses on the provider",
|
||||
"temperature": "Temperature",
|
||||
"top_p": "Top P"
|
||||
},
|
||||
"data_description": {
|
||||
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
|
||||
"store_responses": "Sets the Responses API store flag, asking the provider to keep requests and responses in its own logs. Support varies: OpenAI shows them in its dashboard, while many compatible servers ignore the flag entirely."
|
||||
},
|
||||
"title": "Additional settings"
|
||||
},
|
||||
@@ -123,12 +123,12 @@
|
||||
"data": {
|
||||
"chat_model": "Model",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"store_responses": "Store requests and responses in OpenAI",
|
||||
"store_responses": "Store requests and responses on the provider",
|
||||
"temperature": "Temperature",
|
||||
"top_p": "Top P"
|
||||
},
|
||||
"data_description": {
|
||||
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
|
||||
"store_responses": "Sets the Responses API store flag, asking the provider to keep requests and responses in its own logs. Support varies: OpenAI shows them in its dashboard, while many compatible servers ignore the flag entirely."
|
||||
},
|
||||
"title": "Additional settings"
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user