---
title: "Azure PTU Flat Cost Attribution"
url: "/docs/proxy/ptu_flat_cost"
canonical_url: "https://docs.litellm.ai/docs/proxy/ptu_flat_cost"
type: "docs"
last_updated: "2026-10-09"
summary: "Azure provisioned throughput is billed by the hour for reserved capacity, not per token. A team with its own PTU deployment pays that hourly rate whether it sends one request or a million, so LiteLLM's per-token cost tracking reports..."
related:
  - "/docs/proxy/custom_model_cost_map"
  - "/docs/proxy/pricing_calculator"
---
# Azure PTU Flat Cost Attribution

> Index of all LiteLLM docs: https://docs.litellm.ai/llms.txt


Azure provisioned throughput is billed by the hour for reserved capacity, not per token. A team with its own PTU deployment pays that hourly rate whether it sends one request or a million, so LiteLLM's per-token cost tracking reports nothing that matches the invoice. PTU flat cost attribution fixes the mismatch: you tell LiteLLM how much capacity a deployment reserves and what it costs per hour, and a daily job attributes that cost to the owning team.

A PTU deployment is billed by its reserved capacity alone. LiteLLM stores zero per-token pricing on it, so the traffic the capacity serves is never charged on top of the flat cost.

## Enable it

The feature is off by default and inert until you opt in:

```bash
export LITELLM_ENABLE_PTU_COST_ATTRIBUTION=True
```

With the variable unset the daily job is not scheduled, the model endpoints reject PTU configuration, the usage read path reports zero flat cost, and the PTU inputs stay hidden in the model form.

## Configure a deployment

PTU configuration lives on the deployment's `model_info`. In the Admin UI, open Models + Endpoints, pick the deployment, and edit its settings. The PTU inputs sit directly under the per-token costs, which LiteLLM holds at zero for you:

The same thing through `POST /model/new`:

```bash
curl -X POST http://localhost:4000/model/new \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model_name": "gpt-4o-ptu",
    "litellm_params": {
      "model": "azure/<your-deployment-name>",
      "api_key": "os.environ/AZURE_API_KEY",
      "api_base": "os.environ/AZURE_API_BASE"
    },
    "model_info": {
      "team_id": "<the owning team id>",
      "ptu_count": 100,
      "cost_per_ptu_per_hour": 0.02,
      "ptu_effective_from": "2026-01-01T00:00:00Z"
    }
  }'
```

Or in `config.yaml`:

```yaml
model_list:
  - model_name: gpt-4o-ptu
    litellm_params:
      model: azure/<your-deployment-name>
      api_key: os.environ/AZURE_API_KEY
      api_base: os.environ/AZURE_API_BASE
    model_info:
      id: gpt-4o-ptu-team-a
      team_id: <the owning team id>
      ptu_count: 100
      cost_per_ptu_per_hour: 0.02
      ptu_effective_from: "2026-01-01T00:00:00Z"
```

`model_info.id` is required on a deployment declared this way, and the proxy refuses to load one without it, naming the deployment in the startup log. Left to itself the id is derived from the model name and the resolved `litellm_params`, so rotating the credential mints a new identity and the reservation is charged a second time under it, which nothing later retracts. Any stable string works, and it has to be unique across your deployments

Upgrading an existing reservation that has already accrued cost, set `id` to the id it uses today rather than a fresh name, or the charges already written stay under the old identity and the new one starts beside them. The startup refusal quotes that current id so you can copy it

`team_id` is what the capacity is billed to, so a declaration without one accrues nothing

| Field | Required | Meaning |
| --- | --- | --- |
| `id` | in `config.yaml` | The deployment's stable identity. Not needed through the API or the UI, where one is stored for you |
| `team_id` | yes | The team the capacity belongs to. One deployment maps to one team |
| `ptu_count` | yes | Provisioned throughput units reserved |
| `cost_per_ptu_per_hour` | yes | Your contracted hourly rate per unit |
| `ptu_effective_from` | yes | When the reservation starts accruing |
| `ptu_effective_to` | no | When it stops. Leave unset for an open reservation |

