AI Gateway
Point Claude Code, Codex, Gemini CLI, Cursor, Aider, or any Azure-OpenAI- or Ollama-speaking tool at a governed Scrydon endpoint — the developer keeps their tool, the admin keeps model policy, per-developer cost attribution, and audit.
The AI Gateway is a single endpoint an administrator hands to developers. Their existing coding tool points at it instead of a vendor API, and nothing about how they work changes. What changes is that every turn now runs through the same governed pipeline every other Scrydon capability call uses — model policy, DLP, moderation, audit, and metering — and is attributable to the developer who made it.
There is no second execution path. The router is a protocol translator in front of the pipeline your platform already runs.
Choose the walkthrough for your identity: an AI app connected from the browser, your Scrydon account, a service account in Claude Code, or a CI pipeline.
Two facts to have in mind before you start. Cost figures are indicative — Scrydon's own estimate from catalog pricing, not a provider invoice, so they will not agree to the cent. And the models a developer can reach are the ones you enabled: the router enforces your allowlist at dispatch, so a model you have not enabled is refused no matter what a tool asks for.
Connect your AI apps
Use the tools your organisation has connected to Scrydon (email, documents, tickets) from Claude Desktop, Codex, Cursor, or Claude Code. Nothing needs to be installed first. This connects tools only: the app keeps its own sign-in and its own models. To route an app's model traffic through the gateway as well, follow For developers.
Open Settings → Account → AI Gateway and choose the tab for your app. Every tab has the same three steps.
Create your access key
Choose Create key. If you belong to more than one environment and they give apps different tools, pick one first: the environment decides which tools the app can use. When every environment has the same tools, Scrydon picks one for you and names it under Environment; open it if you want usage recorded under another. Scrydon shows the key once, under Copy your key now, and it expires after your organisation's router-key lifetime.
A key belongs to the tab it was created on, and switching tabs keeps it. Reloading the page does not: if you reload before you finish, create a new key and revoke the unused one on the Your keys tab.
Connect the app
| App | What to do |
|---|---|
| Claude Desktop | Choose Download the extension and open the file. Choose Install, then paste your key when Claude asks for it. Check that the extension is on under Settings → Extensions in Claude. |
| Codex | Run the two commands the tab shows. The first saves your key on this computer, the second adds Scrydon to Codex. Then open a new terminal window. |
| Cursor | Run the command that saves your key, then choose Add to Cursor. Quit and reopen Cursor so it can read the key. |
| Claude Code | Run the two commands the tab shows, then open a new terminal window. |
On macOS, the Codex commands look like this:
echo 'export SCRYDON_API_KEY="scry_…"' >> ~/.zshrc
codex mcp add scrydon --url 'https://auth.your-company.com/api/mcp/v1' --bearer-token-env-var SCRYDON_API_KEYThe Windows tab saves the key with setx instead. Only the first command contains the key.
The registration and the Add to Cursor link name the SCRYDON_API_KEY variable, and the Claude
Desktop extension asks for the key when it installs. Nothing that ends up in shell history, browser
history, or a downloaded file carries the key itself.
Check that it works
Ask the app "Which Scrydon tools can you use?". The tab shows Waiting for … until the app first uses your key, then Connected with the time it did.
The tools you see come from the extensions your organisation has turned on for that environment. A tool that works as you, such as reading your email, also needs your own account connected under Settings → Account → Connected Accounts; until then the app reports that the tool has no connected account. An action that needs a confirmation in Scrydon cannot run from another app.
The Developer tools tab holds the base URLs and shell lines for routing model traffic. They are
the same lines scrydon router env prints.
The Your keys tab lists every key you created for an AI app in this organisation, newest first,
including keys scrydon router setup made: its environment, when an app last used it, and when it
expires. Revoke a key there when you stop using the app, or if someone else may have seen it. The
app is refused from its next call.
For developers
Install the CLI and sign in
bunx scrydon login https://auth.your-company.comWhich URL? It depends on how your Scrydon is deployed, and both shapes
accept a login — so the wrong one signs you in and then 404s at router setup.
- Subdomain install (services on their own hosts): use the platform
host, often
auth.your-company.com. That is the shape every example on this page uses. - Subpath install (every service behind one host): use that single
host —
https://scrydon.your-company.com— with no path. Substitute it forauth.your-company.comeverywhere below.
Not sure? curl -o /dev/null -w '%{http_code}\n' <url>/api/auth/get-session
answers 200 on the right origin. Your administrator can confirm the shape;
it is routing.mode in the Helm chart.
This opens a device-authorization flow (RFC 8628). The CLI prints a short code and a URL; open the URL in a browser where you are already signed in to Scrydon, confirm the code matches, and approve.
The token is written to ~/.scrydon/credentials.json with mode 0600.
Running this in CI? Don't — a pipeline has no browser to approve a device code in, and this flow authenticates a person. See For pipelines below: a pipeline authenticates as its own service account with scrydon login --service-account.
Create a router key
bunx scrydon router setupThis mints an environment-scoped key and prints the shell lines to paste:
export ANTHROPIC_BASE_URL="https://auth.your-company.com/api/router"
export ENABLE_TOOL_SEARCH="true"
export OPENAI_BASE_URL="https://auth.your-company.com/api/router/v1"
export GOOGLE_GEMINI_BASE_URL="https://auth.your-company.com/api/router"
export ANTHROPIC_API_KEY="scry_…"
export OPENAI_API_KEY="scry_…"
export GEMINI_API_KEY="scry_…"If Claude Code is installed, setup also registers one user-scoped HTTP MCP
server named scrydon at
https://auth.your-company.com/api/mcp/v1. The registration stores
Authorization: Bearer ${ANTHROPIC_API_KEY} literally, so the router key is not
copied into Claude's configuration. If Codex is installed, setup registers the
same server with Codex, which stores only the variable's name
(bearer_token_env_var = "OPENAI_API_KEY") and reads the key from it each time
it connects. For both, an existing correct registration is kept and a stale one
is updated.
The two base URLs differ by one path segment, and that is not a typo. The Anthropic SDK appends /v1/messages to its base URL; the OpenAI SDK appends /chat/completions. Both land on the same endpoint from different starting points. Use the lines the CLI printed rather than composing them by hand — a wrong segment produces a bare 404 with nothing in it to explain the cause.
ENABLE_TOOL_SEARCH makes Claude Code keep the full MCP catalog locally and
send only the relevant tool definitions with each model request. Keep this line
when using a custom ANTHROPIC_BASE_URL; without it, Claude can send every
configured MCP tool to a provider at once and exceed that provider's tool
limit.
The key expires after your organization's router-key lifetime — 90 days unless an admin
changed it under Settings → Governance → Credentials → Policy. A request for longer is cut to
the organization's maximum, which can never exceed 365 days. Run setup again for a new key.
If you belong to more than one environment, setup asks which one to use — organisation and environment, and nothing else. A key is scoped to that pair, which is what an extension connection is keyed by, so the choice decides which models and tools you can reach and where the usage is recorded. There is no workspace to pick: connections are not scoped to one.
Confirm the organisation tool broker
The preceding router setup command configures this automatically and prints:
✓ Claude MCP configured (https://auth.your-company.com/api/mcp/v1)
✓ Codex MCP configured (https://auth.your-company.com/api/mcp/v1)If Claude Code was not installed when setup ran, the CLI prints this fallback command. Run it after installing Claude Code:
claude mcp add-json --scope user scrydon \
'{"type":"http","url":"https://auth.your-company.com/api/mcp/v1","headers":{"Authorization":"Bearer ${ANTHROPIC_API_KEY}"}}'The fallback for Codex is:
codex mcp add scrydon --url 'https://auth.your-company.com/api/mcp/v1' --bearer-token-env-var OPENAI_API_KEYClaude discovers the enabled extension tools from Scrydon and searches that catalog locally. When it invokes an exact tool, the call returns to Scrydon, which resolves the current organisation binding, policy, and OAuth account before contacting the vendor. Vendor credentials are never installed in Claude Code.
For a user-delegated OAuth tool, Scrydon uses the connected account owned by the user who created the router key. It requires exactly one matching account on the selected product connection: no match, or more than one match, fails the tool call with remediation in the error. Scrydon never falls back to the organisation's shared execution account. OAuth app installations, OAuth client credentials, API keys, and credential-free products keep their organisation-managed behavior.
If GitHub, Slack, or another service is already available through a Scrydon extension, you do not need that vendor's separate MCP server. Directly configured third-party MCP servers bypass Scrydon's extension governance and also add their tools to Claude's local catalog.
Check it end to end
bunx scrydon router doctorChecking https://auth.your-company.com
✓ credential accepted
✓ 4 models available
anthropic/claude-opus-5
anthropic/claude-sonnet-5
openai/gpt-5
openai/gpt-5-mini
✓ completion succeeded (claude-opus-5 via anthropic)
✓ Claude MCP registered and connected
https://auth.your-company.com/api/mcp/v1
✓ Codex MCP registered
https://auth.your-company.com/api/mcp/v1
✓ 61 organization tools published by Scrydon MCP
Everything checks out. Models route, Claude Code and Codex are registered, and organization tools are published.
Personal OAuth account readiness is checked when each tool is called.doctor walks both planes: the key authenticates, your organisation has models
enabled, a real completion round-trips, Claude's effective scrydon MCP entry
is connected, Codex's entry points at this installation, and Scrydon publishes a
non-empty tool catalog for the selected environment. A tool that is not installed
is skipped. Doctor fails when an installed tool is not registered, or when
neither is installed. Codex has no command that connects to a server, so for
Codex doctor checks the registration and the catalog check proves the endpoint. The catalog check is live but read-only; doctor does not invoke a
Microsoft, GitHub, or other vendor action, refresh OAuth tokens, or prove that
you connected an account for every published tool. Personal OAuth readiness is
checked when that exact tool is called. Run doctor first whenever something
looks wrong — every failure below otherwise surfaces inside your other tool,
as a vendor-shaped error that names none of the causes.
Use your tool
Start your tool exactly as you normally would. Most read the environment variables you just set; the rest take the router URL in their own settings:
| Tool | What it reads | Notes |
|---|---|---|
| Claude Code | ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY | Keep ENABLE_TOOL_SEARCH="true". |
| Codex | OPENAI_BASE_URL, OPENAI_API_KEY | Codex speaks the Responses API; the router serves it at …/v1/responses. openai_base_url in ~/.codex/config.toml is the equivalent setting. |
| Gemini CLI | GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY | Pick a model from scrydon router doctor — ids look like google/gemini-2.5-pro. |
| Cursor, Aider, Continue, OpenCode | OPENAI_BASE_URL, OPENAI_API_KEY | Chat and embeddings both route. |
| Azure OpenAI SDK clients | endpoint https://auth.your-company.com/api/router, api-key = your router key | The deployment name is the model id (azure-foundry/gpt-5); api-version is accepted and ignored. |
| JetBrains AI Assistant, Zed, Open WebUI (Ollama mode) | Ollama host https://auth.your-company.com/api/router | The tool must be able to send Authorization: Bearer <router key>; keyless access does not exist. |
Commands
| Command | What it does |
|---|---|
scrydon router setup | Mint a key, print the environment, and register Scrydon's MCP server with Claude Code and Codex, whichever are installed. --name labels the key; --no-key omits it from output. |
scrydon router env | Print the export lines again. |
scrydon router status | What this machine is configured for. Local state only — it does not contact the server. |
scrydon router doctor | Check model routing, the Claude Code and Codex MCP registrations, and the live organization tool catalog. --model picks which model to probe with. |
scrydon router forget <url> | Remove the local key. Does not revoke it — do that under Settings → Account → AI Gateway → Your keys. |
When something fails
The router answers in your tool's own error dialect, so the message reaches you rather than being swallowed. What the common ones mean:
| What you see | What happened | What to do |
|---|---|---|
authentication_error / invalid_api_key | The key is wrong, expired, or revoked. | scrydon router setup to mint a new one. |
permission_error naming a model | That model is not enabled for your organisation. | Ask an administrator, or use one from scrydon router doctor. |
invalid_request_error mentioning a policy | A guardrail (DLP or moderation) refused the turn. | The message says which. This is deliberate and retrying will not help. |
rate_limit_error mentioning a spend cap | You, your team, or the organisation reached a budget. | Retry-After is the real time until the period resets — hours, not seconds. An administrator can raise the cap. |
Invalid 'tools': array too long | Claude Code is not deferring MCP tools locally. | Re-run scrydon router env and export ENABLE_TOOL_SEARCH="true" before starting Claude Code. |
Claude MCP registration missing | Models are routed, but Claude was never connected to Scrydon's tool broker. | Re-run scrydon router setup, or run the exact claude mcp add-json command doctor prints. |
Codex MCP registration missing | Codex is installed, but it was never connected to Scrydon's tool broker. | Re-run scrydon router setup, or run the exact codex mcp add command doctor prints. |
no organization tools are enabled | The MCP connection works, but its environment-scoped catalog is empty. | Ask an administrator to enable the extensions in Settings → Platform → Extensions. |
Your Scrydon identity has no connected account | The tool uses delegated OAuth, but the router-key owner has no account on that product connection. | Connect your account in Settings → Account → Connected Accounts, then retry the tool call. |
Your Scrydon identity has multiple connected accounts | More than one personal account matches, so Scrydon refuses to choose one arbitrarily. | Disconnect the unused account in Settings → Account → Connected Accounts, then retry. |
A 404 on every call | The base URL is missing or has the wrong path segment. | scrydon router env. |
Tool calls Scrydon refuses. When Scrydon refuses an MCP tool call — no connected account, a guardrail, a spend cap, an unavailable provider — the call returns a tool result marked as an error (isError: true) whose text is the sentence from the table above, so your AI app can read it and tell you what to do. The Scrydon code and HTTP status are in _meta["scrydon/error"]. Only a request Scrydon cannot accept at all — an unknown tool name, malformed parameters, or an access key it does not recognise — is a JSON-RPC error.
Errors after a stream starts. A streamed turn that fails partway is still an HTTP 200: the stream ends with one error event in your tool's dialect, after whatever text was already delivered. That event carries the same Scrydon code and details a non-streamed error does, so a policy refusal mid-answer still names its policyCode. They sit under scrydon at the top of the Anthropic and Ollama error bodies, under error.scrydon for OpenAI Chat Completions, on the error event itself for OpenAI Responses, and in error.details[] for Gemini. A stream the provider stopped without finishing ends with code INTERNAL_ERROR and details.reason STREAM_ENDED_WITHOUT_TERMINAL. Treat that answer as truncated, not as complete. It is recorded as a failed turn, not a successful one.
What is not supported
The router refuses these rather than ignoring them, because a parameter silently dropped gives you wrong output with no way to find out:
n,logprobs,top_logprobs,seed,logit_bias,presence_penalty,frequency_penalty(and their Gemini / Ollama spellings)- the deprecated
functions/function_callfields — usetools/tool_choice - server-side conversation state: Responses
previous_response_id,item_reference,background; GeminicachedContent. Send the full input each turn — your tool already does. - a vendor's hosted tools (
web_search,file_search,googleSearch,codeExecution, …). Organisation tools reach you through the Scrydon MCP server instead, governed. - pre-tokenized embedding input (integer arrays) — the router has no tokenizer for the target model.
- a single text part above 1,000,000 characters (roughly 250k tokens); the request as a whole is bounded at 16 MiB.
- Anthropic
count_tokens, Ollama/api/generate,/api/embedand model management.
Sending any of them returns a 400 naming the field. Sending them as null (which some SDKs do for unset options) is fine. Fields that carry no request semantics for a hosted model — store, include, safetySettings, num_ctx, keep_alive, user — are accepted and ignored.
The OpenAI-compatible /chat/completions endpoint carries provider reasoning in the optional reasoning_content field, including streaming deltas and assistant history for tool follow-ups. Reasoning stays separate from visible content; providers that do not expose reasoning do not produce this field. The Anthropic /v1/messages shape carries thinking blocks in full, including their signatures.
Use Claude Code with a service account
Use this walkthrough to send a message from your terminal as an organization-owned service
account. You need Bun, curl, jq, and Claude Code
installed. Your Scrydon deployment and CLI must support service-account machine-token login.
For CI jobs with an OIDC provider, use secretless federation.
Prepare the service account
As an organization admin, open Settings → Members → Service accounts and create an account
with capabilities:write. Pin it to an environment with a configured Claude provider connection
and an enabled Claude model. Copy its client ID and the secret shown once at creation; rotate
the secret if you no longer have it.
This login uses the account's ordinary client ID, not its Federated client ID. The secret
authenticates the account to Scrydon; Claude receives the short-lived bearer issued by that login.
You do not need scrydon router setup for this identity. bunx scrydon router doctor
currently checks the separate router key saved by personal-account setup; use the bearer
model-list request and Claude message below to verify this service account.
Log in from your terminal
Run these commands in Bash or Zsh. Replace the URL with your platform origin, without a trailing slash, and replace the client ID. The prompt reads the secret without echoing it or putting its value in shell history.
export SCRYDON_PLATFORM_URL="https://auth.your-company.com"
export SCRYDON_CLIENT_ID="YOUR_SERVICE_ACCOUNT_CLIENT_ID"
printf 'Client secret: '
IFS= read -rs SCRYDON_CLIENT_SECRET
printf '\n'
bunx scrydon login "$SCRYDON_PLATFORM_URL" \
--service-account "$SCRYDON_CLIENT_ID" \
--service-account-secret "$SCRYDON_CLIENT_SECRET" \
--ttl 2h
unset SCRYDON_CLIENT_SECRETContinue only after login succeeds. If the CLI redirects you from an application URL to its
platform URL, update SCRYDON_PLATFORM_URL to the platform URL it prints before the next step.
The credential file is keyed by that canonical URL. --ttl 2h requests two hours; organization
policy and the deployment ceiling can shorten it, and the CLI reports the issued lifetime.
Configure Claude and choose a model
In the same terminal, load the bearer saved by login and configure Claude's gateway:
TOKEN="$(jq -er --arg url "$SCRYDON_PLATFORM_URL" \
'.[$url].token' \
"${SCRYDON_CREDENTIALS_DIR:-$HOME/.scrydon}/credentials.json")"
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN
export ANTHROPIC_BASE_URL="$SCRYDON_PLATFORM_URL/api/router"
export ANTHROPIC_AUTH_TOKEN="$TOKEN"
export ENABLE_TOOL_SEARCH=true
unset TOKEN
curl --fail --silent --show-error \
"$ANTHROPIC_BASE_URL/v1/models" \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
| jq -r '.data[].id'The base URL ends at /api/router: Claude appends /v1/messages. ANTHROPIC_AUTH_TOKEN sends
the Scrydon bearer in the Authorization header. These exports configure this terminal and the
Claude process started from it. See Claude's gateway setup
if existing Claude settings override your shell configuration.
Copy an enabled Claude model ID from the list, including any provider prefix, into the command below. An empty list means an administrator needs to check the provider and model configuration for the account's environment.
Send your first message
export SCRYDON_CLAUDE_MODEL="PASTE_AN_ENABLED_CLAUDE_MODEL_ID"
claude --model "$SCRYDON_CLAUDE_MODEL" \
-p "Reply with one sentence confirming you received this message."-p prints the response and exits. For an
interactive conversation, run:
claude --model "$SCRYDON_CLAUDE_MODEL"In the interactive session, /status should show your gateway base URL and the environment
token as the credential source. Check Settings → Governance → Cost Insights → Usage, select
AI Gateway and the Principal breakdown: the request belongs to the service account's
service account, shown by its name, not your personal account. Listing models alone does not
demonstrate completion spend.
When the bearer expires, exit Claude, repeat login and token export, then start Claude again. Do not persist the bearer in your shell profile or commit it to Claude settings. Disabling the service account rejects its bearer on the next authenticated request; rotating its client secret only changes future logins. See credential lifecycle.
For pipelines
A CI pipeline is not a developer at a terminal — there is no browser to approve a device-flow login in, and the credential it holds needs to be revocable and time-bounded on its own terms, not borrowed from a person's session. A service account is the pipeline's own identity.
Create the account
Settings → Members → Service accounts → New service account. Grant it the capabilities:write scope — the same scope every router call declares — and pin its environment.
The environment is a property of the account, not of a flag or the token: a dev pipeline and a prd pipeline are two separate service accounts, each pinned to its own environment, not one account switched between them at run time. See Service accounts for what pinning does and how to change it later.
Store the credentials
For CI providers that issue OIDC tokens, use secretless federation. That recipe registers the issuer, attaches an exact pipeline subject, and exchanges a fresh assertion with the account's Federated client ID for an AI Gateway bearer. It requires no stored Scrydon client secret. The CLI secret-login instructions below are the alternative when your pipeline cannot federate.
Save the client id and secret as your pipeline's repository secrets — for example SCRYDON_CLIENT_ID and SCRYDON_CLIENT_SECRET in GitHub Actions.
Unlike the bearer it is exchanged for, the secret is not meant to be minted per run — but it does expire. Your organization has a maximum lifetime for it (180 days by default, up to a 365-day hard ceiling); Service accounts covers where that number comes from and how rotation works. Rotating replaces the secret immediately — there is no overlap window — so update the pipeline's stored secret in the same change that rotates it.
Authenticate
Login and export belong in the same step. Each - run: step is a fresh
shell, so exporting a variable in one step and reading it in the next reaches
nothing — the export dies with the shell that made it. A separate step
invoking your coding tool needs the variables written somewhere later steps
actually inherit: on GitHub Actions, that is $GITHUB_ENV; other CI systems
have their own equivalent (a shared env file, a pipeline-variable API — check
yours before assuming export alone is enough).
- run: |
scrydon login https://auth.your-company.com \
--service-account "$SCRYDON_CLIENT_ID" \
--service-account-secret "$SCRYDON_CLIENT_SECRET" \
--ttl 2h
TOKEN="$(jq -r '."https://auth.your-company.com".token' ~/.scrydon/credentials.json)"
{
echo "ANTHROPIC_BASE_URL=https://auth.your-company.com/api/router"
echo "OPENAI_BASE_URL=https://auth.your-company.com/api/router/v1"
echo "ANTHROPIC_API_KEY=$TOKEN"
echo "OPENAI_API_KEY=$TOKEN"
} >> "$GITHUB_ENV"Use your platform URL — the same one this whole page uses, covered below in Which URL do I point at?. A pipeline's login resolves it exactly the way a developer's does: type either URL and the CLI asks the deployment which origin owns its auth. If you typed the application URL instead, scrydon login prints "<the url you typed> is the application URL; its platform URL is <the one it found>. Logging in there instead." and continues — that is the CLI correcting a URL you did not have to get exactly right, not a failure.
--ttl is a request, not a guarantee. Your organization's credential policy — and, under that, a hard ceiling your operator sets for the whole deployment — can clamp it shorter, and the server clamps rather than refusing. Ask for 2h (7200s) against a deployment whose ceiling is lower and the command prints, for example, "Requested 7200s; this organization's policy caps it at 3600s. The token is valid for the shorter time." — the login still succeeded. Ask for nothing and you get the organization's default (an hour, until an administrator sets one). An organization admin sets that maximum, default, and the client-secret lifetime at Settings → Governance → Credentials → Policy — the deployment ceiling itself is shown there too, read-only, since only your operator can move it.
The token lands in ~/.scrydon/credentials.json, the same file a developer's login uses. There is no separate router-key mint step for a service account the way there is for a person's router setup — the bearer the login returns is itself a valid router credential, which is why the snippet above reads it straight back out rather than minting anything new.
Log in again at the start of every job rather than caching the exported key across jobs — see the expiry failure mode below.
Choose a model
GET /api/router/v1/models, sent with the bearer above, lists what your organization enabled — the same allowlist a developer's key is bound by:
curl -H "authorization: Bearer $ANTHROPIC_API_KEY" https://auth.your-company.com/api/router/v1/modelsWhen something fails
| What you see | What happened | What to do |
|---|---|---|
A vendor-shaped 401 naming nothing, partway through a long-running job | The bearer expired mid-run. A machine token lives hours, not forever, and a long coding-agent run can outlive it. | Re-run scrydon login at the start of every job rather than caching a token across jobs — a fresh one is minted every time. |
CAPABILITY_NOT_CONFIGURED | The account is pinned to an environment with no matching connection — usually not the one you meant to reach. | Confirm which environment the account is pinned to in Settings → Service accounts, and that the environment has the extension enabled. |
CAPABILITY_NOT_CONFIGURED with "The provider endpoint's TLS certificate could not be verified." | The connection's endpoint address presents a certificate that does not verify, so no request reached the provider and a retry will not help. | Correct the connection's endpoint address. For Azure AI Foundry, see Certificate errors. |
Re-pinning an account's environment takes effect on its next request, including for a bearer already issued — the environment is read from the account on every request, not stamped into the token, so there is nothing to re-mint.
Connect an app that only takes an API key
Some apps cannot sign in or refresh a token. Their AI settings ask for a base URL and an API key, store both, and send the same key on every call: Twenty CRM, Open WebUI, n8n, LibreChat. Give such an app a router key issued to a service account. Its calls are then governed, billed and audited as that account — not as you, and not as whoever set the app up.
Prepare the service account
Create a service account for the app and name it after the app, for example "Twenty CRM", so its
spend is easy to find. Grant it capabilities:write and limit it to one environment with
the models the app should use. A router key acts in its account's environment and carries no
other scope. See Router keys for every requirement.
Issue the key
On the account, select Add router key, name it after the app, and choose a lifetime. Copy the key — it is shown once — and select Test key to confirm Scrydon accepts it.
Configure the app
Use the base URL for the API the app speaks:
| The app speaks | Base URL | The app sends the key as |
|---|---|---|
| OpenAI (Chat Completions) | https://auth.your-company.com/api/router/v1 | Authorization: Bearer <key> |
| Anthropic (Messages) | https://auth.your-company.com/api/router | x-api-key: <key> |
The AI Gateway accepts the key in either header, so you do not need to choose. The sheet that shows the key lists both base URLs for your deployment. As with a developer key, they differ by one path segment, and that is deliberate: each SDK appends its own path.
For Twenty CRM, add a provider to its AI_PROVIDERS configuration that uses the
OpenAI-compatible SDK (@ai-sdk/openai-compatible), with baseUrl set to the OpenAI base URL
above and apiKey set to the router key. Keep the key in the secret your deployment already
mounts for Twenty rather than in plain configuration.
Before configuring the app, you can confirm the key reaches a model:
curl -H "authorization: Bearer $ROUTER_KEY" https://auth.your-company.com/api/router/v1/modelsThe list shows the models enabled for the account's environment — the same allowlist the app's calls are held to.
Qualification examples
The repository includes pinned LibreChat configuration and a LangGraph example
that reopens its SQLite checkpoint and switches models through Chat Completions
(tests/qualification/governed-ai). The application's graph, conversation and
tools remain application-owned. Its review interrupt is an application decision;
privileged Scrydon operations still require their own execution grants. A shared
service-account key identifies the application, not an employee.
Local HTTP/SDK regression checks cover streamed responses, tool-call continuity, checkpoint resume and errors. Live client, provider, privacy and collector runs must be qualified against your deployment; these examples do not certify them.
Keep it working
- Rotate before it expires. The account's creator is warned six days before the key expires. Rotate key keeps the old key working for a day or a week, so update the app within that window.
- A change to the account reaches the key at once. Disabling the account, removing
capabilities:write, or setting it to act in every environment stops the key on its next call. - Revoke it from the account or from the credential ledger when the app no longer needs it.
For administrators
Enable models
Settings → Platform → Extensions → the provider → Includes. What you select there is what developers can reach.
The allowlist is enforced at dispatch, not only where models are offered. A tool that names a model directly gets a 409 with your policy message, not a silent substitution.
Three states, and the difference matters:
| Allowlist state | Effect |
|---|---|
| Not configured | Every model the provider exposes is allowed. |
| Some models selected | Exactly those, and nothing else. |
| Nothing selected | Nothing is allowed. This is a policy, not an empty form. |
See what it costs
Settings → Governance → Cost Insights → Usage, with the source set to AI Gateway.
Router traffic is metered with its own source, separate from application API traffic, so the per-principal table answers "who is using this and what is it costing me" — a person or a service account — without an application's workload muddying the numbers. Each row carries calls, tokens, indicative spend, and modelled CO₂.
The breakdown switches between Principal and Model, the tool table lists what was reached through the MCP endpoint, and the budget line at the top of the page shows the organisation cap. The same data is available through billing.summary with filters: { source: "router" }.
Set spend caps
Cost Insights → Limits. Every cap sits beside the scope's spend so far this period, with the highest team or developer named. A cap can be set on a developer, a team, or the whole organisation, measured over a rolling day or month, against either indicative spend or call count.
- Caps are evaluated from the usage ledger, so they hold across every replica.
- Enforce refuses further calls; Audit records the breach and lets the call through.
- A blank field means uncapped.
0means capped at zero — that distinction is real. - When a cap is reached the developer gets a
429whoseRetry-Afteris the actual time until the period resets, so a well-behaved client backs off properly instead of hammering.
Caps govern indicative spend — Scrydon's estimate. Treat them as a guardrail against runaway usage, not as a hard contractual limit against a provider's bill.
Choose a DLP posture
Cost Insights → Limits, under Router guardrails.
DLP runs over every prompt, and for a chat message that is straightforwardly right. A coding agent's prompt is a different shape: it carries the working set of a repository, re-sent every turn, and source code is adversarial input for rules tuned on prose — a credential pattern matches test fixtures, .env.example, and base64 in a lockfile.
So the router's posture is a decision you make rather than one you inherit:
| Posture | Effect |
|---|---|
| Inherit (default) | Whatever your organisation policy says. Unchanged from before the router existed. |
| Enforce | A finding blocks the developer's turn. |
| Audit | A finding is scanned, recorded, and reported — but the turn proceeds. |
Audit is not "DLP off." Every rule still runs under the same limits as in enforce mode: built-in detectors always run, and each custom rule runs within its field limit and the per-field time budget, with compiling the policy's rules limited to 1 second per request (see DLP limits). A custom rule those limits stop is not silently dropped: it fails closed on that field — which, under audit, means the hit is reported — and the scan row names it. Every finding is still counted in the audit log, and every kind of dlp.violation an auditor reads is still produced — one row per rule, finding type and channel per request, with the number of occurrences. The only thing it changes is whether a finding refuses the turn. Changing it affects the router only; chat, workflows, and every application surface keep the organisation policy.
Enforce streams too. A streamed answer under an enforce posture is governed
in windows: DLP scans roughly every 512 characters, holds the last 256 back
until the next window confirms them, and releases the rest — so text flows
while the model is still writing instead of arriving all at once when it
finishes. A finding later in the answer still stops it: the stream ends with a
policy error after the clean prefix was delivered, and the text that carried
the finding is never sent. Moderation in enforce mode still judges the whole
answer before releasing it, because its judge is a model call.
Define routes — virtual models that pick a real one
A route is a model id you define. A developer names it (ANTHROPIC_MODEL=team/agent,
or --model team/agent) and an algorithm picks the real, enabled, allowlisted
model for each turn. Nothing is substituted unless the developer asked for the
route by name, and the model a route picks still has to pass your allowlist —
a route cannot reach a model you did not enable.
Routes live in the organisation policy platform.llm_router.routes, written
through the organisation policy API (organization.policy.set). Three route
types:
{
"version": 1,
"routes": {
"team/fast": { "type": "passthrough", "target": "anthropic/claude-sonnet-5" },
"team/ab": {
"type": "random",
"targets": [
{ "model": "anthropic/claude-opus-5", "weight": 1 },
{ "model": "openai/gpt-5", "weight": 3 }
]
},
"team/agent": {
"type": "stage_router",
"capable": "anthropic/claude-opus-5",
"efficient": "anthropic/claude-sonnet-5",
"picker": "efficient_first",
"confidenceThreshold": 0.5,
"recentTurnWindow": 3
}
}
}| Type | What it does |
|---|---|
passthrough | An alias. |
random | A weighted split, for A/B tests and cost experiments. |
stage_router | Reads the recent tool-result history for signs the agent is in trouble (errors, the same call repeated, reading without producing) or settling (edits landing, tests passing), squashes them to a confidence, and picks the capable or efficient model once it clears confidenceThreshold. Below it, the picker default serves (efficient_first or capable_first). A fatal failure escalates regardless. No extra model call is made, so a decision costs microseconds. |
What is recorded: the usage event and audit row carry the model that actually
served the turn and the route that chose it (route.id, route.type, the
decision source and tier), and the Anthropic message_start frame names
the served model, so Cost Insights shows the real split and an auditor can ask
"which turns did team/agent send to the capable tier". Routes are not listed on /v1/models — name
them directly. A policy that fails validation deactivates every route and is
logged; the developer then gets the ordinary "model not enabled" answer.
Revoke a developer's access
Settings → Governance → Credentials lists every member's keys, router keys included, by the name the developer gave them. Revoking one takes effect on the next call. A developer revokes their own under Settings → Account → AI Gateway → Your keys.
Removing the local copy with scrydon router forget does not revoke the key. If a key may have leaked, revoke it here.
What the router does with a request
- Authenticate. The router key carries its own organisation and environment — the request never asserts a tenant, and the router strips any header that tries to.
- Admit. Scope, deadline, data classification, clearance, and your spend caps.
- Resolve a target. Your enabled provider, your allowlist, the connection bound to that environment, and its credential. A developer's tool never sees a vendor credential.
- Apply guardrails. DLP, then moderation, under the posture you chose.
- Dispatch. Through the same runtime every other capability call uses.
- Record. One audit event and one usage event, attributed to the person or service account.
Steps 2 through 6 are not router code. They are the pipeline your platform already runs; the router's own job is steps 1 and the translation on either side of it.
Capacity and back-pressure
Each API Platform pod accepts a bounded number of open calls at once — one
budget shared by router turns, direct platform API calls and MCP tool calls
(the chart value auth.maxInFlight, default 64; env PLATFORM_MAX_IN_FLIGHT;
the older auth.llmRouter.maxInFlight / LLM_ROUTER_MAX_IN_FLIGHT still work as
a fallback). When the pod is more than half full and another organization is using it,
no organization may take more than a quarter of it
(platform_org_in_flight_share), so one busy tenant cannot refuse everyone
else. An organization alone on the pod can use all of it but four slots (a
quarter, on a small pod), kept free for a newcomer. Calls still being
authorized draw on a separate allowance of the same size, so an
organization's own retries cannot hold those four slots. A newcomer gets in
unless one tenant keeps that many calls in authorization at once. If the pod
fills while a call is being authorized, that call is refused after
authorization with the same 429. A streamed turn counts until its stream ends or you disconnect; a
non-streamed turn counts until its response is returned. Model listing
(/models) is exempt: it is a short catalog read that never holds a slot. Past that bound the router answers
429 with a Retry-After header — rate_limit_error in the Anthropic
dialect, rate_limit_error / rate_limit_exceeded in the OpenAI dialect — and
does not touch the database or start any work for the refused request. The
Anthropic and OpenAI SDKs both honour Retry-After automatically.
This is a per-pod overload guard, not a per-developer quota: it exists so a
saturated pod sheds load instead of slowing every stream and then running out
of memory. Sustained 429s at steady state mean the deployment needs another
replica or a higher CPU limit, not client-side retries. Per-developer and
per-organisation limits are the spend caps above.
Operators can watch scrydon_llm_router_streams_in_flight,
scrydon_llm_router_admission_rejections_total and the TTFT / duration
histograms on the AI Gateway SigNoz dashboard shipped with the operations
chart. Two histograms separate the platform from the provider:
scrydon_llm_router_overhead_ms is a turn's wall time minus the time spent
inside the provider call (queue wait, guardrails and audit settlement
included), and scrydon_llm_router_provider_ttfb_ms is the provider's own
time to first event. scrydon_llm_router_turns_total carries a
response_class (success, retryable_error, other_error) and every
provider attempt, retries included, is counted in
scrydon_extension_outbound_attempts_total with the same classes — the
difference between the two error rates is what the router's retries are worth.
Protocol coverage
Every dialect is a translation in front of the same pipeline; GET /api/router/v1/health lists the ones your deployment speaks.
POST /api/router/v1/messages · GET /api/router/v1/models
| Feature | Supported |
|---|---|
Content blocks (text, image, document) | Yes |
tool_use / tool_result, block order preserved | Yes |
| Extended thinking, including signature round-trip | Yes |
cache_control breakpoints accepted on the request | Yes |
| Cache-read and cache-write reported back separately | Yes |
cache_control forwarded to the provider | Yes |
anthropic-beta forwarded to the provider | Yes, for recognised betas — see below |
Streaming (message_start → content_block_* → message_delta → message_stop) | Yes |
system as a string or as a block array | Yes |
stop_sequences, temperature, top_p, top_k | Yes |
Prompt caching matters more here than anywhere else: a coding agent re-sends its whole context every turn, so without breakpoint passthrough every turn re-pays full input tokens.
Breakpoints travel with the prompt. cache_control markers on system
blocks and content parts are forwarded exactly as sent, a system block array
stays an array, and cache_read_input_tokens / cache_creation_input_tokens
come back on every turn. The router also never rewrites a prompt: on the
router surface a DLP rule configured to mask, redact or hash is recorded as an
audit finding and not applied, because one changed byte early in a 150k-token
working set re-pays the whole prompt on every turn.
Beta features. Claude Code sends an anthropic-beta header naming the
preview features it relies on. The router forwards the names it recognises
(the ones Claude Code sends today plus request-only features such as
context-1m-2025-08-07 and interleaved-thinking-2025-05-14) and drops the
rest rather than rejecting the turn — Anthropic answers an unknown beta with a
400. An administrator can add betas to every request from a connection in
Settings → Platform → Extensions → Anthropic → Beta features.
POST /api/router/v1/chat/completions · GET /api/router/v1/models
| Feature | Supported |
|---|---|
tools / tool_choice, tool_calls, tool_call_id | Yes |
| Text and image content parts | Yes |
Streaming, including the [DONE] sentinel | Yes |
stream_options.include_usage | Yes |
response_format, reasoning_effort | Yes |
max_completion_tokens (and the deprecated max_tokens) | Yes |
| Reasoning content | Yes — optional reasoning_content on messages and streaming deltas |
POST /api/router/v1/responses — what Codex and the newer OpenAI SDKs speak.
| Feature | Supported |
|---|---|
input as text or as items (message, function_call, function_call_output) | Yes |
instructions, max_output_tokens, temperature, top_p | Yes |
Flat function tools, tool_choice, parallel_tool_calls | Yes |
text.format (json_object, json_schema), text.verbosity, reasoning.effort | Yes |
Streaming: response.created → output_item / content_part / output_text events → response.completed, with sequence_number | Yes |
Reasoning summaries (reasoning items and reasoning_summary_text events) | Yes — this wire has a channel for it |
previous_response_id, background, item_reference, hosted tools | No — refused, see above |
POST /api/router/v1/embeddings
| Feature | Supported |
|---|---|
input as a string or an array of strings, dimensions | Yes |
encoding_format: "float" and "base64" (little-endian float32, the Python SDK's default) | Yes |
| Pre-tokenized integer input | No |
POST /api/router/v1beta/models/{model}:generateContent · …:streamGenerateContent?alt=sse · GET /api/router/v1beta/models
| Feature | Supported |
|---|---|
contents with text, inlineData (images and documents), fileData | Yes |
systemInstruction as text or parts | Yes |
functionDeclarations, functionCall / functionResponse round trip (ids minted when the client sends none), toolConfig | Yes |
generationConfig: temperature, topP, topK, maxOutputTokens, stopSequences, JSON output (responseMimeType / responseSchema), thinkingConfig | Yes |
Thoughts as thought: true parts | Yes |
Built-in tools (googleSearch, codeExecution, urlContext), cachedContent, candidateCount > 1 | No |
The credential is x-goog-api-key (what the SDK sends) or ?key=.
POST /api/router/openai/deployments/{deployment}/chat/completions · …/embeddings · …/responses · /api/router/openai/v1/*
The same OpenAI dialects under Azure's path shape. The deployment name in the path is the model id when the body omits model; api-version is accepted and ignored; the credential is the api-key header.
POST /api/router/api/chat · GET /api/router/api/tags · POST /api/router/api/show · GET /api/router/api/version
| Feature | Supported |
|---|---|
Messages with content, images, tool_calls (object arguments), tool replies | Yes |
tools, format ("json" or a schema), think | Yes |
options: temperature, top_p, top_k, num_predict, stop | Yes |
NDJSON streaming by default; stream: false for one object | Yes |
/api/generate, /api/embed, model management, keyless access | No |
Which URL do I point at?
Your platform URL — the one serving /api/router, usually auth.<your-company>.
Not the application URL you sign in to every day.
The distinction matters more than it should, because the application origin
proxies /api/auth. A login against it therefore succeeds, and nothing looks
wrong until your editor reports a bare 404 from an endpoint that knows nothing
about Scrydon.
The CLI handles this for you: type either URL and scrydon login asks the
deployment which origin owns its auth, tells you if it redirected, and stores the
right one. Every scrydon router subcommand accepts either URL too — when the one
you typed has no stored credentials it follows the same redirect, says which URL
it used, and continues. scrydon router setup then probes /api/router before
minting a key, so a mismatch fails immediately with the cause named rather than
silently later.
If you are configuring a client by hand, use the platform URL.
Related
- Billing and usage metering — the
billing.*API behind Cost Insights - DLP overview — what the guardrails scan for
- Governance usage — the wider Cost Insights surface
Customer-managed models
Connect a hosted or on-premises inference server through OpenAI Compatible, then discover its models or enter the deployment IDs in the connection settings. Use the model ID published by the gateway; no new first-party extension or catalog entry is required for each model. Model names can include organization/path prefixes.
The connection's Chat compatibility profile selects the upstream API behavior.
Leave it blank for standard Chat Completions. Use reasoning-content for compatible
servers that require reasoning content in tool history, or glm, mimo, qwen,
or deepseek when the endpoint implements that dialect. Select based on the server's
API, not the model's brand: Qwen hosted by vLLM can expose a different contract from
Alibaba's hosted endpoint.
Gateway Chat Completions, Responses, and Anthropic Messages requests are normalized
before reaching the selected upstream connection, which then chooses the upstream
OpenAI API for that model: Chat Completions or /responses, set by the connection's
OpenAI API field (blank means auto). A Responses client does not require the
upstream server to implement /responses, and a Chat Completions client can reach a
model that works only through it. Existing gateway
limits still apply, including rejection of unsupported hosted tools and server-side
conversation state.
The executor must have a network route to the private endpoint. Connection settings do not establish a VPN or tunnel. Existing organization egress controls still apply. See OpenAI-compatible endpoints for connection setup and profile details.