Skip to content

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.

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.

Terminal window
MANAGED_LLM_PROVIDER=anthropic # anthropic | cline
MANAGED_LLM_MODEL=claude-opus-5
MANAGED_LLM_API_KEY=sk-… # 🔒 secret
CREDITS_METERING_ENABLED=true # required — see below

MANAGED_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.

  • It never beats a real channel. The user’s own llm:// (or dashboard://) 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’s findByOwner excludes it regardless.
  • It never answers a pinned channel id. A step configured with a specific llmChannelId that 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 model is 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.

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.