# OpenAI-compatible Endpoints

Using any OpenAI-compatible `/v1` endpoint as a backend: how to add one, where its URL and key live, and what has been tested.

## Anything OpenAI-compatible

Beyond those eleven, the `compat` entry (**Local/Custom**) is not a twelfth vendor — it is the escape hatch that makes the list open-ended. Point it at any OpenAI-compatible `/v1` base URL, with an optional key, and it becomes a usable backend: Ollama, LM Studio, vLLM, LiteLLM, a self-hosted gateway, a vendor that shipped an OpenAI-shaped API last week. Register as many as you need — each one is its own named entry.

Two local endpoints are built in (`configs/jsons/local_compat.json`): **Ollama Local** at `http://localhost:11434/v1` and **Llama.cpp Local** at `http://localhost:8080/v1`. `/model add` probes both with `GET /models` in parallel (500 ms budget) and lists the ones that answer at the top of the provider list, going straight to model selection with nothing written to `config.json`. Endpoint models are registered as `<name>@<model>` with the endpoint name lowercased (`ollama@gemma3:4b`); the older `compat[NAME]@<model>` form is still accepted and rewritten whenever `config.json` is loaded or saved.

Model lists are fetched live when a model is added (TUI `/model` → `add`, or `GET /v1/provider/:provider/models`); for a local or custom endpoint the list comes from the endpoint's own `GET /models`. There are no static model catalogs in the repository.

## Adding a custom OpenAI-compatible endpoint

Use **Local/Custom** (`compat`) and point it at any endpoint that accepts the OpenAI Chat Completions schema. URL convention follows Zed: **enter the URL up to `/v1`** (e.g. `http://192.168.1.10:4000/v1`); the router appends `/chat/completions`. The built-in Ollama and llama.cpp ports need no entry.

### Storage split (URL vs key)

| What | Where | API |
|---|---|---|
| URL | `~/.config/agenvoy/config.json` `compats[]` — `{provider, url}`, provider name uppercased | `config.UpsertCompat` / `config.GetCompatURL` (`internal/session/config`) |
| API key | OS keychain | `keychain.Set("COMPAT_<NAME>_API_KEY", value)` |

`GetCompatURL` checks `compats` first, then the built-in local endpoints. There is no `COMPAT_<NAME>_URL` keychain key — it was removed after a bug where the TUI wrote the URL to config while the runtime read the keychain and always fell back to localhost.

### Tested compat targets

| Target | Works | Notes |
|---|---|---|
| Ollama | Yes | built in at `http://localhost:11434/v1` |
| llama.cpp server | Yes | built in at `http://localhost:8080/v1` |
| LM Studio | Yes | |
| vLLM | Yes | `--enable-auto-tool-choice --tool-call-parser <name>` for tool use |
| LiteLLM proxy | Yes | virtual key as Bearer token |
| Groq / Together / DeepInfra / Fireworks | Yes | |
| Azure OpenAI | No | needs an `api-key` header (not `Bearer`) plus `?api-version=` — not supported |
| opencode Go (`opencode.ai/zen/go/v1`) | No | requires a per-conversation `x-opencode-session` header; missing it returns `400 MissingSessionID` — not supported |

### What counts as compatible

A target qualifies when the request body and `Authorization: Bearer <key>` are the whole contract. Agenvoy carries no vendor-specific headers through the compat channel, so an endpoint that demands one is out of scope regardless of how OpenAI-shaped its body is. Only three kinds of backend are in scope: vendors that serve their own models, OpenAI-compatible NIM, and Cloudflare.

opencode Go is the working example of the boundary. Since its [2026-09-03 announcement](https://x.com/opencode/status/2095410501400289576) the gateway rejects any request without `x-opencode-session`, a private header whose value has to change per conversation so the request lands on the node holding that conversation's cache. Chat Completions is stateless — the conversation lives in `messages` and the protocol has no session concept — so requiring the client to track and rotate a vendor header puts the endpoint outside the compatible set. Opening the compat channel to custom headers for one vendor would open it for every vendor, so it stays closed.
