Files
ha-openai-compatible/README.md
T
Adam SmithandClaude Opus 5 b79164b60c docs: record the provider-genericity audit
Writes up what is still OpenAI-specific in this fork, since the base URL
being configurable is what makes it reach a compatible endpoint, not what
makes it behave correctly against one. Four findings, each with what it
actually costs:

1. service_tier, store and prompt_cache_retention are sent on every
   request whatever the provider is (entity.py:515-563); reasoning,
   include and text.verbosity are gated on OpenAI name prefixes.
2. 23 startswith() model-name gates plus the UNSUPPORTED_* lists infer
   capability from OpenAI naming, which fails in both directions: service
   tier, code interpreter and web search are offered to providers that
   cannot honour them, while reasoning effort/summary and verbosity are
   withheld from any model whose name does not look like OpenAI's.
3. manufacturer="OpenAI" and the "Error talking to OpenAI" error text.
4. Repair issues citing core's 2026.9.0 removal schedule, which has
   passed and was never this fork's commitment, and an OpenAI-only
   organization-verification repair.

Two things worth having written down beyond the list itself:

- A proxy set to drop incompatible parameters hides finding 1 entirely
  and makes finding 2 worse, because a dropped parameter turns a
  misconfigured option into a silent no-op rather than an error. Testing
  only against such a proxy therefore says nothing about portability, and
  a strict server is the honest target. This reorders the priorities:
  finding 2 first, since it is what the user sees and the failure is
  silent; finding 1 matters for setups without a normalising proxy.
- The prerequisite decision is recorded rather than assumed. There is no
  capability discovery in the OpenAI API -- GET /v1/models returns ids,
  not feature support -- so the three candidates (per-entry capability
  toggle, host detection, per-endpoint checkboxes) are laid out with
  their trade-offs and explicitly marked unsettled.

No behaviour change; the README links it from a short "known
OpenAI-specific behaviour" section so it is findable from the front page.

Local: 170 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
2026-09-17 23:32:21 -04:00

211 lines
9.2 KiB
Markdown

