> ## Documentation Index
> Fetch the complete documentation index at: https://omnirelay.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tags

> Label calls with key/value tags such as client=acme or env=prod to split usage and cost per customer, project or environment.

Tags are key/value labels you put on a call, such as `client=acme` or `project=invoices`. OmniRelay stores them with the request, so the console can split usage and cost per customer, project or environment. There is nothing to set up: tag a call and it appears on the **Usage** page.

## Adding tags to a call

### With the `X-Gateway-Tags` header

Write `key=value` pairs separated by commas. Spaces around a pair are ignored. If a proxy repeats the header, every copy's pairs are kept.

```bash theme={null}
curl https://api.omnirelay.ai/v1/chat/completions \
  -H "Authorization: Bearer $OMNIRELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Gateway-Tags: client=acme,env=prod" \
  -d '{"model": "gpt-6-luna", "messages": [{"role": "user", "content": "Hello"}]}'
```

### With a `tags` field in the body

```json theme={null}
{
  "model": "gpt-6-luna",
  "messages": [{"role": "user", "content": "Hello"}],
  "tags": {"client": "acme", "project": "invoices"}
}
```

The field is removed before the request reaches a provider. A `tags` field that is not an object, such as the list some SDKs send, is ignored rather than refused.

### As default tags on an API key

Tags set on a key are added to every call made with it, so a key that serves one customer or one environment needs nothing in its requests. Set them under **Default tags** when you create a key or open **Key settings** on the **API Keys** page.

### From the OpenAI SDK

```python Python theme={null}
from openai import OpenAI
import os

client = OpenAI(
    api_key=os.environ["OMNIRELAY_API_KEY"],
    base_url="https://api.omnirelay.ai/v1",
    max_retries=0,
    # Sent on every call made with this client
    default_headers={"X-Gateway-Tags": "client=acme,env=prod"},
)

response = client.chat.completions.create(
    model="gpt-6-luna",
    max_completion_tokens=64,
    messages=[{"role": "user", "content": "Hello"}],
    # Added to this call only; wins over the header on the same key
    extra_body={"tags": {"project": "invoices"}},
)
```

```javascript JavaScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.OMNIRELAY_API_KEY,
  baseURL: "https://api.omnirelay.ai/v1",
  maxRetries: 0,
  defaultHeaders: { "X-Gateway-Tags": "client=acme,env=prod" },
});

const response = await client.chat.completions.create({
  model: "gpt-6-luna",
  max_completion_tokens: 64,
  messages: [{ role: "user", content: "Hello" }],
  // An OmniRelay field; the SDK sends it as part of the body
  tags: { project: "invoices" },
});
```

## Combining sources

When a call gets tags from more than one place, they are merged by key and the call's own tags win: the header over the key's defaults, the body over both.

| API key | Header | Body | Recorded |
| - | - | - | - |
| `client=acme,env=dev` | `env=prod` | `{"project": "invoices"}` | `client=acme`, `env=prod`, `project=invoices` |
| `client=acme` | | `{"client": "globex"}` | `client=globex` |
| | `client=acme,env=prod` | `{"client": "globex"}` | `client=globex`, `env=prod` |

## Rules

| | Rule |
| - | - |
| Number | At most 10 tags per call, counted after the merge. A key default that the call overrides counts once |
| Keys | 1 to 64 characters, stored in lowercase: `Client` is recorded as `client` |
| Values | 1 to 128 characters, kept as sent: `Acme` and `acme` are two different values |
| Characters | Letters, digits and `-` `_` `.` `:` `/`, in both keys and values. No spaces, commas, `=` or accented letters |
| Types | Values are strings. In the body, send `"42"`, not `42` |
| Reserved | Keys starting with `gateway:` or `omnirelay:` |
| Duplicates | A key can appear once in each source. Keys compare in lowercase, so `Client=a,client=b` in one header is a duplicate |

A call that breaks a rule fails with `400 invalid_tags`, is not charged, and still appears on the **Usage** page as a failed request.

> [!WARNING]
>
> Every distinct value is a separate line in your usage breakdown. Use a small set of stable values, such as a customer, project, environment or feature name. Do not put personal data, request IDs or timestamps in tags.

## Reading tagged usage

On the **Usage** page:

* The **Tag** filter keeps calls carrying a tag key, or, with a value, only that value. It combines with the model, provider, key and status filters.
* **Explore → Group by → Tag** splits spend, requests, tokens or errors by one tag key. Calls without that tag form their own row, so the rows add up to the total.
* When the per-request list is enabled for your account, the **Requests** log shows each call's tags and the CSV export has a `tags` column.

To see which tags were recorded for one call, ask for [Request metadata](https://omnirelay.ai/docs/llms/request-metadata.md).

## Compatibility

The header and field names used to carry the OmniRelay name. The old names stay accepted and returned, with no end date, so nothing you already ship needs to change:

| Current name | Former name | Behaviour |
| - | - | - |
| `X-Gateway-Tags` request header | `X-OmniRelay-Tags` | Both are read. If both are sent with different values the new name wins and the metadata block notes `"deprecated_header": "x-omnirelay-tags"` |
| `X-Gateway-Metadata` request header | `X-OmniRelay-Metadata` | Same rule |
| `gateway_metadata` response field | `omnirelay_metadata` | Both are returned with the same content |
| `X-Gateway-Cost-Microusd` response header (Gemini-style calls) | `X-OmniRelay-Cost-Microusd` | Both are returned with the same value |
| `gateway:` reserved tag prefix | `omnirelay:` | Both are reserved |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.