@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.
Related
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.