Features Apograph CMS on GitHub

The scripted provider

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

Why a fresh install has no copilot at all, rather than a scripted one.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

@apograph/copilot-provider-fake is a scripted, deterministic ModelProvider with no network, no clock and no randomness. It is private and unpublished, and the composition root does not register it.

This was a shipped adapter and is no longer one

Earlier versions registered it last, so a clone with no API keys still had a working chat. That is no longer true, and the change was deliberate — see below. It is not on npm, so npm install @apograph/copilot-provider-fake will not resolve.

What a keyless install does instead

apps/server/src/plugins.ts registers only the backends a deployment has actually configured. A fresh clone configures none, so the provider list is empty:

export function copilotProviders(config: ApographConfig): ProviderRegistration[] {
    const { claude, ollama } = config.plugins.copilot.providers;
    return [
        ...(claude
            ? [{ name: 'claude', provider: createAnthropicProvider(claude) }]
            : []),
        ...(ollama
            ? [{ name: 'ollama', provider: createOpenAiProvider(ollama) }]
            : [])
    ];
}

An empty list is a valid state, because COPILOT_ENABLED is false by default. The plugin may hold no providers only while it is switched off; turning it on with nothing configured fails at boot, where a misconfiguration is cheapest to diagnose.

So the two states are: the copilot is off and there is nothing to call, or it is on and every backend it can reach is one somebody configured. There is no third state where it answers out of a script.

Why a canned answer was the wrong default

A scripted adapter registered by the host is indistinguishable, from the outside, from a working one. A production deployment that lost its API key — or never had one — kept serving the chat panel and answered every question with a canned sentence. Nobody reads that as “the model is unreachable”; they read it as the product being bad at its job.

Failing at boot when the copilot is enabled with no backend puts that same misconfiguration in front of the person who can fix it, at the moment they can fix it.

It is still the test fixture

The package has not gone anywhere. apps/server-e2e imports it to drive the whole tool loop with no API key and no network, and determinism is what makes that worth doing: the run engine is a non-deterministic multi-step loop, so a flaky fake would make every assertion downstream of it flaky too.

Its calls member — every request served, in order — is the assertion target for the negative path the authority model makes mandatory: a viewer’s run must be verified not to be offered write tools. That is an assertion about the tools in calls[0], made without a model, a key, or a network.

The identity plugin's scripted provider is different

@apograph/identity-provider-fake is published and is installed in every generated app. It is not registered by the default composition root either, but for a different reason: an adapter only matters once a composition root names it, and a scripted identity provider signs people in without anyone authenticating.

Running offline for real

To exercise the copilot with no hosted key, run a local model over the OpenAI-wire adapter — copilot-provider-openai reaches Ollama, vLLM and anything else speaking that protocol, and it is a real backend giving real answers.

Writing a provider covers the port and its conformance kit. The media plugin ships an in-memory adapter on comparable reasoning — that one is published, because a storage backend with no bytes behind it cannot be mistaken for a working one.