Model Access
Choose which AI models the agent may use, route them to Anthropic, OpenAI, Azure or AWS, keep provider keys off the sandbox, and cap token spend
Model access decides which AI models the agent may use and how much it may spend. By default the agent can use no model at all. Each request the agent makes is checked on the computer (is this model approved? is there budget left? must the user approve?) and only then sent to the provider. The key that pays for the request is added outside the sandbox, so the agent never holds it.
Model access is configured in the proxies.llm section of policy.
What does the agent see?
The agent sees an ordinary model service with placeholder keys. Inside the sandbox, the standard settings that AI tools read point at Levee:
| Variable | Points to |
|---|---|
ANTHROPIC_BASE_URL | Levee's Anthropic-compatible endpoint |
ANTHROPIC_API_KEY | A placeholder key for this session |
OPENAI_BASE_URL | Levee's OpenAI-compatible endpoint (including /v1) |
OPENAI_API_KEY | A placeholder key for this session |
The placeholder keys are useless outside the sandbox and change every session. The real provider key never enters the sandbox, so there is nothing for the agent to leak.
Levee offers the agent two protocols:
| Protocol | Endpoint | Used by |
|---|---|---|
| Anthropic Messages | POST /v1/messages | Claude Code |
| OpenAI Chat Completions | POST /v1/chat/completions | Codex CLI and other OpenAI-protocol clients |
GET /v1/models returns the models the policy allows, so tools that list models only offer approved ones.
There is no translation between protocols. A request in the Anthropic protocol can only go to a provider that speaks it, and the same holds for OpenAI:
| Agent protocol | Providers it can be routed to |
|---|---|
| Anthropic | anthropic, bedrock (Claude on AWS), a compatible-endpoint with protocol: anthropic |
| OpenAI | openai, azure-openai, azure-foundry, a compatible-endpoint with protocol: openai |
GitHub Copilot CLI talks to GitHub's own service; it is governed by Web Access rules for GitHub's sites, not by this section.
Section shape
{
"proxies": {
"llm": {
"default": "deny",
"budgets": {
"perProjectDailyTokens": 2000000,
"perProjectSessionTokens": 500000,
"perProjectMaxConcurrentRequests": 4
},
"providers": {
"anthropic": {}
},
"models": [
{
"name": "claude-sonnet-4-5",
"alias": "claude-sonnet-4-5-20250929",
"provider": "anthropic",
"upstreamModel": "claude-sonnet-4-5-20250929"
}
],
"promptFilters": ["project-codename-x"]
}
}
}
| Key | Required | Meaning |
|---|---|---|
default | yes | Must be deny. A model that is not listed in models is always refused. Any other value makes the section invalid. |
budgets | yes | Token and concurrency caps per project. See Budgets. |
providers | yes | The providers you use, keyed by provider name. See Providers. |
models | yes | The approved models. See Models. |
promptFilters | no | Terms that block a request. See Prompt filters. |
If the section is missing or invalid, the agent cannot call any model. When you publish an invalid section to a running project, it keeps its previous model rules and the rejection is recorded.
Models
Only the models you list can be used. Each entry in models approves one model:
| Key | Required | Meaning |
|---|---|---|
name | one of name/alias | The model name the agent requests. Exact, case-sensitive match. |
alias | one of name/alias | A second name that resolves to the same entry, for example a dated model ID that a tool sends. A name match takes priority over an alias match. |
provider | yes | A key of providers. |
upstreamModel | yes | The model ID sent to the provider. Lets you pin a specific version, or map a friendly name to a provider-specific ID. |
deployment | Azure OpenAI only | The Azure OpenAI deployment name (letters, digits, - and _). |
promptOnFirstUse | no | When true, the first request from a project for this model asks the user to approve it. Useful for expensive models. |
Tools send the model name they are configured with. If a tool requests a dated or suffixed model ID (for example claude-sonnet-4-5-20250929), add it as name or alias. The audit trail shows the exact name requested when a model is refused.
Approval on first use
With "promptOnFirstUse": true, the first request to that model in a project pauses while the Levee app asks the user. The request is held open until the user answers. If the user approves, the model can be used for the rest of the project run. If the user denies, or does not answer within 5 minutes, the request fails with User denied access to this model. and later requests for that model in the same run fail with User previously denied this model. Stopping and starting the project resets the decision.
Providers
A provider is the company or cloud service that runs the model. List the ones you use under providers; each has its own settings below.
Anthropic
"providers": { "anthropic": {} }
No settings. Requests go to Anthropic's public API. The key is read from LEVEE_KEY_ANTHROPIC on the computer.
OpenAI
"providers": { "openai": {} }
No settings. The key is read from LEVEE_KEY_OPENAI.
Azure OpenAI
"providers": {
"azure-openai": {
"endpoint": "https://contoso-ai.openai.azure.com",
"apiVersion": "2024-10-21"
}
},
"models": [
{ "name": "gpt-4o", "provider": "azure-openai", "upstreamModel": "gpt-4o", "deployment": "gpt4o-prod" }
]
endpointmust behttps, on the default port, with no user name or password in it, on an Azure OpenAI host (*.openai.azure.com,*.openai.azure.usfor Azure Government, or*.openai.azure.cnfor Azure operated by 21Vianet).apiVersionis required and has the formYYYY-MM-DDorYYYY-MM-DD-preview.- Every model needs a
deployment. - The key is read from
LEVEE_KEY_AZURE_OPENAI.
Azure AI Foundry
"providers": {
"azure-foundry": { "endpoint": "https://contoso.services.ai.azure.com" }
}
endpointmust behttpson an Azure AI host (*.services.ai.azure.com,*.inference.ai.azure.comor*.models.ai.azure.com).- Uses the OpenAI protocol.
- The key is read from
LEVEE_KEY_AZURE_FOUNDRY.
AWS Bedrock (Claude models)
"providers": {
"bedrock": { "region": "eu-central-1" }
},
"models": [
{ "name": "claude-sonnet-4-5", "provider": "bedrock", "upstreamModel": "eu.anthropic.claude-sonnet-4-5-20250929-v1:0" }
]
regionis required, for exampleus-east-1oreu-central-1.- Uses the Anthropic protocol, so it serves Claude Code.
- Credentials are read from
LEVEE_KEY_BEDROCK_ACCESS(access key ID) andLEVEE_KEY_BEDROCK_SECRET(secret access key), plusLEVEE_KEY_BEDROCK_SESSION_TOKENwhen you use temporary credentials. upstreamModelis the Bedrock model ID or inference profile ID.
Vediram LLM Gateway
If your organization uses the Vediram LLM Gateway, the agent's model calls are billed and governed centrally, and no provider key is stored on the computer at all. Requests are authenticated with the Vediram identity of the user signed in to the Levee app. Configure one provider entry per protocol, using the gateway address Vediram gives you:
"providers": {
"vediram-anthropic": {
"type": "compatible-endpoint",
"baseUrl": "https://<your-gateway-address>",
"protocol": "anthropic",
"authMode": "vediram-user"
},
"vediram-openai": {
"type": "compatible-endpoint",
"baseUrl": "https://<your-gateway-address>",
"protocol": "openai",
"authMode": "vediram-user"
}
},
"models": [
{ "name": "claude-sonnet-4-5", "provider": "vediram-anthropic", "upstreamModel": "claude-sonnet-4-5" },
{ "name": "gpt-4o", "provider": "vediram-openai", "upstreamModel": "gpt-4o" }
]
| Key | Meaning |
|---|---|
type | compatible-endpoint |
baseUrl | The gateway address. Must be https. |
protocol | anthropic or openai |
authMode | vediram-user |
Requirements:
- The user must be signed in to Vediram Cloud in the Levee app, and the app must be running. If nobody is signed in, model requests are refused.
- Run the Levee app normally, not "as administrator". An elevated app is not accepted as a source of the user's identity.
- Which models each user may use on the gateway, and their gateway budgets, are managed in the Vediram Portal under AI agents > LLM Catalogs and LLM Assignments. The
modelslist in the device policy must also include them.
Provider keys
Provider keys are stored on each computer as machine-level environment variables, never in policy.
A policy section that contains an apiKey for a provider is rejected as a whole with the message inline 'apiKey' is not supported. This keeps keys out of files that are copied, synced and backed up.
Instead, set the key as a machine-level environment variable on the computer. The Levee service passes it to its model access component outside the sandbox, never to the sandbox.
| Provider | Variable(s) |
|---|---|
anthropic | LEVEE_KEY_ANTHROPIC |
openai | LEVEE_KEY_OPENAI |
azure-openai | LEVEE_KEY_AZURE_OPENAI |
azure-foundry | LEVEE_KEY_AZURE_FOUNDRY |
bedrock | LEVEE_KEY_BEDROCK_ACCESS, LEVEE_KEY_BEDROCK_SECRET, optional LEVEE_KEY_BEDROCK_SESSION_TOKEN |
From an elevated PowerShell 7 prompt:
$key = Read-Host -Prompt 'Anthropic API key' -MaskInput
[Environment]::SetEnvironmentVariable('LEVEE_KEY_ANTHROPIC', $key, 'Machine')
Remove-Variable key
Restart-Computer
Restart the computer after setting or changing a key. Windows services read machine-level environment variables when the system starts, so restarting only the Levee service does not pick up a new value.
To deploy keys at scale, use your endpoint management tool (for example an Intune remediation script or a Configuration Manager script running as SYSTEM) to set the same machine-level variables.
Security considerations:
- Machine-level environment variables are readable by any account that can sign in to the computer. Use keys dedicated to Levee, restrict them at the provider (spending limits, allowed models, allowed IP ranges where supported), and rotate them on your normal schedule.
- For shared or high-risk computers, prefer the Vediram LLM Gateway, which needs no key on the device.
- If a key is missing, requests to that provider fail with
No credential configured for provider '<name>'.
Budgets
Budgets cap how many tokens (the units model providers bill by) each project may use per day and per run. When a budget is reached, further requests are refused until it resets. Budgets are enforced on the computer before a request is sent.
| Key | Meaning |
|---|---|
perProjectDailyTokens | Maximum tokens per project per day. Resets at midnight UTC. |
perProjectSessionTokens | Maximum tokens per project run (from project start to stop). |
perProjectMaxConcurrentRequests | Maximum in-flight model requests per project. 0 or absent means no limit. |
Both token budgets are required and must be positive. A budget of 0 refuses every request.
How budgets are counted:
- When a request arrives, Levee reserves an estimate based on the request size. If the reservation would exceed a budget, the request is refused with
session token budget exceeded.ordaily token budget exceeded.(HTTP 429, so SDKs treat it as a rate limit). - When the provider reports actual usage, the reservation is replaced by the real input and output token count.
- Budgets are tracked on the computer for the lifetime of the project run. Stopping and starting a project starts a new session budget; the daily count also starts again after the model access component restarts.
Treat per-project budgets as a guard against runaway loops. For organization-wide spend control, combine them with limits at the provider or use the Vediram LLM Gateway's budgets.
Prompt filters
Prompt filters block requests that contain a term you list, such as a client name or a confidential project name. They catch accidents; they are not a security boundary.
promptFilters is a list of terms. If any term appears in a request (case-insensitive, after Unicode normalization and removal of invisible characters), the request is refused with Prompt contains a disallowed term. The term itself is never written to the audit trail or logs; the audit records only an anonymous marker.
Prompt filters are defense in depth, not a security boundary. A determined agent can rephrase, encode or split text so that a literal term no longer matches. Use them to catch accidents (for example a project code name pasted into a prompt), not to protect secrets. Keep secrets out of the sandbox instead.
Limits
- Request bodies are limited to 4 MB (
Request body exceeds 4 MB limit.). - Levee does not retry failed provider calls; the agent's SDK already does.
- Streaming responses are passed through as they arrive.
- Prompts and responses are not written to the audit trail. The audit records which model was used, token counts, latency and outcome.
Errors the agent can receive
When Levee refuses a model request, the agent receives a normal error from its model service. Errors use the native error format of the protocol the agent called, so SDKs handle them normally.
| HTTP | Error code | Message | Meaning |
|---|---|---|---|
| 401 | authentication_error | Unauthorized. | The request did not come from the sandbox's session (or policy has not been delivered yet). |
| 403 | model_not_permitted | Model '<name>' is not permitted. | The model is not in models. |
| 403 | user_denied | User denied access to this model. / User previously denied this model. | The user denied a first-use approval, or it timed out. |
| 403 | prompt_blocked | Prompt contains a disallowed term. | A prompt filter matched. |
| 400 | protocol_mismatch | Cross-protocol calls are not supported in v1. | The model's provider speaks the other protocol. |
| 400 | invalid_request | Request body must be JSON object with a 'model' field. / Request body is not valid JSON. | Malformed request. |
| 413 | request_too_large | Request body exceeds 4 MB limit. | Request too large. |
| 429 | budget_exceeded | session token budget exceeded. / daily token budget exceeded. / concurrent request limit exceeded. | A budget was reached. |
| 401 | missing_auth | No credential configured for provider '<name>'. | The provider key is not set on the computer, or no user is signed in for the Vediram LLM Gateway. |
| 502 | upstream_error | Upstream provider failure. | The provider returned an error or could not be reached. |
| 500 | internal_error | Internal server error. | Unexpected failure; check the logs. |
See Model errors for fixes.
Audit
| Event | Recorded when |
|---|---|
llm.request | A request completed. Includes provider, model, requested name, input and output tokens, latency, status and whether it streamed. |
llm.denied.model | A model not in policy was requested. |
llm.denied.protocol | A cross-protocol request was refused. |
llm.denied.budget | A budget was reached (which budget, used and cap). |
llm.denied.user | The user denied, or did not answer, a first-use approval. |
llm.denied.auth | No credential was available for the provider. |
llm.prompt_blocked | A prompt filter matched. |
llm.upstream_error | The provider failed. |