# Per-key spending limits: what LiteLLM, OpenRouter and Portkey enforce — and the four leaks a cap doesn't close

> How LiteLLM, OpenRouter and Portkey cap spend per API key — layer, timing, status code, reset — and the four leaks a cap doesn't close, read from the docs.

- Published: Aug 25, 2026
- Author: Leo Kaka, Engineering
- Tags: cost, gateway, api-keys
- Canonical: https://pirouter.ai/blog/per-key-spending-limits

---
Yes, all three do it. **LiteLLM** caps a virtual key (the key the proxy issues to your team,
in front of the real provider key) with `max_budget` and a reset window, **OpenRouter** caps a
key with `limit` plus `limit_reset`, and **Portkey** caps an API key by cost or by tokens. All
three also have a shared wallet, under different names: a LiteLLM team, an OpenRouter
organization credit pool or workspace budget, a Portkey workspace.

Two facts to settle before the tables. Shape: LiteLLM is a proxy you run yourself, and every
budget on its page "is enforced against spend read from the database", so no Postgres means
no budgets; OpenRouter is a hosted account paid from prepaid credits; Portkey is a hosted
gateway with a self-hosted enterprise option. Price of entry: LiteLLM's key, team and user
budgets carry no Enterprise mark, its per-model budget does; OpenRouter's per-key `limit` has
no plan gate on the pages checked, its workspace budgets and spend alerts are Enterprise, and
an organization "can only have 10 members"; Portkey's budgets are "Available on Enterprise
plan and select Pro customers". The tier table below has the links.