# OpenAI Compatible for Home Assistant
[![test](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/test.yaml/badge.svg)](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/test.yaml)
[![validate](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/validate.yaml/badge.svg)](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/validate.yaml)
[![HACS: custom](https://img.shields.io/badge/HACS-custom-41BDF5.svg)](https://hacs.xyz)
Home Assistant's built-in **OpenAI** integration, with one change that matters: the
**API base URL is configurable**. Point Home Assistant's conversation agent, AI Task,
speech-to-text and text-to-speech at any OpenAI-compatible endpoint — LiteLLM, vLLM,
OpenRouter, a gateway of your own — instead of only `api.openai.com`.
Everything else is upstream Home Assistant code, vendored unmodified, so behavior and
options match the core integration you already know.
## Why this exists
Core's `openai_conversation` hardcodes OpenAI's endpoint. If you run a proxy for cost
control, routing, key management or privacy, or you self-host models, there is no way to
tell the core integration about it. This fork adds a **Base URL** field to the config
flow and threads it to every client it creates.
## How it is organised
**One entry per server or provider**, and **one endpoint per model** underneath it.
Setup asks only for the connection; nothing is created on your behalf. You then add
endpoints yourself, picking the type and the model each time — so a single LiteLLM entry
can carry a conversation agent on one model, another on a second, and a TTS endpoint on
a third.
Each endpoint is one of four types, and each becomes its own Home Assistant entity:
- **Conversation agent** — an Assist conversation entity, with the LLM Assist API,
tool calling, and the full upstream option set (model, temperature, top-p, max tokens,
reasoning effort, verbosity, web search, code interpreter, service tier, …)
- **AI Task** — for `ai_task.generate_data` / image generation
- **Speech-to-text** — an STT entity
- **Text-to-speech** — a TTS entity, with speed control
Plus two actions on the entry itself: `openai_compatible.generate_content` and
`openai_compatible.generate_image`.
## Requirements
- Home Assistant **2026.9.0** or newer
- An OpenAI-compatible endpoint (see [Endpoint compatibility](#endpoint-compatibility))
## Installation
### HACS (custom repository)
1. HACS → ⋮ → **Custom repositories**
2. Add `https://github.com/adamjs83/ha-openai-compatible`, category **Integration**
3. Find **OpenAI Compatible** in HACS, download it, and restart Home Assistant
4. **Settings → Devices & services → Add integration → OpenAI Compatible**
### Manual
Copy `custom_components/openai_compatible/` into your Home Assistant
`config/custom_components/` directory and restart.
## Configuration
### 1. Add the provider
| Field | Notes |
| --- | --- |
| **Name** | Label for this provider, e.g. `LiteLLM`. Leave blank to use the endpoint's host. |
| **Base URL** | Full URL including any version path, e.g. `https://api.openai.com/v1`. Passed to the OpenAI SDK as-is. |
| **API key** | Optional — **leave blank** for an endpoint that does not authenticate. |
Submitting it checks the connection with `GET /v1/models` and creates the entry. No
entities are created yet, deliberately: guessing them would mean pointing STT and TTS at
OpenAI model names your provider probably does not serve.
You can add **several entries for the same server** — one per provider-side key, say.
Upstream refuses this as a duplicate; this fork does not.
### 2. Add endpoints for the models you want
On the entry, use **Add conversation agent** / **Add AI task** / **Add speech-to-text** /
**Add text-to-speech**. Each flow offers a **model dropdown populated from that
provider's own `GET /v1/models`**, read when you open the form, so it lists what is
actually available right then. The dropdown also accepts a typed-in name, for a provider
that routes models it does not advertise (LiteLLM wildcard routes, for instance), and
falls back to a plain text box if the endpoint will not list models at all.
Repeat per model. Name each endpoint something you will recognise in Assist — the
subentry title becomes the entity name.
### Base URL rules
- Only `http` and `https` are accepted; the URL must include a host.
- Whitespace and trailing slashes are stripped.
- **Include the version path** if your server expects one. Most do: `/v1`.
- Credentials embedded in the URL (`https://user:pass@host/v1`) are **rejected**. Put
the token in the API key field — it is stored as a secret and kept out of logs, while
a base URL is echoed back in error and log messages.
- Re-authenticating (rotating the API key) **keeps** your configured base URL.
### Empty API keys
Local endpoints often require no auth. Leave the API key blank and the integration sends
the placeholder `not-required` — the OpenAI SDK refuses to build a client with no key at
all, so something has to be sent, and servers that ignore auth ignore it.
### Endpoint examples
| Server | Typical base URL |
| --- | --- |
| OpenAI | `https://api.openai.com/v1` |
| LiteLLM | `http://litellm.local:4000/v1` |
| vLLM | `http://vllm.local:8000/v1` |
| OpenRouter | `https://openrouter.ai/api/v1` |
| Azure OpenAI (via a proxy) | your proxy's URL |
## Endpoint compatibility
This is the important caveat. Upstream uses OpenAI's **Responses API**, not Chat
Completions, so a server that only implements `/v1/chat/completions` **will not work**
for conversation or AI Task.
| Feature | Endpoint the server must implement |
| --- | --- |
| Setup / connection test | `GET /v1/models` |
| Conversation, AI Task | `POST /v1/responses` |
| Text-to-speech | `POST /v1/audio/speech` |
| Speech-to-text | `POST /v1/audio/transcriptions` |
| `generate_image` action | `POST /v1/images/generations` |
You only need the endpoints for the subentries you actually add. Check your server's
docs for Responses API support before filing a bug — LiteLLM and vLLM have it in recent
versions; several popular local servers still do not.
### Model names
Pick from the dropdown and this mostly takes care of itself. Two upstream behaviours to
know about anyway:
- The fallback defaults are OpenAI's (`gpt-4o-mini`, `gpt-4o-mini-tts`,
`gpt-4o-mini-transcribe`), used only if you never choose a model. A new endpoint starts
with **Recommended settings** unticked so the flow always asks.
- Upstream's lists of models that don't support web search, image generation, reasoning
effort and so on are keyed to OpenAI names, so they won't match a third-party model
string. Options are offered based on those name patterns, and the endpoint's own error
is what you'll see if you ask for something it can't do.
## Known OpenAI-specific behaviour
The base URL is configurable, but much of the rest is still upstream's OpenAI-shaped
logic: some request parameters are sent to every provider, and which options the config
flow offers is inferred from OpenAI model-name prefixes. That means options can appear
for endpoints that cannot honour them, and be withheld from endpoints that could.
[`docs/provider-genericity.md`](docs/provider-genericity.md) has the full audit, what
each item costs, and the design decision that has to be made first.
## Relationship to Home Assistant core
`custom_components/openai_compatible/` is derived from
`homeassistant/components/openai_conversation/` at core tag **2026.9.2**
(commit recorded in [`UPSTREAM_COMMIT`](custom_components/openai_compatible/UPSTREAM_COMMIT)),
with the domain renamed to `openai_compatible` and the base URL made configurable.
Base-URL handling lives in its own module,
[`url_util.py`](custom_components/openai_compatible/url_util.py), deliberately separate
from the vendored files so re-syncing with core stays a clean merge.
Installing this alongside core's OpenAI integration is fine — different domain, separate
config entries. This is **not** an official Home Assistant integration and is not
affiliated with or endorsed by OpenAI.
## Development
Requires Python 3.14+ (Home Assistant 2026.9's floor).
```bash
pip install -r requirements_test.txt
pytest tests/ -v
```
The suite is core's `openai_conversation` test suite, ported as a regression net, plus
tests for the base-URL behavior this fork adds.
## Releases
HACS falls back to serving the default branch when a repository has no releases, so
every download looks identical and there is no way to tell what is installed. Tagged
releases fix that, and Home Assistant shows `manifest.json`'s version on the
integration page.
Versions are `0.x` while the config entry shape is still changing; `1.0.0` when it
settles.
To cut one:
```bash
python scripts/release.py 0.2.0
git push origin main --follow-tags
```
The script bumps `manifest.json`, commits, and creates the annotated tag `v0.2.0`. It
deliberately does not push — pushing the tag is what publishes. That triggers
[`release.yaml`](.github/workflows/release.yaml), which refuses to publish if the tag
and the manifest version disagree (HACS reads the tag, HA reads the manifest; when they
drift you install "v0.2.0" and it reports 0.1.0) and then creates the GitHub release.
## License
[Apache-2.0](LICENSE), inherited from Home Assistant core. See [NOTICE](NOTICE) for
attribution.