LLM Gateway
Route your agents' LLM calls through STACK. You keep the provider's native request and response shape; STACK injects the API key from the vault, governs and logs the call, and meters it. The provider key never lives on the box your agent runs on.
Two ways in: the SDK (a one-line change to an existing Anthropic or OpenAI call), orproxy serve (a localhost endpoint any tool can point its base URL at, no code change). Supported providers today: Anthropic, OpenAI, OpenRouter.
This page is about calling an LLM through STACK. If instead you want to give an Anthropic-driven agent the STACK tool surface (STACK as an MCP server inside the Messages API), see Anthropic Agent SDK.
Before you start
- An operator account (getstack.run) and an API key.
- The LLM provider connected as a service at /services. STACK does not subsidize calls with its own keys; it injects yours.
Register the agent with the proxied-llm-bot profile. It sets the posture a bot that just runs needs: credential_access: proxy_only (never sees the raw key), accountability_mode: logged (every call recorded, no per-call approval gate), and key_mode: stack_managed (no enrollment step). Its first call works with no extra dance.
npx @getstackrun/cli agent register familybot --profile proxied-llm-botSDK path
Issue a passport once, then call the provider through stack.llm. The passport flows per call as a second argument so a long-running loop keeps working past the 15-minute token TTL; issuePassport returns a session that auto-refreshes about 60 seconds before expiry.
import { Stack } from '@getstackrun/sdk';
const stack = new Stack({ apiKey: process.env.STACK_API_KEY });
const session = await stack.issuePassport({ agent_id: 'agt_familybot' });
// Drop-in vs the Anthropic SDK — same body, plus the passport.
const res = await stack.llm.anthropic.messages.create(
{ model: 'claude-haiku-4-5-20251001', max_tokens: 256, messages: [{ role: 'user', content: 'Hi' }] },
{ passport: session },
);
console.log(res); // the provider's native response, unchangedStreaming
Set stream: true and the call returns an async iterable of the provider's parsed SSE events.
for await (const event of await stack.llm.anthropic.messages.create(
{ model: 'claude-haiku-4-5-20251001', max_tokens: 256, messages: [{ role: 'user', content: 'Hi' }], stream: true },
{ passport: session },
)) {
console.log(event);
}OpenAI and OpenRouter
await stack.llm.openai.chat.completions.create(
{ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
{ passport: session },
);
await stack.llm.openrouter.chat.completions.create(
{ model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
{ passport: session },
);Drop-in path: proxy serve
For a tool that already expects a provider key and a base URL (an existing SDK, a bot you did not write, any framework), run a localhost endpoint that speaks the provider's native shape. Point the tool's base URL at it and use any dummy key. Integration becomes a one-URL change with no code rewrite.
npx @getstackrun/cli proxy serve --agent familybot
# listening: http://127.0.0.1:8788
# anthropic -> http://127.0.0.1:8788/v1/messages
# openai -> http://127.0.0.1:8788/v1/chat/completionsThen point your tool at it:
ANTHROPIC_BASE_URL=http://127.0.0.1:8788 ANTHROPIC_API_KEY=dummy your-toolThe dummy key the tool sends is discarded, and the real provider key always stays in the vault. How the shim itself authenticates to STACK depends on the agent's signing model:
- stack_managed (the default, and what the proxied-llm-bot preset uses): the agent has no on-box keypair - STACK signs on its behalf. The shim authenticates with your operator credential (STACK_API_KEY or the session from auth login) and issues agent-scoped passports. Your operator credential is what sits on the box.
- customer_managed: the agent owns an Ed25519 keypair on the box (via agent enroll). The shim signs each call with a fresh 60-second agent token; only that one scoped agent key is on the box, no operator-wide credential. This is the strongest posture for an untrusted host.
Both keep the provider key in the vault; they differ in which STACK credential lives on the box. On a host you do not fully trust, register the agent --key-mode customer_managed and enroll it so only a scoped, sub-60-second-revocable agent key is present.
proxy serve binds 127.0.0.1 by default. Passing --host a non-loopback address exposes governed calls as this agent to anyone who can reach the port.
One command, end to end
quickstart sequences the whole path: register a safe agent, issue a passport, make one governed call, and print the audit row it produced.
npx @getstackrun/cli quickstartPer-agent controls
Two things happen to the request body before STACK forwards it, both on by default and both switchable per agent (dashboard agent settings, or llm_inject_system_prompt / llm_redact_pii on PATCH /v1/agents/:id):
- Context injection - STACK prepends a passport-context system message (agent identity, authorised services, active mission) so the model knows what scope it operates under. Roughly 150-250 input tokens per call; stable for the life of the passport, so provider prompt caching still applies.
- PII redaction - STACK scans user-role messages and replaces detected PII (email, phone, SSN, credit card, IBAN, IP address) with [REDACTED:<category>]. A no-op when a message contains no PII.
Turning PII redaction off means user-supplied PII flows to the LLM provider unredacted. That is an explicit operator decision; the audit log marks those calls pii_hits: -1 so skipped redaction stays visible.
See what agents spent
Every gateway call lands in the audit chain and the usage ledger.
const buckets = await stack.llm.usage.summary({ since: '2026-07-01T00:00:00Z' });
const rows = await stack.llm.usage.list({ limit: 50, provider: 'anthropic' });Related
- Anthropic Agent SDK — the reverse pattern (STACK as a tool inside Anthropic): /docs/integrations/anthropic
- Passport lifecycle — TTL, refresh, revocation: /docs/guides/passport-lifecycle
- Enforced mode — when to gate calls on approved intents instead of just logging: /docs/guides/enforced-mode