Company AI keys
A workspace can store one key for each AI provider below, and let apps use them, each up to a monthly budget. An app calls the provider with no key at all. Our outbound proxy adds the workspace’s key to each request, counts what the request cost, and refuses the app’s requests once its budget for the month is spent. The key never reaches the app’s code, its logs or its versions, and nobody can read it back. It’s on every plan.
| Provider | Name in the CLI | Where the key goes |
|---|---|---|
| Anthropic | anthropic |
api.anthropic.com |
| OpenAI | openai |
api.openai.com |
| Google Gemini | gemini |
generativelanguage.googleapis.com |
| Mistral | mistral |
api.mistral.ai |
| Groq | groq |
api.groq.com |
| Cohere | cohere |
api.cohere.com |
| Azure OpenAI | azure |
your resource, such as acme.openai.azure.com |
| Amazon Bedrock | bedrock |
one region’s runtime, such as bedrock-runtime.us-east-1.amazonaws.com |
Store a key
Section titled “Store a key”Workspace owners store keys. Paste it at the hidden prompt, or pipe it in:
jiayang ai key set acme anthropicStored acme's Anthropic key, ending WXYZ. It won't be shown again.An Azure OpenAI key is for one resource. Give its name or its endpoint with --endpoint:
jiayang ai key set acme azure --endpoint https://acme.openai.azure.comStored acme's Azure OpenAI key for acme.openai.azure.com, ending AZUR. It won't be shown again.An Amazon Bedrock key is a Bedrock API key, used in one region. Give the region with --region:
jiayang ai key set acme bedrock --region us-east-1The key goes to that resource or region and nowhere else, even if a request names another. To move it, store it again with the new one.
In the dashboard it’s under AI keys in the sidebar. Storing a key again replaces it, and the next request goes out with the new one. jiayang ai key remove acme anthropic removes it, and with it every app’s grant: their next request goes without it. It asks first, and where there’s no terminal to ask at, such as in a script, it needs --yes.
Let an app use it
Section titled “Let an app use it”Give the app a monthly budget in dollars:
jiayang ai grant acme/payroll anthropic --budget 25acme/payroll can use the Anthropic key, up to $25.00 a month. It has spent $0.00 this month.Running it again changes the budget. jiayang ai revoke acme/payroll anthropic takes the grant away, and the app’s next request goes without the key. jiayang ai keys acme lists the keys, the apps that use each, and what each has spent this month. jiayang ai show acme/payroll shows one app’s budgets, and the app’s Secrets tab shows them too. Each provider’s key has its own budget.
Owners store keys and set budgets. Admins can see them. The app’s editors can see the app’s own budgets and spend. Every change is in the audit log as ai_key.put, ai_key.remove, ai_grant.put or ai_grant.remove, with the provider, an Azure or Bedrock key’s host, and never the key.
In your app
Section titled “In your app”Call the provider as you normally would, with no key. With the official SDKs, pass any placeholder, since they want one:
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({ apiKey: "set-by-jiayang" });const message = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }],});The same goes for Gemini, with Google’s SDK:
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: "set-by-jiayang" });const response = await ai.models.generateContent({ model: "gemini-2.5-flash", contents: "Hello", config: { maxOutputTokens: 1024 },});For Bedrock, set AWS_BEARER_TOKEN_BEDROCK to any placeholder, so the AWS SDK sends a bearer token rather than signing the request. For Azure OpenAI, point the SDK at your resource as usual.
Whatever key, token or signature your app sends to the provider is taken off and replaced with the workspace’s: the Authorization, x-api-key, api-key, x-goog-api-key and Ocp-Apim-Subscription-Key headers, AWS’s signing headers, and a key, api-key, subscription-key or access_token in the URL. It is never sent beside the workspace’s key.
The key goes on these requests only, since they are the ones whose cost we can count from the answer:
| Provider | Requests |
|---|---|
| Anthropic | POST /v1/messages, POST /v1/messages/count_tokens, GET /v1/models |
| OpenAI | POST /v1/chat/completions, POST /v1/responses, POST /v1/embeddings, GET /v1/models |
| Google Gemini | generateContent, streamGenerateContent, embedContent, batchEmbedContents and countTokens on /v1beta/models/{model} or /v1/models/{model}, and listing models |
| Mistral | POST /v1/chat/completions, POST /v1/fim/completions, POST /v1/embeddings, GET /v1/models |
| Groq | POST /openai/v1/chat/completions, POST /openai/v1/responses, GET /openai/v1/models |
| Cohere | POST /v2/chat, POST /v2/embed, POST /v1/tokenize, POST /v1/detokenize, GET /v1/models |
| Azure OpenAI | chat completions and embeddings on /openai/deployments/{deployment}, and /openai/v1/chat/completions, /openai/v1/responses, /openai/v1/embeddings and models |
| Amazon Bedrock | POST /model/{model}/converse, POST /model/{model}/converse-stream, POST /model/{model}/count-tokens |
Anything else to the provider’s host from an app with a grant is refused with a 403, and nothing is sent: batches, files, fine-tuning, Gemini’s Interactions API, Mistral’s agents and conversations, Cohere’s rerank, Bedrock’s InvokeModel, and each provider’s OpenAI-compatible API where it has one. A Bedrock model named by an ARN, such as a provisioned throughput or a prompt router, is refused too: name the model or its inference profile, such as us.anthropic.claude-sonnet-4-6.
Every refusal comes in the provider’s own error shape, with an error type its SDKs know: permission_error from Anthropic, invalid_request_error from OpenAI, Azure and Groq, INVALID_ARGUMENT from Gemini, ValidationException from Bedrock, for example. So are requests whose cost we can’t bound before they go, with a 400:
- server tools: Anthropic’s web search, web fetch, code execution and MCP servers, OpenAI’s and Groq’s built-in tools, Groq’s compound models, Gemini’s Google Search, code execution, URL context and the like, Cohere’s connectors, Bedrock’s system tools, and Azure’s On Your Data. Tools your app runs itself are fine;
- Anthropic’s context management, and Bedrock Guardrails, which are billed apart from tokens;
- content egress doesn’t know how to price: anything other than text, inline images and files, and your own tools’ calls and results;
- audio and video, which several providers bill at rates of their own. Embeddings with the company key are text only;
- images, documents and files by URL, by file id or in S3, a Gemini cached context, and on OpenAI an earlier response, a stored item, conversation or prompt, or reasoning without its encrypted content. Send images and files inline, as base64 or a
data:URL, and the conversation in full; - in Bedrock’s
additionalModelRequestFields, anything but thinking and sampling settings; - OpenAI background responses, and search, audio and realtime models.
Requests go over HTTPS only, to the provider’s usual port.
If the app’s list of hosts is on, add the provider’s host to it, from the table at the top, or your Azure resource’s or Bedrock region’s. Until you do, its requests are refused, and jiayang ai grant, jiayang ai show and the dashboard say so.
How the budget is kept
Section titled “How the budget is kept”Before a request goes out, we set aside the most it could cost: every byte of the request, and 5,000 tokens more for what the provider adds, as input at the dearest input price, plus its output limit (128,000 tokens if it sets none). If the month’s budget can’t cover that, the request is refused and nothing is sent. When the answer comes back, we read its usage and count what it actually cost, and the rest of what was set aside is freed. Because everything a request could cost is in it, many requests at once can’t spend more than the budget between them.
What we send the provider is the request as we read and priced it, written out again, so a field given twice reaches the provider the way it was priced.
Set an output limit on every request: max_tokens (Anthropic, Mistral, Cohere), max_completion_tokens or max_output_tokens (OpenAI, Azure, Groq), generationConfig.maxOutputTokens (Gemini) or inferenceConfig.maxTokens (Bedrock). A request without one sets aside the cost of 128,000 output tokens, which can be refused by a small budget even when its real cost would fit.
A request with an image, a document, a file, or reasoning the provider encrypted (including Gemini’s thought signatures) sets aside the cost of 1,050,000 input tokens, since the provider bills those by what they show, not by their size: a small picture or a compressed PDF can come to many more tokens than it has bytes. Give an app that sends them a budget to match.
Streamed answers are counted too: from Anthropic’s message_delta event, OpenAI’s, Azure’s and Groq’s last chunk, which we ask for by turning on stream_options.include_usage for you, Mistral’s last chunk, Gemini’s chunk that finishes, Cohere’s message-end event, and Bedrock’s metadata event.
What we can’t know is counted at the most it could be:
- An answer with no usage, or a stream that stops early, costs what was set aside for it. So does a 5xx from the provider, or a request that never got an answer, since the work may have been done. A 4xx costs nothing. For a request with an image, that can be a large share of a small budget.
- An answer that says a dearer model ran than the one asked for is priced as that model.
- A model we don’t have a price for is priced at the dearest rate on the provider’s price list, which spends a budget many times faster. Use a model by the name on the provider’s pricing page. Some providers list models with no public price, such as Groq’s “Contact sales” models and Cohere’s Command A family, and those are priced the same way.
- An Azure deployment can be called anything. Name it after its model, such as
gpt-4o-mini, and the request is set aside at that model’s price; otherwise at the dearest, and settled at the model the answer says ran. A deployment named after a cheaper model than the one it runs is set aside at the cheaper price, so one request can take its budget past the limit. And search, audio and realtime models are refused by the deployment’s name: one calledprodthat runs a search model gets through, and its search fee isn’t counted. - Azure’s Data Zone and Regional deployments cost 10% more than Global ones, and Bedrock’s in-Region inference up to 10% more than global cross-Region inference. A request doesn’t say which it was, so it is priced as the dearer.
- Priority tiers are priced at their own rates: Gemini’s at 1.8 times, Mistral’s at 1.75 times, and Bedrock’s priority and latency-optimized inference at twice. A request that may run at a priority tier is set aside at that price.
- A set-aside amount nobody settled within an hour is counted as spent.
Prices come from the providers’ pricing pages and are kept up to date in one place, including price changes a provider has announced for a later date, such as Gemini’s on 1 January 2027. Budgets reset on the first of each month, UTC.
Once an app has spent its budget, its requests get a 429 with the provider’s error shape, so its SDK shows the message:
{"type": "error", "error": {"type": "rate_limit_error", "message": "This app has used its $25.00 monthly budget for the company key ($25.00 spent or held), so nothing was sent. It starts again on 2026-11-01. A workspace owner can raise it."}}Gemini’s is a RESOURCE_EXHAUSTED error and Bedrock’s a ThrottlingException. A single request that could cost more than what’s left of the budget is refused the same way, while smaller ones still go through. The message says so, and which setting makes the request smaller:
{"type": "error", "error": {"type": "rate_limit_error", "message": "This request could cost up to $9.68, more than the $4.96 left of this app's $5.00 budget for the company key this month, so nothing was sent. Ask for fewer tokens with max_tokens, or ask a workspace owner to raise the budget."}}Both answers carry x-should-retry: false, so the SDKs that read it don’t keep trying, and x-jiayang-ai-budget, which says spent for the first and short for the second.
If the provider turns the company key down, the app gets a 401 in the provider’s shape that says so, never the provider’s own message, which can name part of the key or whose it is. That covers every provider’s 401, Gemini’s 400 for a key that isn’t valid and its 403 for a suspended or blocked key, and Bedrock’s 403 AccessDeniedException.
Any other refusal is passed on as the provider sent it, unless it holds the key or a long piece of it, or is over 64 KB. Then the app gets the same status with a message of ours: Google Gemini refused the request, and its answer named the company key, so it isn't passed on.
Requests the key wasn’t added to, refused for any of the reasons here, show on the app’s Activity tab under Sends requests to as refused.
What we see
Section titled “What we see”The proxy reads only the usage counts in the provider’s answer, and the model and tier it ran in. It doesn’t store the prompts, the answers or anything else in them. We keep each app’s spend and number of requests per month. The headers that name the key’s organization and project at the provider are taken off the answer.
Every app granted a key uses the same account at the provider. Requests that point at something stored there by id are refused, as above, so one app can’t read what another app’s requests left at the provider through the key.