`ptu_count` and `cost_per_ptu_per_hour` must be set together, and `ptu_effective_from` is required because flat cost accrues from that instant. Without it a deployment configured today would bill for days it did not exist.

Take `cost_per_ptu_per_hour` from your Azure agreement rather than a list price; PTU rates are negotiated and vary by region and commitment term.

## How the cost is calculated

A job runs at 00:15 UTC and writes one row per team and model for the previous day:

```
flat cost = ptu_count x cost_per_ptu_per_hour x hours active that day
```

Active hours are the overlap between the day and the reservation window, so a reservation starting at noon accrues 12 hours on its first day and 24 thereafter. The rows are written into `LiteLLM_DailyTeamSpend` under the reserved key `__ptu_flat_cost__`, which keeps flat cost separate from the per-request spend recorded against real API keys.

A reservation that starts before the job first sees it is filled in as well: the catch-up pass prices each elapsed day back to `ptu_effective_from`, up to 91 days. A deployment configured today with a backdated start therefore accrues its whole window on the first run

Flat cost does not count against team or key budgets. Reserved capacity is already paid for, so a team cannot exhaust a budget by using the capacity it reserved.

## Spillover requests

When a PTU deployment is full, Azure can send the overflow to a pay-as-you-go deployment you configure as its spillover target, and bills those requests per token. LiteLLM does the same: a response with `x-ms-is-spilled-over: true` is priced at the served model's standard rates, while requests the PTU serves stay at zero. The hourly flat cost is unchanged either way

Spilled requests are tagged in the spend log metadata, so you can tell them apart on the Logs page:

```json
"azure_spillover": {"from_deployment": "<your-deployment-name>"}
```

This needs `LITELLM_ENABLE_PTU_COST_ATTRIBUTION` set and a pricing map entry for the served model (set `base_model` if the deployment name does not match one). Without either, spilled requests still log at zero

## Read the cost back

`/team/daily/activity` reports `flat_cost` per day and `total_flat_cost` for the range, alongside the usual per-token `spend`:

```bash
curl -s "http://localhost:4000/team/daily/activity?team_ids=<team-id>&start_date=2026-01-01&end_date=2026-01-31" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq '{
    total_spend: .metadata.total_spend,
    total_flat_cost: .metadata.total_flat_cost
  }'
```

The Usage page in the Admin UI shows the same figures under Team Usage, charting flat cost separately from request cost, and CSV export carries them:

## Rates you must not set

LiteLLM refuses a per-token, per-second, or cache rate on a PTU deployment, and names the field it rejected. Sending `0`, an all-zero table, or no value at all is accepted:

```
A PTU deployment bills by reserved capacity, so input_cost_per_token cannot be charged on top
of it. Send 0 or no value, or remove ptu_count and cost_per_ptu_per_hour to bill per token.
```

A rate already stored on a deployment when you add PTU configuration is zeroed rather than refused, so a deployment priced before the feature was enabled heals on its next save. Removing `ptu_count` and `cost_per_ptu_per_hour` releases those zeros, and the deployment goes back to billing per token.

Web search rates are handled the same way. Note that xAI models bill their list price per search call regardless, because their pricing reader ignores a zero rate.

## Limitations

The legacy `POST /model/update` does not run the rules above, so a PTU deployment configured through it keeps billing per token and never accrues flat cost. Use `POST /model/new`, `PATCH /model/{model_id}/update`, the Admin UI, or `config.yaml`

The Python `Router` used on its own zeroes per-token pricing at registration, but nothing schedules the daily job outside the proxy, so flat cost is not accrued there

## Related pages

- [Custom Model Cost Map](https://docs.litellm.ai/docs/proxy/custom_model_cost_map.md)
- [Pricing Calculator (Cost Estimation)](https://docs.litellm.ai/docs/proxy/pricing_calculator.md)
