Pre-publication audit of the full history (all 14 commits, every blob)
found no API keys, tokens, JWTs, private keys or credentials -- the
`user:secret@` strings in the tests are synthetic literals. It did find
one piece of personal infrastructure: 192.0.2.10:4000, a real
RFC1918 host, in 8 places.
Most of those are test fixtures, but one was shipping to users:
strings.json's base_url data_description used it as *the* example in the
config flow, so every install would have displayed a stranger's
internal LiteLLM address as the suggested endpoint. That is a bug
independent of whether this repo is public.
- strings.json: example is now http://litellm.local:4000/v1
- tests: 192.0.2.10 (RFC 5737 TEST-NET-1, reserved for documentation,
so it can never collide with a real host)
Not addressed here: the address remains in the 14 commits of git
history, which publishing would expose. Scrubbing that needs a history
rewrite, which is a separate decision. The commit author email is
likewise in every commit and would become public.
Local: 137 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
The ported suite passes in CI and fails locally on the same commit, same
Python (3.14.7) and byte-identical dependency versions (syrupy 6.0.0,
pytest 9.0.3, pytest-homeassistant-custom-component 0.13.365, HA
2026.9.2): tests/test_conversation.py::test_model_args fails with
"Snapshot 'test_model_args[subentry_options0]' does not exist!" even
though tests/snapshots/test_conversation.ambr contains exactly that key.
Cause: syrupy and pytest-homeassistant-custom-component both register a
`snapshot` fixture via the pytest11 entry point, and the plugin loaded
last wins. syrupy's stores snapshots in `__snapshots__/`;
HomeAssistantSnapshotExtension overrides dirname() to `snapshots/`,
which is where core's ported .ambr files are. Entry-point iteration
order is install/filesystem dependent, so which one wins varies per
machine -- confirmed by running with --snapshot-update locally, which
wrote a new file to tests/__snapshots__/ instead of finding the
committed one under tests/snapshots/.
That means the green CI run was luck, not design: any change to install
order could have flipped it, and the failure mode is a *missing*
snapshot, which reads like a stale fixture rather than a harness bug.
Fixed the way core's own tests/conftest.py does it -- redefine
`snapshot` in conftest, since a conftest fixture always takes precedence
over a plugin's. This is the one piece of core's conftest the port
dropped.
Local: 137 passed (was 136 passed, 1 failed). Matches CI's 137.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
Corrects the previous commit, which removed `render_readme` from
hacs.json on a wrong diagnosis. Two errors there:
1. `render_readme` IS in HACS_MANIFEST_JSON_SCHEMA
(custom_components/hacs/utils/validate.py), so it was never the
schema violation I claimed, and HacsManifest defaults it to False --
dropping it would have silently stopped HACS rendering the README on
the integration's HACS page. Restored.
2. The real cause of both remaining failures is that this repository is
**private**. hacsjson's error text ("has an invalid 'hacs.json'
file") is raised on `rawhacsjson is None`, not on a schema error --
a schema error would surface as a humanized voluptuous message.
get_hacs_json_raw() fetches
raw.githubusercontent.com/<repo>/<ref>/hacs.json *unauthenticated*
(wrapped in @return_none_on_exception), and that 404s for a private
repo. get_integration_manifest() fetches manifest.json the same way,
which is why integration_manifest reports "expected a dictionary.
Got None" -- voluptuous validating None, not a bad manifest. Both
files are in fact valid and manifest.json already carries all six
keys INTEGRATION_MANIFEST_JSON_SCHEMA requires.
So these last 2/9 checks cannot pass while the repo is private, and no
repo-side change can fix them; they should clear on their own when it
goes public. The other four failures the previous commit addressed
(topics, description, information, brands) did clear -- confirmed in
run 35296482777, which went 6/9 failed -> 2/9 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
The `hacs` validation job has failed on every push since the scaffold
commit (6/9 checks). This fixes the four failures that live in the repo;
the two that live in GitHub metadata (description, topics) are set via
the API and need no commit.
- hacs.json: remove `render_readme`. It is not in the HACS manifest
schema (valid keys: name, content_in_root, zip_release, filename,
hide_default_branch, country, homeassistant, hacs,
persistent_directory), and HACS rejects the whole file on an unknown
key -- which is what `<Validation hacsjson> failed` was. Expecting
the cascading `integration_manifest` failure ("expected a dictionary.
Got None") to clear with it, since manifest.json itself already
carries every key HACS requires.
- README.md: the repo had none, so the `information` check failed and
HACS had nothing to render. Documents the one thing this fork adds
(configurable base URL) and, importantly, the compatibility ceiling:
upstream calls the Responses API, so an endpoint that only serves
/v1/chat/completions cannot back the conversation or AI Task
subentries. Also records which endpoint each subentry needs, that
upstream's recommended model names and unsupported-model lists are
OpenAI-specific, the base-URL rules (http/https, no embedded
credentials, include /v1), and why a blank API key sends the
`not-required` placeholder.
- brand/icon.png + icon@2x.png: `brands` fails unless the repo ships
brand assets or the domain is in home-assistant/brands. Shipping
assets is the option that does not depend on a third-party PR.
Generated with stdlib zlib/struct (no PIL or ImageMagick in this
environment) from supersampled SDFs; the generator is scratch, not
committed. Deliberately neither OpenAI's nor Home Assistant's mark,
and a placeholder -- replace it with real artwork before release.
Not done here: this fork has no git tag or GitHub release yet. HACS
treats releases as preferred-but-not-required and the action does not
check for one, so it is not part of this failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
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
MockConfigEntry defaults to version=1, which triggers
async_migrate_integration on component setup; that legacy path
indexes entry.data[CONF_API_KEY] directly and raised KeyError for the
new optional-api-key entry in test_api_key_optional, before
async_setup_entry (and thus AsyncOpenAI) was ever reached. Pin both
entries in this file to version=2/minor_version=7, matching the
config flow's current VERSION/MINOR_VERSION and conftest's
mock_config_entry fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
The OpenAI SDK rejects api_key=None, so all three client sites now
fall back to a PLACEHOLDER_API_KEY ("not-required") when the entry has
no key, letting an unauthenticated LiteLLM/proxy endpoint be configured
with the API key field left blank.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
New url_util.py (deliberately separate from the vendored files, so a
future upstream re-sync never conflicts with it): normalize_base_url
strips whitespace/trailing slashes and rejects non-http(s) or hostless
URLs; sanitize_url_for_logging masks embedded basic-auth credentials
for Task 7's discovery logging.
Wires normalization into config_flow.validate_input before the client
is built, with a new invalid_base_url form error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
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
custom_components/openai_compatible/tts.py imports homeassistant.components.tts,
which imports homeassistant.components.ffmpeg, which does
`from haffmpeg.core import HAFFmpeg` at module level. Because
async_forward_entry_setups imports all of PLATFORMS (ai_task, conversation,
stt, tts) before setting any of them up, this one missing package failed
config entry setup entirely (SETUP_ERROR) and cascaded into the bulk of the
previous run's 104 failures: "Agent ... not found" in test_conversation.py,
KeyError('ai_task') in test_ai_task.py, and entry_not_loaded aborts /
KeyError('step_id') in test_config_flow.py's subentry flow tests.
Pinned to core tag 2026.9.2's homeassistant/components/ffmpeg/manifest.json.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
homeassistant.components.ai_task.task imports camera, conversation, image and
media_source; camera/img_util.py does `from turbojpeg import TurboJPEG` at
module level. Pinned to core tag 2026.9.2's
homeassistant/components/camera/manifest.json.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
The manifest declares dependencies: ["conversation"], and the ported tests
also exercise stt/tts. A full Home Assistant install resolves each core
component's own manifest.json requirements at runtime; bare pytest does not,
so CI failed with ModuleNotFoundError: No module named 'hassil' as soon as
entity.py's `from homeassistant.components import conversation` actually ran
(pythonpath fix in the prior commit got collection far enough to hit this).
Versions pinned to core tag 2026.9.2's own manifests:
homeassistant/components/conversation/manifest.json (hassil, gazetteer-matcher,
home-assistant-intents) and homeassistant/components/tts/manifest.json (mutagen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
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
- pyproject.toml: add pythonpath = ["."] so tests/ can import
custom_components.openai_compatible (CI was red at collection without it)
- pyproject.toml: ruff target-version py313 -> py314, matching HA 2026.9's
floor (already reflected in the CI workflow and requirements_test.txt)
- .gitignore: add .ruff_cache/
- remove custom_components/openai_compatible/quality_scale.yaml — a
core-repo-only concept; the manifest no longer declares a quality scale,
so the file asserted a tier this fork hasn't earned
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
Verbatim copy from home-assistant/core at tag 2026.9.2, with only the
domain constant and manifest changed. Apache-2.0, attribution in NOTICE.
Also fixed self-references the domain rename left dangling: the
config_entry selector's `integration:` field in services.yaml, the
`[%key:component::openai_conversation::...%]` translation
cross-references in strings.json, the deprecated-action descriptions
naming the old domain, and config_flow.py's instructions_url (now
pointing at this repo instead of the core docs page).
Local pytest run skipped per owner decision (see prior commit); files
validated with py_compile / json.load / yaml.safe_load instead. CI
(added in the prior commit) is the real gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
Local pytest execution is skipped by owner decision: no release of
pytest-homeassistant-custom-component that pins a Home Assistant
2026.9.x floor installs on Python 3.13 (0.13.317+ requires Python
3.14, which this workstation does not have). requirements_test.txt
is pinned to the correct >=0.13.317 range for CI (Python 3.14) and
files are validated with py_compile instead of a local pytest run.
CI workflows (test.yaml, validate.yaml) moved forward from a later
task since, with local testing gone, CI is the only gate and must
exist before the code it gates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4