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:
Adam SmithandClaude Opus 5 committed 2026-09-17 23:18:02 -04:00
1 parent 030822c845
commit 6a599e092a
7 files changed
+128 -91

No files matched your search

@@ -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]