The differences that matter are in **which layer** holds the number, **when** the check runs
relative to the provider call, **what** the caller receives when the cap is hit, and **when**
the counter goes back to zero. Rate limits (RPM/TPM) are a separate axis and not covered here.
Helicone does something adjacent, a cost-based rate limit set per request via a
[`Helicone-RateLimit-Policy` header](https://docs.helicone.ai/features/advanced-usage/custom-rate-limits)
with `u=cents`; it is a throttle, not a key attribute, so it stays out of the tables. If you
only need the settings, the last section is a checklist.

Everything here was read from each product's own documentation on 2026-08-25, with wording
quoted and links on every cell. Where the docs say nothing, the cell says so. Where two
pages from the same vendor disagree, both are quoted; adjudicating that is their job. LiteLLM
is open source, so its gaps were filled from its source code; OpenRouter and Portkey are
closed, so "not documented" there means exactly that and nothing more. For the record, we are
building a router ourselves and the metering layer is where we spend our days, which is why
this is a reading exercise rather than a comparison row.

## Which layer holds the budget: key, user, team, workspace, organization

"Per-key budget" is the entry point, but none of the three stop there, and the layer you
pick decides who can route around the cap by creating another key.

| Layer | LiteLLM | OpenRouter | Portkey |
|---|---|---|---|
| API key | `max_budget` + `budget_duration` on [`/key/generate`](https://docs.litellm.ai/docs/proxy/users); multi-window `budget_limits` | `limit` + `limit_reset` on the key, via dashboard or [Management API](https://openrouter.ai/docs/guides/overview/auth/management-api-keys) | Cost (min $1) or tokens on the [API key](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit) |
| Team / workspace | Team `max_budget` + `budget_duration` | [Workspace budgets](https://openrouter.ai/docs/guides/features/workspaces/workspace-budgets): daily / weekly / monthly / lifetime | [Workspace budget](https://docs.portkey.ai/docs/product/administration/enforce-workspace-budget-limts-and-rate-limits), cost or tokens |
| Organization / account | Organization budget; global `litellm_settings.max_budget` | [Organization](https://openrouter.ai/docs/cookbook/administration/organization-management) shared credit pool ("An organization can only have 10 members. Contact support if you need more.") | [Integration](https://docs.portkey.ai/docs/product/ai-gateway/virtual-keys/budget-limits) budget across all workspaces; an integration is Portkey's name for a connected provider account |
| User / member | Internal user `max_budget`; team-member `max_budget_in_team` | [Guardrail](https://openrouter.ai/docs/guides/features/guardrails) budget, a per-member policy object | Not documented as a first-class layer; policies grouped by `metadata.*` |
| Per model, per key | `model_max_budget` | Not documented (checked 2026-08-25) | Usage-limit policy with `model` condition |
| End customer (no key issued) | `max_end_user_budget_id`, applied to the `user` field | Not documented | Policy with `metadata._user` group-by |
| Who can mint a key, and does the cap follow | Anyone allowed on `/key/generate`; `upperbound_key_generate_params` forces a cap on every new key; a no-team key inherits its owner's `max_budget` ([virtual keys](https://docs.litellm.ai/docs/proxy/virtual_keys)) | "All organization members can create API keys"; members "can only view and manage API keys they created", admins see all; the per-member backstop is a guardrail budget | Not documented on the budget pages checked (2026-08-25) |

The same layers, read for price of entry; only what the pages state is written down.

| Feature | LiteLLM | OpenRouter | Portkey |
|---|---|---|---|
| Per-key budget | No Enterprise mark; database required | No plan gate stated on the [limits page](https://openrouter.ai/docs/api_reference/limits) | "Available on Enterprise plan and select Pro customers" ([API key page](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit)) |
| Per-member budget | No Enterprise mark (`max_budget_in_team`, internal user) | Guardrail budgets; no plan gate stated on the [guardrails page](https://openrouter.ai/docs/guides/features/guardrails) | Policies only: "Enterprise Self Hosting plan only", Gateway 1.17.0+ ([policies](https://docs.portkey.ai/docs/product/enterprise-offering/budget-policies)) |
| Team wallet | No Enterprise mark (team budget) | Organization credit pool, 10 members; workspace budgets "available on the Enterprise plan" ([workspace budgets](https://openrouter.ai/docs/guides/features/workspaces/workspace-budgets)) | Workspace budget: Enterprise and select Pro ([workspace page](https://docs.portkey.ai/docs/product/administration/enforce-workspace-budget-limts-and-rate-limits)) |
| Per-model budget | Enterprise (`model_max_budget`) | Not documented | Policies: Enterprise self-hosted |
| Spend alerts | No Enterprise mark on the [alerting page](https://docs.litellm.ai/docs/proxy/alerting) (`soft_budget`, webhooks) | "Enterprise private preview", email-only ([notifications](https://openrouter.ai/docs/guides/features/notifications)) | Key-level email alerts on the same tier as the budget; policy alerts "are not sent today" |

Three rules inside those tables are worth reading twice.

LiteLLM: "If a key belongs to a team, only the team (and team-member) budgets are enforced;
the key owner's personal budget does not apply." So a personal `max_budget` on a user is not
a ceiling over their team keys. Use `max_budget_in_team` for that.

OpenRouter: guardrail budgets "are enforced per-user and per-key, not shared across all users
with that guardrail", and "Individual API key budgets still apply. The lower limit wins." Two
keys with $20/day each under a member with a $20/day guardrail get blocked at $20 combined,
not $40. That is the behaviour you want; it is also the one that surprises people.

OpenRouter again, on team size: the shared credit pool is an organization, and "An
organization can only have 10 members. Contact support if you need more." A 40-person team
is past that line before it has set a single cap; the last section lists the three roads.

Portkey's five levels stack the same way: the [use-cases page](https://docs.portkey.ai/docs/guides/use-cases/enforcing-limits-and-budgets)
puts it as "Every request passes through each applicable check before reaching the
provider." A key inside a workspace inside an integration has three counters.

![Portkey's Add Budget form on an API key: a Cost / Tokens radio pair, a Budget Limit field whose placeholder reads Min $1, an Alert Threshold field, a Periodic Reset dropdown showing No Periodic Reset, plus Periodic Reset in Days and Set Next Usage Reset At fields](/blog/images/per-key-spending-limits-portkey-budget-form.png "One key's budget in Portkey: the cap is either a dollar amount or a token count, the floor is $1, and the alert threshold is a separate number you have to fill in yourself. Source: [Portkey Docs — Budget and rate limits on an API key](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit).")

## When the check happens: before the request, after the response, or both

This is the axis nobody's marketing page mentions, and it is the one that decides how far
past the cap you can actually go.

**LiteLLM** runs a check before the request and a correction after it. The mechanism is
called [budget reservation](https://docs.litellm.ai/docs/proxy/users#budget-reservation) and
it is on by default:

> LiteLLM estimates the request's maximum cost from the request body and the model's
> pricing. It temporarily reserves that amount against the applicable budget. If the
> reservation would exceed the budget, LiteLLM rejects the request before sending it to the
> provider. After the response is priced, LiteLLM replaces the reservation with the actual
> cost.

The estimate uses `max_tokens` when you send one; otherwise, the docs say, "it uses the
model's configured limits". The source is more specific: [`budget_reservation.py`](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/spend_tracking/budget_reservation.py)
reserves `min(16384, max_output_tokens)` output tokens through a constant named
`DEFAULT_MAX_OUTPUT_TOKENS_FALLBACK`. The actual spend is written by a success callback after
the response ([`proxy_track_cost_callback.py`](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/hooks/proxy_track_cost_callback.py)),
and the hot-path counter lives in Redis with the database reconciled in the background.

**OpenRouter** checks before the request against spend already recorded. The
[workspace budgets FAQ](https://openrouter.ai/docs/guides/features/workspaces/workspace-budgets)
states plainly what that leaves open:

> Budget checks run before the request is routed to a provider. In-flight requests that were
> already dispatched will complete, so actual spend may slightly exceed the budget limit.

The help centre says the same for per-key limits: "Because the check runs per request, a
burst of simultaneous requests can slightly overshoot the limit"
([article 51680687417499](https://openrouter.zendesk.com/hc/en-us/articles/51680687417499)).

**Portkey** checks before the request and says so in one line: "All checks happen before a
request reaches the provider. A blocked request costs nothing." Whether the check reserves
an estimate or reads recorded spend is not documented (checked 2026-08-25), so the
concurrency behaviour is unknown rather than known-bad.

![Timeline of a budgeted request: the cap is checked, the request is dispatched, the response returns, the cost is priced, the counter is updated; concurrent requests all pass the first step before any reaches the last](/blog/images/per-key-spending-limits-leak-window.png "Fig. 1 — the window between check and counter update is where concurrent requests overshoot. A pre-request estimate narrows it; a post-response ledger alone does not.")

## What you get when the cap is hit: 400, 402, 403, 412, or a quiet reroute

| Behaviour on exceed | LiteLLM | OpenRouter | Portkey |
|---|---|---|---|
| Hard reject | ✓ 400 / 401 / 429 depending on the docs example: 400 in the [per-model key and team examples](https://docs.litellm.ai/docs/proxy/team_budgets), 401 for an end user, 429 for a user's per-model budget; the [`BudgetExceededError`](https://github.com/BerriAI/litellm/blob/main/litellm/exceptions.py) class defaults to 429 | ✓ [402 Payment Required](https://openrouter.ai/docs/api_reference/limits) for a key limit or empty balance; [403 Forbidden](https://openrouter.ai/docs/guides/features/workspaces/workspace-budgets) for a workspace or guardrail budget | ✓ [412 Precondition Failed](https://docs.portkey.ai/docs/guides/use-cases/enforcing-limits-and-budgets) for budget, token or request caps; 429 is reserved for rate limits, 401 for an expired key |
| Soft alert, no block | ✓ `soft_budget` on the key; webhook events `threshold_crossed` at 85% and 95%, `projected_limit_exceeded` ([alerting](https://docs.litellm.ai/docs/proxy/alerting)) | ⚠ Per-key spend alert at 80% and 100% by default, "email-only for now", Enterprise private preview ([notifications](https://openrouter.ai/docs/guides/features/notifications)) | ⚠ Alert threshold in USD or tokens, email to org admins, owners and the key creator; for policies, "Email and other proactive alerts for threshold crossings are work in progress; they are not sent today" ([budget policies](https://docs.portkey.ai/docs/product/enterprise-offering/budget-policies)) |
| Fallback to a cheaper model | ✓ [`budget_fallbacks`](https://docs.litellm.ai/docs/proxy/budget_fallbacks) per key, v1.92.x+: "the request is silently rerouted … Spend is attributed to the fallback model, not the exhausted one" | ✗ Not documented as a budget response; `provider.max_price` refuses expensive endpoints per request instead | ✗ Not documented |
| Throttle instead of block | ⚠ `throttle_on_budget_exceeded` exists in [source](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/auth/budget_throttle.py), not on the docs page checked | ✗ Not documented | ✗ Not documented |
| Free models still allowed | ✓ "Budget checks are skipped entirely for zero-cost models" (both cost fields explicitly `0`) | ⚠ A negative balance blocks free models too ([limits](https://openrouter.ai/docs/api_reference/limits)) | ✗ Not documented |

In the table, ✗ means the docs describe no such behaviour or say nothing; ⚠ means it exists
with a condition attached.

The status-code spread is the practical problem. If your client maps 402 to "top up", 403 to
"permission", 412 to "retry with a different precondition" and 429 to "back off and retry",
then three of those four codes will be handled by code written for something else. LiteLLM's
own examples disagree with each other on the code and on the `type` (`budget_exceeded` in the
per-model examples, `auth_error` in the team and end-user ones), and its exception class
defaults to 429, which is the one you least want a budget rejection to wear: a retry loop
will keep knocking, politely, forever. Whatever router you run, match on the error message,
not the number and not the type.

## Reset semantics: rolling durations, UTC midnights, and one weekday the docs disagree on

Reset rules decide whether "$100 a month" means a calendar month or thirty days from the day
someone created the key.

**LiteLLM** has two spellings. A single `budget_duration` is relative: it accepts "seconds
("30s"), minutes ("30m"), hours ("30h"), days ("30d")", the budget "is reset at the end of
specified duration", and the next reset sits on the key as `budget_reset_at`, applied by a
scheduler: "By default, the server checks for resets every 10 minutes, to minimize DB
calls." The same strings inside `budget_limits` align to the calendar instead:

```bash
curl 'http://0.0.0.0:4000/key/generate' \
  --header 'Authorization: Bearer <your-master-key>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "budget_limits": [
      {"budget_duration": "24h", "max_budget": 10},
      {"budget_duration": "30d", "max_budget": 100}
    ]
  }'
```

Per the [docs table](https://docs.litellm.ai/docs/proxy/users): `1h` resets every hour,
`24h` daily at midnight UTC, `7d` every Sunday at midnight UTC, `30d` on the 1st of every
month at midnight UTC. So `"budget_duration": "30d"` on its own is thirty days from the key's
creation and then from each reset; the same `"30d"` inside `budget_limits` is the first of
the month. Two spellings, two calendars, on the same page. Pick one per deployment and write
it down.

**OpenRouter**'s per-key `limit_reset` is `daily`, `weekly`, `monthly` or `null`, and per the
[help centre](https://openrouter.zendesk.com/hc/en-us/articles/51680687417499) "Resets run
automatically at midnight UTC (weeks are Monday–Sunday)". Workspace budgets add `lifetime`
and a rule keys do not have: the limits must be strictly decreasing as the interval narrows,
`lifetime > monthly > weekly > daily`, enforced with a 400 if you get it wrong. The key below
also opts BYOK (bring-your-own-key) traffic into the cap; more on that in Leak 2.

```json
{
  "name": "Customer Instance Key",
  "limit": 50,
  "limit_reset": "monthly",
  "include_byok_in_limit": true
}
```

**Portkey**'s reset options are none, weekly or monthly on an API key; workspaces (via the
Admin API) and policies add a custom `periodic_reset_days` from 1 to 365 with an optional
`next_usage_reset_at`. On which day the week resets, its pages disagree: three app-level
pages, the [API key](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit),
[workspace](https://docs.portkey.ai/docs/product/administration/enforce-workspace-budget-limts-and-rate-limits)
and [provider budget](https://docs.portkey.ai/docs/product/ai-gateway/virtual-keys/budget-limits)
pages, say "every Sunday at 12 AM UTC"; the self-hosted [policies page](https://docs.portkey.ai/docs/product/enterprise-offering/budget-policies)
says "every Monday at 12:00 AM UTC". Both are quoted as written on 2026-08-25; policies run
on the self-hosted Gateway, so these may be two implementations rather than one typo. If a
weekly budget matters to you and it lives on a workspace or a policy, set
`periodic_reset_days: 7` with an explicit `next_usage_reset_at` and stop depending on the
weekday. Keys have no such option.

![The Periodic Reset dropdown on a Portkey API key expanded to its three options: No Periodic Reset, Reset Weekly, Reset Monthly, with the Set Next Usage Reset At field beside it](/blog/images/per-key-spending-limits-portkey-periodic-reset.png "The whole reset vocabulary available on a key: three options, no custom interval. The day the week rolls over is not stated in this control — it is the one the docs disagree on. Source: [Portkey Docs — Budget and rate limits on an API key](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit).")

```python
portkey.admin.workspaces.update(
    workspace_id="WORKSPACE_ID",
    usage_limits=[{
        "type": "cost",
        "credit_limit": 500,
        "alert_threshold": 400,
        "periodic_reset_days": 7,
        "next_usage_reset_at": "2026-09-01T00:00:00Z",
    }],
)
```

One more Portkey rule with no equivalent elsewhere: on the provider (integration) level,
"Once set, budget limits cannot be edited by any organization member"; to change one you
"duplicate the existing provider and create a new one with the desired limit". Budget
limits there are also not retroactive: "The spend counter starts from zero only after you've
set a budget limit for a key."

## Leak 1 — the concurrency window every pre-check leaves open

The cap is a comparison against a counter. Between the comparison and the counter update
there is a window, and every request that enters the window sees the same counter value.
Fire 50 requests at a key with $1 left and, in the simplest model, all 50 pass the check;
the counter finds out later.

OpenRouter documents this twice, quoted above, and the help centre's mitigation list is the
honest version: keep Auto Top-Up off so prepaid credits are the real ceiling, set a per-key
limit, cap `max_tokens`, and use `provider.max_price`. Their own words: "one very large
request, or many requests fired at the same time, can push you a little past a limit before
usage is recorded." Note that the firmest ceiling in that list is the wallet, not the key.

LiteLLM's reservation closes most of the window, with three edges. First, the estimate needs
`max_tokens`; without it the reservation is `min(16384, max_output_tokens)` from the source
quoted above, so a request that will emit 200 tokens holds 16,384 tokens' worth of budget
until the response is priced, and a burst of such requests can hit the cap on reservations
alone. Second, "For routes without token pricing, such as some image and audio routes,
LiteLLM cannot reserve a cost", and a `POST /batches` submission "contains an
`input_file_id` rather than the prompts in the file", so the window reopens there. Third,
the counter is Redis: "If Redis restarts and reloads an older snapshot, the counter can come
back lower than the spend already recorded in the database … which can let a key keep
spending past its `max_budget` until the counter is corrected." The fix is
`fail_closed_budget_enforcement: true`, priced in the same paragraph: roughly one database
read per budgeted entity per cache window per worker, and a 503 "if current spend can be
verified against neither Redis nor the database". Postgres is not optional; Redis holds the
hot-path counter, and the page's own advice for when Redis "is unavailable or contains stale
data" is that setting. With no database at all, `litellm_settings.max_budget` "fails open
there rather than erroring".

Portkey: not documented. The one sentence available says a blocked request costs nothing,
which is true of every pre-check and says nothing about the requests that were not blocked.

## Leak 2 — what counts as spend: cache tokens, reasoning tokens, unpriced models

A budget in dollars is only as good as the pricing function behind it, and each router's
pricing function has holes with different shapes.

**Reasoning tokens.** OpenRouter: "Reasoning tokens are considered output tokens and charged
accordingly", and for models with summarised thinking "usage is billed based on the tokens the
model actually generates", not the visible summary. So a per-key cap does count them, at
output price. LiteLLM prices from its own
[`model_prices_and_context_window.json`](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json);
on the main branch as of 2026-08-25 it holds 3,174 model entries (excluding the file's two
reserved non-model keys, as LiteLLM's own loader does), of which 2,632 carry a per-token
input price and 57 carry an `output_cost_per_reasoning_token`. For the rest, the
[cost calculator](https://github.com/BerriAI/litellm/blob/main/litellm/litellm_core_utils/llm_cost_calc/utils.py)
falls back to the plain output rate, which is correct for most providers and wrong for any
provider that bills reasoning separately.

**Cache reads and writes.** OpenRouter exposes a `cache_discount` field per response and
notes that "Some providers, like Anthropic, will have a negative discount on cache writes,
but a positive discount … on cache reads"; both directions land in the key's usage. Its own
response cache is different: a hit "returns it immediately from cache with no billing (all
billable usage counters are reported as `0`)". On LiteLLM this one errs on the safe side: the
key runs out before the bill does. When an entry has no `cache_read_input_token_cost`, the
same calculator prices cached input at the full input rate, so the provider's discount never
reaches the key's ledger. The field is present on 794 of the 2,632 priced entries, and
`cache_creation_input_token_cost` on 255; many of the rest belong to providers with no prompt
cache to discount, so those numbers are a ceiling on the gap, not a count of broken entries.
The docs' own [debugging page](https://docs.litellm.ai/docs/proxy/cost_tracking) tells you
to "compare token categories (including cache)" when the number does not match the bill.

**Unpriced models.** Portkey is explicit and it is the sharpest edge in this section: "If a
specific request log shows `0 cents` in the COST column, it means that Portkey does not
currently track pricing for that model, and it will not count towards the providers's [sic]
budget limit." A model the gateway cannot price is, under a cost budget, unlimited. Portkey's
answer is the token-based budget, "allowing you to control usage independent of cost
fluctuations". LiteLLM has the same hole in the opposite direction by design: a model with
both cost fields explicitly `0` skips budget checks entirely, and the docs warn that unset
fields do not count as zero.

**BYOK (bring your own key).** On OpenRouter, requests served with your own provider key are
excluded from guardrail budgets and workspace budgets by default; the docs state that default
only for those two, and the key-level `include_byok_in_limit` shows `false` in every example.
The opt-in is `include_byok_in_limit` on the key and `include_byok_in_budgets` on the others,
which then count "the amount OpenRouter would have charged had the request not used your own
provider key". A team that brings its own keys and forgets the toggle has a cap on the part of
its spend that goes through OpenRouter's credits and nothing on the part that does not.

## Leak 3 — retries and fallbacks: who pays for the attempt that failed

A cap meters the request you sent. Routers send more than that.

OpenRouter's help centre has an entire article titled
[Why Am I Still Being Charged When Using My Own Key (BYOK)?](https://openrouter.zendesk.com/hc/en-us/articles/43219817892123),
and the main answer is provider fallback: if your key fails, "the router will fallback and
try another provider that also offers" the model, and "This fallback request is fulfilled
using your OpenRouter credits". The "Always use for this provider" toggle does not stop
this; it only stops OpenRouter from using its own key for the same provider. The request-level
`"provider": {"only": ["anthropic"]}` does stop it, at the price of the request failing
outright when Anthropic does.

LiteLLM's `budget_fallbacks` is the budget-aware version of the same move: when a key's
per-model budget is exhausted "the request is silently rerouted to the first fallback that
still has budget remaining", and spend is attributed to the fallback model. That is the point
of the feature, and it also means the caller asked for one model, paid for another, and got
no error telling them so. On the failure path, the hook releases the reservation and then,
unless `disable_error_logs` is set, writes a failure row against the key
([source](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/hooks/proxy_track_cost_callback.py));
its comment reads "A stream that broke mid-flight still billed the provider for the chunks
already delivered … attribute the real partial spend to this failure row instead of zero",
and guardrail cost rides along. The blind spot is narrower than "failures are free": a
non-streaming request the provider billed but returned as an error, with no usage attached,
lands in the ledger at zero. The docs do not describe either case; that reading is from the
code.

Portkey: retry and fallback cost attribution is not documented (checked 2026-08-25).

## Leak 4 — the price sheet your budget is denominated in

Every budget above is a number in USD. What that number buys depends on the price sheet the
gateway is holding at the moment of the request, and each router holds a different one.
LiteLLM fetches its sheet from the GitHub `main` branch when the process starts
([`get_model_cost_map.py`](https://github.com/BerriAI/litellm/blob/main/litellm/litellm_core_utils/get_model_cost_map.py)),
keeps a bundled copy as the fallback if the fetch fails, and uses your own file only if you
set `LITELLM_LOCAL_MODEL_COST_MAP=True`. So by default you do not choose when a price change
lands: whichever worker restarts next picks up whatever `main` says that day, and two workers
started a week apart can price the same key from two sheets. Portkey prices from its own
pricing support, which is why unpriced models fall through. OpenRouter charges "the same rate
as you would directly with the provider", no markup, at the listed per-endpoint price at
request time, so a provider price change moves the number of requests a $100 key can make on
the day it lands.

How much warning you get before that move is a contract clause, and we read four of them in
[Price-change notice: 14 days, 30 days, or nothing at all](/blog/price-notice-is-a-contract-clause).
The short version: 14 days, 30 days, 30 days with an immediate-effect carve-out, and no
commitment at all. A per-key cap set on the first of the month is a forecast, and its error
bar is that clause.

OpenRouter's own pages show the same drift in miniature. The [pricing page](https://openrouter.ai/pricing)
and the [FAQ](https://openrouter.ai/docs/faq) put the BYOK free allowance at "$25,000 of list
price inference / month" on pay-as-you-go and $200,000 on Enterprise, "measured by
list-price inference cost, not request count" (5% fee after, on top of the 5.5% fee on credit
purchases); the [help-centre article](https://openrouter.zendesk.com/hc/en-us/articles/43219817892123)
linked above, last updated 2026-06-14, says "The first 1,000,000 BYOK requests each calendar
month are free of this fee." Two pages, two units; the rule under your budget can be
rewritten in a place you were not reading.

## What to configure today, per product

Every recommendation here has its cost written next to it. None of them is free.

**On LiteLLM** (self-hosted; per-model budgets are Enterprise, the rest is not marked)

- Run it with a database, or you have no budgets at all. Cost: a Postgres instance and the
  operational duty that comes with it.
- Put every production key in a team and budget the team; add `max_budget_in_team` per
  member. Personal `max_budget` does not reach team keys.
- Use `budget_limits` with a `24h` and a `30d` window rather than a single `budget_duration`,
  so one bad day cannot spend the month. Cost: two counters per key.
- Send `max_tokens` on every request so the reservation estimates from your number, not the
  16,384-token fallback. Cost: a client change and the occasional truncated answer.
- Turn on `fail_closed_budget_enforcement` for keys whose cap is a hard promise. Cost: one
  database read per budgeted entity per cache window per worker, and a 503 when neither
  Redis nor the database can answer.
- Set `upperbound_key_generate_params` so nobody can mint a key without a cap. The docs
  note that upperbounds "also act as defaults and cannot be opted out of". Both settings
  live in `config.yaml`, in different sections:

```yaml
general_settings:
  fail_closed_budget_enforcement: true
litellm_settings:
  upperbound_key_generate_params:
    max_budget: 100
    budget_duration: "30d"
```

**On OpenRouter** (hosted; workspace budgets and spend alerts are Enterprise)

- Under 10 people: one organization, one credit pool, one key per person or service with
  `limit` and `limit_reset`, provisioned through the Management API so the cap is created
  with the key, not added later. Over 10: the org page says "Contact support if you need
  more"; the alternatives are Enterprise workspaces, or one account with many keys where any
  member can mint another and only admins see them all.
- Keep Auto Top-Up off if the account balance is meant to be the ceiling. Cost: a request
  failure at zero instead of a card charge.
- Set `include_byok_in_limit` on every key that might route through your own provider keys,
  and `include_byok_in_budgets` on workspaces and guardrails. Cost: BYOK spend now counts
  at list price toward caps you sized for credit spend.
- Add `provider.only` on BYOK traffic whose fallback you are not willing to pay for. Cost:
  that traffic fails when the provider does.
- Handle 402 and 403 as budget events in the client, not as payment or permission bugs.

**On Portkey** (hosted; budgets are Enterprise and select Pro, policies are Enterprise
self-hosted)

- Budget the integration (provider) first, as the ceiling, then workspaces, then keys; the
  provider budget cannot be edited once set, so size it with the duplication step in mind.
- For anything that must not be unlimited, add a token budget beside the cost budget; the
  cost budget does not see models Portkey cannot price.
- On workspaces and policies, use `periodic_reset_days` plus `next_usage_reset_at` instead
  of `weekly`, until the pages agree on the weekday. Keys only offer weekly or monthly.
- Route 412 to your budget handler and 429 to your backoff handler; they are different
  events and Portkey keeps them apart for you.

Two ownership notes, stated and not interpreted. OpenRouter
[announced on 2026-08-19 that it is joining Stripe](https://openrouter.ai/blog/announcements/openrouter-is-joining-stripe/),
with "nothing about your integration changes" and closing "in the coming weeks". Portkey's
documentation now carries the footer "Portkey is now PRISMA AIRS AI Gateway"
([as seen on the API key page](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit)).
Either could change what these pages say. Re-read them before you copy the numbers into a
runbook; the dates on each quote above are there for that reason.

---

## Sources

### docs.helicone.ai

- [`Helicone-RateLimit-Policy` header](https://docs.helicone.ai/features/advanced-usage/custom-rate-limits)

### docs.litellm.ai

- [`/key/generate`](https://docs.litellm.ai/docs/proxy/users)
- [virtual keys](https://docs.litellm.ai/docs/proxy/virtual_keys)
- [alerting page](https://docs.litellm.ai/docs/proxy/alerting)
- [budget reservation](https://docs.litellm.ai/docs/proxy/users#budget-reservation)
- [per-model key and team examples](https://docs.litellm.ai/docs/proxy/team_budgets)
- [`budget_fallbacks`](https://docs.litellm.ai/docs/proxy/budget_fallbacks)
- [debugging page](https://docs.litellm.ai/docs/proxy/cost_tracking)

### openrouter.ai

- [Management API](https://openrouter.ai/docs/guides/overview/auth/management-api-keys)
- [Workspace budgets](https://openrouter.ai/docs/guides/features/workspaces/workspace-budgets)
- [Organization](https://openrouter.ai/docs/cookbook/administration/organization-management)
- [Guardrail](https://openrouter.ai/docs/guides/features/guardrails)
- [limits page](https://openrouter.ai/docs/api_reference/limits)
- [notifications](https://openrouter.ai/docs/guides/features/notifications)
- [pricing page](https://openrouter.ai/pricing)
- [FAQ](https://openrouter.ai/docs/faq)
- [announced on 2026-08-19 that it is joining Stripe](https://openrouter.ai/blog/announcements/openrouter-is-joining-stripe/)

### docs.portkey.ai

- [API key](https://docs.portkey.ai/docs/product/administration/enforce-budget-and-rate-limit)
- [Workspace budget](https://docs.portkey.ai/docs/product/administration/enforce-workspace-budget-limts-and-rate-limits)
- [Integration](https://docs.portkey.ai/docs/product/ai-gateway/virtual-keys/budget-limits)
- [policies](https://docs.portkey.ai/docs/product/enterprise-offering/budget-policies)
- [use-cases page](https://docs.portkey.ai/docs/guides/use-cases/enforcing-limits-and-budgets)

### github.com

- [`budget_reservation.py`](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/spend_tracking/budget_reservation.py)
- [`proxy_track_cost_callback.py`](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/hooks/proxy_track_cost_callback.py)
- [`BudgetExceededError`](https://github.com/BerriAI/litellm/blob/main/litellm/exceptions.py)
- [budget throttle](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/auth/budget_throttle.py)
- [`model_prices_and_context_window.json`](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
- [cost calculator](https://github.com/BerriAI/litellm/blob/main/litellm/litellm_core_utils/llm_cost_calc/utils.py)
- [`get_model_cost_map.py`](https://github.com/BerriAI/litellm/blob/main/litellm/litellm_core_utils/get_model_cost_map.py)

### openrouter.zendesk.com

- [article 51680687417499](https://openrouter.zendesk.com/hc/en-us/articles/51680687417499)
- [Why Am I Still Being Charged When Using My Own Key (BYOK)?](https://openrouter.zendesk.com/hc/en-us/articles/43219817892123)
