A ModelProvider speaking the OpenAI chat-completions wire format against a
configurable base URL. Ollama, vLLM, llama.cpp, LM Studio, LiteLLM, OpenRouter,
Azure and OpenAI itself all speak it.
That is what makes a local, air-gapped install a configuration choice rather than a fork.
No SDK
It depends only on the copilot’s domain package — for the port’s type, erased
at runtime — and Node built-ins. The surface is one POST /chat/completions and
a few lines of event-stream parsing, and a dependency for that would not earn
its place.
Install
npm install @apograph/copilot-provider-openaiimport { createOpenAiProvider } from '@apograph/copilot-provider-openai';
createOpenAiProvider(config.plugins.copilot.providers.openai);Configuration
{ baseUrl, models, apiKey?, headers?, capabilities?, timeoutMs?, maxRetries?, maxTokensField? }
| Variable | Default | What it does |
|---|---|---|
COPILOT_OPENAI_BASE_URL | unset | Setting it registers this backend |
COPILOT_OPENAI_MODELS | llama3.1 | Comma-separated. The first is the default |
COPILOT_OPENAI_API_KEY | empty | Leave empty for a local runtime wanting no auth |
baseUrl includes the version segment
http://localhost:11434/v1, not http://localhost:11434.
/chat/completions is appended, with any trailing slashes trimmed first. There
is no default: an endpoint nobody named is a backend that can only time out.
Pointing it somewhere
| Runtime | COPILOT_OPENAI_BASE_URL |
|---|---|
| Ollama | http://localhost:11434/v1 |
| vLLM | http://localhost:8000/v1 |
| LM Studio | http://localhost:1234/v1 |
| OpenAI | https://api.openai.com/v1 |
| LiteLLM, OpenRouter, Azure | whatever that deployment publishes |
The model list is a list
One endpoint and one credential back several models, so a user can switch mid-conversation. A run naming a model outside the list raises an unknown-model error rather than quietly answering on the default.
Two pieces worth knowing about
The retry policy is pure. Which statuses retry, the backoff, and honouring
Retry-After live in their own module with their own tests, and it runs
pre-stream only — once bytes are flowing, a retry would replay a partial
answer.
Tool-call fragments are assembled by index. Parallel tool calls interleave on the wire, arriving as fragments that have to be reassembled per call. That accumulator is the only piece here holding real state, so it is pinned directly rather than covered only through the provider.
capabilities is configurable here
Unlike the Claude adapter, there is no capability probe: the wire format is spoken by dozens of runtimes with different support for tools and streaming, and no endpoint reliably reports its own.
Declare what yours supports, or take the conservative default.