233 lines
11 KiB
Markdown
233 lines
11 KiB
Markdown
# OpenAI Compatible for Home Assistant
|
|
|
|
[](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/test.yaml)
|
|
[](https://github.com/adamjs83/ha-openai-compatible/actions/workflows/validate.yaml)
|
|
[](https://hacs.xyz)
|
|
|
|
> **Fork of [adamjs83/ha-openai-compatible](https://github.com/adamjs83/ha-openai-compatible) v0.2.1**
|
|
> with fixes for running it against a proxy that serves Claude models. Drop it once
|
|
> upstream has fixed these issues:
|
|
>
|
|
> - [#3](https://github.com/adamjs83/ha-openai-compatible/issues/3): a tool call with an
|
|
> empty argument string (tools without parameters) is treated as `{}` instead of
|
|
> raising `JSONDecodeError`.
|
|
> - [#4](https://github.com/adamjs83/ha-openai-compatible/issues/4): earlier web searches
|
|
> are not replayed in the history; they carry no results, and the model then disowns
|
|
> its own answers and searches again.
|
|
> - [#5](https://github.com/adamjs83/ha-openai-compatible/issues/5): the home location
|
|
> lookup for web search uses the subentry's model instead of `gpt-4o-mini` and asks
|
|
> as a user message (Anthropic needs one); a failed lookup no longer blocks saving
|
|
> the options, and empty location fields are left out of the web search tool.
|
|
>
|
|
> HACS only installs from GitHub, so this fork is installed by copying
|
|
> `custom_components/openai_compatible` into the Home Assistant config.
|
|
|
|
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.
|
|
|
|
The manifest requires `openai>=2.45.0` rather than core's exact pin: Home Assistant
|
|
installs custom-integration requirements against its own package constraints, so an
|
|
exact pin stops the integration loading whenever core bumps `openai`.
|
|
|
|
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.
|