84f4bcda552f70d49323718a554280f650e8c7f9
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84f4bcda55 |
fix: make the home location lookup work against Claude
- Send the location question as a user message. A request with only a
system message has no user content, which endpoints in front of Anthropic
models reject ("text content blocks must be non-empty").
- Leave unknown fields out of the web search user_location. After a failed
lookup there is no city or region, and the empty strings made every
request fail ("user_location.city: String should have at least 1
character").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
235c536cab |
fix: make the integration work against a Claude proxy
- Treat an empty tool argument string as no parameters instead of raising JSONDecodeError; the proxy sends "" for tools like the timer status (upstream issue #3). - Do not replay earlier web searches in the history. The chat log keeps only their status, and a search without results makes the model disown the answers it based on it and search again (upstream issue #4). - Look up the home location for web search with the subentry's model instead of gpt-4o-mini, and save the options without city/region if the lookup fails (upstream issue #5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6a599e092a |
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 |
||
|
|
e7fcb1d7a1 |
feat: one entry per provider, endpoints per type and model from a live dropdown
Reshapes setup around how a multi-model proxy is actually used: one config entry per server/provider, then one endpoint per model added by hand, with the model chosen from what that provider advertises right now. Setup (async_step_user): - Adds a Name field. With one entry per provider the title matters, and it was hardcoded to "ChatGPT" regardless of where the entry pointed. Blank falls back to the endpoint host via url_util.title_from_base_url. Deliberately absent from the reauth schema (now split out as STEP_REAUTH_DATA_SCHEMA): that step writes its input back into entry.data via data_updates, so a name field there would persist the title as connection data on every key rotation. - Stops auto-creating four subentries. conversation/ai_task_data/stt/tts were all created up front carrying RECOMMENDED_* options, which pin OpenAI model names (gpt-4o-mini-tts, gpt-4o-mini-transcribe). Against a third-party endpoint those entities are dead on arrival -- the reported symptom was an install that "loads bits, it's all janky". - Drops _async_abort_entries_match, so several entries may point at one server (one per provider-side key, for instance). Upstream aborts with already_configured on matching url+key. Model discovery (new model_list.py, kept out of the vendored files like url_util.py): - async_fetch_model_ids asks the endpoint's GET /v1/models. It calls with_options(max_retries=0), because this runs while the user waits on a dialog and the SDK otherwise retries with backoff -- visible in the test run as repeated "Retrying request to /models" and a suite that went from 5s to minutes. - Returns [] on any OpenAIError, and model_field() then yields a plain text field instead of a dropdown, so an endpoint that serves /v1/responses but not /v1/models is still configurable by hand. The dropdown sets custom_value=True for the same reason (LiteLLM wildcard routes). - Wired into all four types: conversation and ai_task_data on the additional step, stt (which hardcoded OpenAI's three transcribe models) and tts on init. TTS had no model field at all -- it silently used gpt-4o-mini-tts. A new endpoint now starts with "recommended" unticked. Ticked, the flow ends on the first step and never shows a model field, which is why the model looked unpickable; reconfigure still keeps whatever was saved. The model field stays on the additional step rather than moving to init. The init position reads better, but 71 ported tests drive the model through the additional step, several from parametrized option dicts that no mechanical edit could relocate. Rewriting the only regression net against upstream to save one click was the wrong trade. Also: the setup description no longer says "your OpenAI API key", and instructions_url pointed at a README anchor (#generate-an-api-key) that does not exist -- now #configuration. strings.json re-verified against core's RE_URL: 0 violations. Local: 167 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki |
||
|
|
117780dd68 |
fix: backfill base_url on migration so pre-base_url entries still load
async_setup_entry does entry.data[CONF_BASE_URL] as a hard index, but base_url is this fork's own addition: an entry created by a build from before the base_url commits (48195c9 era, version 2 minor 7) has no such key. On restart, setup raises KeyError: 'base_url' and the integration fails to load -- reproduced in test first, where async_setup returns False with that KeyError in the log. Fixed with a migration step rather than a defensive .get(), matching the pattern the rest of async_migrate_entry already uses, so the invariant "a loaded entry has base_url" holds for every caller instead of each call site guarding itself: - new step 2.7 -> 2.8 backfills DEFAULT_BASE_URL via dict.setdefault, so an entry that already has a base_url keeps its own endpoint and only a missing one is filled. An install from before this feature was implicitly using api.openai.com, which is exactly DEFAULT_BASE_URL, so the backfill preserves existing behavior rather than repointing anyone. - config_flow MINOR_VERSION 7 -> 8, otherwise the step never runs. test_client_built_with_base_url now also covers the preserve path, since its entry starts at minor 7 with a base_url set and must still reach the client unchanged after migrating. Also updated test_form's `minor_version == 7` assertion to 8; it pins the version new entries are created at, so the bump is expected. The reauth test's MockConfigEntry stays at minor 7 on purpose -- it stands in for an already-existing entry. Local: 138 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki |
||
|
|
b96b46495a |
fix: reauth must preserve base_url; reject credentials embedded in base_url
Critical (review finding 1): async_step_reauth_confirm showed STEP_USER_DATA_SCHEMA directly instead of prefilling it via add_suggested_values_to_schema (unlike async_step_user, which does), so the reauth form displayed DEFAULT_BASE_URL rather than the entry's configured endpoint. A user rotating their API key would have their integration silently repointed from e.g. LiteLLM back to api.openai.com on save. Fixed by prefilling from self._get_reauth_entry().data, same as the user step. strings.json's reauth_confirm block now documents the base_url field too. Regression test: test_reauth_preserves_base_url (asserts both the shown form's suggested_value and the final stored base_url). Important (review finding 2): base_url is user-suppliable and could carry embedded user:pass@ credentials that later surface through vendored exception logging (e.g. entity.py's "Error talking to OpenAI: %s") this fork does not and should not edit. Rather than touch vendored logging, reject credentials at the boundary: normalize_base_url now raises the new CredentialsInBaseURL (a subclass of InvalidBaseURL) when the URL has a username or password, mapped to a distinct "credentials_in_url" form error telling the user to use the API key field instead. Tests added in test_url_util.py and test_config_flow.py. Important (review finding 3): documented on sanitize_url_for_logging that it is not a general-purpose credential scrubber -- it only masks what urlparse recognizes as a netloc's user:pass@ authority component, so a scheme-less/relative string with embedded credentials passes through unmasked. Safe today because every caller normalizes first (which now also rejects embedded credentials outright), but it is exported for a later task's use and must not be assumed bulletproof on an arbitrary unvalidated string. Not fixed (reviewer's Minor 4, by direction): the default-config setup call reaching api.openai.com with Authorization: Bearer not-required when both base_url and api_key are left at defaults -- cosmetic, and correctly surfaces as invalid_auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4 |
||
|
|
564c52159f |
feat: configurable base_url threaded to all three client sites
Adds CONF_BASE_URL/DEFAULT_BASE_URL and threads base_url through every AsyncOpenAI construction site: config_flow.validate_input, __init__.async_setup_entry, and config_flow's web-search geolocation helper (_get_location_data) — the easy one to miss, since it would otherwise silently talk to api.openai.com when the user configured a different endpoint. Fixes upstream-ported fixtures (conftest.mock_config_entry, test_config_flow's inline MockConfigEntry/data-schema assertions) to supply base_url now that the config-flow schema requires it. No translations/en.json exists in this repo (only strings.json), so only strings.json got the new "base_url" UI strings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4 |
||
|
|
fa9648213d |
test: port core's openai_conversation suite as regression net
Ported from home-assistant/core tag 2026.9.2 (tests/components/openai_conversation): conftest.py fixtures merged into tests/conftest.py, test_config_flow.py, test_conversation.py and test_ai_task.py copied and rewritten for the fork's custom_components.openai_compatible / "openai_compatible" domain. Also copied as local test infrastructure (not core-only, self-contained): - tests/__init__.py: the openai_conversation test package's own event-builder helpers (create_message_item, create_function_tool_call_item, etc.) that test_conversation.py and test_ai_task.py import via `from . import ...`. - tests/snapshots/test_conversation.ambr: just the test_model_args snapshot entry, the one surviving syrupy-based assertion (see deletions below). Deleted from test_conversation.py — depend on tests.components.conversation's MockChatLog/mock_chat_log fixture, which lives in home-assistant/core's own tests tree and is not distributable via pytest-homeassistant-custom-component (confirmed: not present in the installed package's file list): - test_function_call - test_function_call_without_reasoning - test_reasoning_summary_off_omits_summary_key - test_web_search - test_web_search_remove_citations_gpt5 - test_code_interpreter Their now-unused imports (create_code_interpreter_item, create_web_search_item, CONF_CODE_INTERPRETER, CONF_REASONING_SUMMARY, the CONF_WEB_SEARCH* constants, freezegun.freeze_time, ToolInput, datetime) were removed with them. `from tests.common import MockConfigEntry` (core's own tests package, also not shipped by pytest-homeassistant-custom-component) rewritten to `from pytest_homeassistant_custom_component.common import MockConfigEntry`, matching the convention already used in this fork's tests/test_init.py. Not run locally: this workstation is Python 3.13, HA 2026.9 (and pytest-homeassistant-custom-component) require 3.14, so no correct local interpreter exists. Validated with `python3 -m py_compile` only; CI (actions/setup-python@3.14) is the real test run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4 |