The managed LLM
A new user should be able to build their first Gmail + Calendar automation without
first going and buying an Anthropic API key. The managed LLM is how: when the
org has connected no llm:// channel of its own, AI steps run on the platform’s
key and the run is debited from that user’s credit balance like any other.
Turning it on
Section titled “Turning it on”Three variables, all required together. A partially-configured deployment has no managed LLM at all — deliberately, since a provider and model with no key would build a client that 401s on every run, and the failure would surface as a broken automation far from its cause.
MANAGED_LLM_PROVIDER=anthropic # anthropic | clineMANAGED_LLM_MODEL=claude-opus-5MANAGED_LLM_API_KEY=sk-… # 🔒 secretCREDITS_METERING_ENABLED=true # required — see belowMANAGED_LLM_API_KEY must never be added to apps/api/config/allowed.config.types.ts;
that file publishes keys over /api/config.
Why it needs CREDITS_METERING_ENABLED=true
Section titled “Why it needs CREDITS_METERING_ENABLED=true”That flag gates two things: writing the ledger debit, and the out-of-credits gate on AI steps. With it off, usage is still captured (dark telemetry to validate the tariff) but nothing is debited and nobody is ever blocked.
Serving the platform’s key in that state would be giving the model away. So the
lend refuses while metering is off, and refuses again for a user whose balance is
spent. Both checks live in ManagedLlmConnectorRepository, which is the only
thing that can produce the managed credentials — so they hold for every caller,
including the compose usecases that never ran an allowance check of their own.
A failing allowance check is treated as “no”. A billing outage must close the key, not open it.
What it does not do
Section titled “What it does not do”- It never beats a real channel. The user’s own
llm://(ordashboard://) connector is asked first, every time. The managed row only exists in the gap where the answer was null. - It never appears in
/channels. That page lists things the user connected and can disconnect; a row they can neither edit nor delete does not belong in it. The repository backing those routes is not wrapped at all, and the decorator’sfindByOwnerexcludes it regardless. - It never answers a pinned channel id. A step configured with a specific
llmChannelIdthat no longer resolves still fails loudly, as it did before — answering the pin with the managed key would silently switch the model out from under a user who had explicitly chosen one. - It never runs a model the caller picked — with one exception. A caller
naming a bare
modelis refused and told so. A caller naming a role (planner,worker,reader) runs that role’s model, because a role asks for a cheaper model than the default rather than a more expensive one, and the set it may reach is a fixed table (ROLE_MODELS) rather than anything the caller supplies. Refusing roles would push every squad tier onto the managed default, which is the expensive outcome the refusal exists to prevent. See the AI gateway.
How it is wired
Section titled “How it is wired”readManagedLlm(env) (apps/api/llm/managedLlm.ts) reads the three variables.
withManagedLlm(repo, options) wraps an IConnectorRepository — returning it
untouched when there is nothing to lend — and is applied at the AI build sites
(buildWork, buildPostDrafts, buildBlogPosts).
The decorator sits at the repository seam rather than on the AI gateway’s
signature because that function has thirteen call sites across jobs, publishing,
triage, accounting and observation. A fallback parameter would have to be
threaded through every one of their build* wirings and through every step added
afterwards; wrapping the repository is one change per build site and covers steps
that do not exist yet.
Related
Section titled “Related”- Environment variables — the inventory entry.
- Jobs architecture — where AI steps resolve their model.
