Features Apograph CMS on GitHub

Model providers

Copilot plugin @apograph/copilot-server@apograph/copilot-admin

Two adapters behind one port, and why a local model is a setting rather than a fork.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

The copilot reaches a model through a port — a framework-free interface with no vendor concepts in it. Two adapters ship, and the composition root constructs them.

Only the Claude adapter may import a vendor SDK. That constraint is what keeps the copilot itself model-agnostic.

The two shipped adapters

AdapterWhat it is
provider-anthropicNative Claude, over the official SDK. The default for tool-heavy work; it probes the Models API for live capabilities.
provider-openaiThe OpenAI wire format, with a configurable base URL.

Claude

ANTHROPIC_API_KEY=sk-ant-…
COPILOT_ANTHROPIC_MODELS=claude-opus-5,claude-sonnet-5,claude-haiku-4-5
ANTHROPIC_BASE_URL=            # optional: a gateway, proxy or regional endpoint

Setting the key is what registers the backend. The key never leaves the server — the admin talks to your API, and your API talks to the model.

Anything speaking the OpenAI wire format

COPILOT_OPENAI_BASE_URL=http://localhost:11434/v1
COPILOT_OPENAI_MODELS=llama3.1
COPILOT_OPENAI_API_KEY=        # empty for a local runtime that wants no auth

One adapter, one base URL, and the list of things it reaches is long: Ollama, vLLM, llama.cpp, LM Studio, LiteLLM, OpenRouter, Azure, OpenAI.

This is the point of the design. Running a model on your own hardware is a setting, not a fork — which matters rather a lot for a self-hosted CMS whose users chose it to keep their content on their own infrastructure.

There is deliberately no default base URL: an endpoint nobody named is a backend that can only time out.

The scripted one is not one of them

provider-fake is deterministic and needs no key and no network, and it is a private test fixture — unpublished, and not registered by the composition root. apps/server-e2e uses it to drive the whole tool loop without a network call; a deployment cannot.

A clone with nothing configured therefore has an empty provider list and no copilot, rather than a scripted one. See the scripted backend for why that changed. To work offline for real, point provider-openai at a local Ollama.

Capabilities are a profile, computed per run

Not every model supports every capability, and the copilot does not assume. A capability profile is resolved per run and decides what is offered to the model — which is enforced again at execution, so a tool that was not offered cannot be called even if the model invents its name.

The Claude adapter probes the Models API for live capabilities rather than carrying a hard-coded table that goes stale.

Switching models mid-conversation

Each provider declares several models, and a user can switch between them within a conversation from the model picker. An operator changes what is on offer by changing the configured list — no redeploy of the copilot packages.

The conversation remembers the model it is set to.

Adding your own adapter

Implement the port, construct it in the composition root, and add it to the provider list. The copilot packages need no change, and the plugin never learns which adapters exist — it takes a list of named, already-constructed providers.

CopilotPlugin({
    providers: [
        { name: 'claude', provider: createAnthropicProvider({}) },
        { name: 'ollama', provider: createOpenAiProvider({}) }
    ],
    config
});

The order is the setting

The first entry serves any run that names no provider — including runs started by an integration test or an API caller — and is what the model picker opens on. A keyless claude at the top of the list would be the house default and would fail on the first message, which is why an unconfigured backend is not registered at all.

Small local models need more room, and still may not cope

A tool loop asks a model to plan, call, read a result, and plan again. A smaller local model needs more round trips than a frontier one to reach the same answer, so it hits the step ceiling sooner — raise all three run limits together, and expect worse tool selection regardless. The port makes a local model possible, not equivalent.