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

# Cloudflare AI Gateway Enrichment

> Attribute LLM spend to teams, users, applications, and features by reading your encrypted Cloudflare AI Gateway Logpush logs from an S3 bucket that Vantage joins to your provider costs.

Vantage reads your encrypted [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) Logpush logs from an S3 bucket and joins them to your model-provider costs. Each log record describes one request the gateway proxied: the upstream provider, model, token counts, and any custom metadata you attach. Vantage splits each matching cost row into enriched rows by token share and adds tags derived from your metadata, so you can filter and group that spend in [Cost Reports](/cost_reports), [Virtual Tags](/tagging), [Budgets](/budgets), and [Cost Alerts](/cost_alerts). Because the telemetry comes from your gateway, this surfaces attribution the provider bill never itemized, even when many applications share one API key.

Cloudflare AI Gateway enriches costs for the model providers your gateway proxies: **OpenAI**, **Anthropic**, **Amazon Bedrock**, **Azure OpenAI**, **Google Vertex AI**, **Google AI Studio**, **Cloudflare Workers AI**, **xAI**, **ElevenLabs**, and **Baseten**. Vantage automatically routes each log record to the matching provider's costs.

<Note>
  This integration enriches the costs of the **downstream model providers** your gateway proxies. It is not the same as the [Cloudflare cost integration](/connecting_cloudflare), which imports Cloudflare's own billing. You do not need a Cloudflare cost integration to use enrichment.
</Note>

<Note>
  Enrichment is metadata-only. To read the provider, model, token counts, and the custom metadata you emit, Vantage transiently decrypts each record's encrypted fields (including the request and response bodies) in memory, but it extracts only those values. Vantage does not store your prompt or completion content, and it is never written to a Vantage-owned artifact. This data is not used to train any models.
</Note>

## How It Works

Cloudflare AI Gateway writes one encrypted Logpush record per request to an S3 bucket you own. Vantage decrypts those records with the private key you provide, normalizes them into a common usage index, and joins that index to your provider costs during each provider's cost ingestion.

The enrichment pipeline runs through the following steps:

| Stage               | What Happens                                                                                                                                                                                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Emit             | Cloudflare AI Gateway writes one Logpush record per request, including any custom metadata you attach to attribute cost by.                                                                                                                                               |
| 2. Deliver          | Cloudflare delivers those records as encrypted, gzipped log files to an S3 bucket you own, partitioned by UTC date.                                                                                                                                                       |
| 3. Read and decrypt | Vantage reads the log objects from S3 using a cross-account IAM role you grant read access to, then decrypts each record with the private key you provided.                                                                                                               |
| 4. Index            | Vantage normalizes each record, deduplicates on the request identifier within each day (when duplicates exist, the record with the highest total token count wins), and aggregates tokens into a per-provider usage index keyed by date, provider, model, and token kind. |
| 5. Join and split   | During each provider's cost ingestion, Vantage joins each cost row to the usage index and splits it proportionally by token share.                                                                                                                                        |
| 6. Tag              | Each enriched row is written with the tags from its usage slice plus a `vntg:ai:model` tag.                                                                                                                                                                               |
| 7. Analyze          | The enriched, tagged costs become available in Cost Reports, Virtual Tags, Budgets, and Alerts.                                                                                                                                                                           |

### How Cost Rows Are Split

For each matched cost row, the cost is allocated proportionally across the tag slices present in your logs, weighted by token usage:

$$
\text{attributed cost} = \text{cost row} \times \frac{\text{slice tokens}}{\text{total matched tokens for row}}
$$

Splits are additive: the sum of the enriched rows always equals the original cost row to the cent. Existing provider-level reports continue to show the same totals; enrichment only *makes new dimensions available* on the underlying rows.

<Info>
  Consider the below example: one cost row for `gpt-5.2-2025-12-11` is `$100` for the day, covering 100M input tokens. Your gateway logs for that day group into these tag slices:

  | Tag slice                   | Input tokens | Tags                                 |
  | --------------------------- | ------------ | ------------------------------------ |
  | Slice A                     | 50M          | `team=growth, purpose=summarization` |
  | Slice B                     | 30M          | `team=research, purpose=eval`        |
  | (usage not covered by logs) | 20M          | (none)                               |

  After enrichment, the single `$100` row becomes three rows:

  | Amount | `team`   | `purpose`     | Source         |
  | ------ | -------- | ------------- | -------------- |
  | `$50`  | growth   | summarization | Enriched split |
  | `$30`  | research | eval          | Enriched split |
  | `$20`  | —        | —             | Leftover       |

  The total is unchanged at `$100`. The **leftover** row carries the portion of the cost row's tokens that your logs do not cover; it keeps the provider's existing tags and the `vntg:ai:model` tag but none of the slice tags, so totals always reconcile and no dollars are lost. Cost rows with no matching logs pass through without allocation splits.

  If your logs instead report more usage than the provider billed, Vantage still allocates only the billed cost, split across your tag slices in proportion to each slice's usage. The surplus does not create additional cost, and there is no leftover row in that case.
</Info>

### Data Freshness and Backfill

Enrichment runs as part of each provider's existing cost ingestion, so it follows that provider's refresh cadence. Recent days are reprocessed within a rolling three-day window so late-arriving logs are picked up. See the [provider data refresh documentation](/provider_data_refresh) for per-provider timing.

A billing period is enriched whenever it is processed while an active source exists, for as long as the matching logs remain in your bucket. Late-arriving logs for recent days are picked up automatically within the rolling three-day window. For older periods, Vantage re-enriches a day when the set of log objects for that day changes (files are added or removed) or when a full rebuild is triggered; replacing the contents of an existing object under the same key does not, on its own, re-trigger enrichment. Re-enrichment reads the already-normalized cost data, so it does not require a full cost re-import.

## Prerequisites

Before you begin, make sure:

* An active cost integration exists for at least one supported provider: [OpenAI](/connecting_open_ai), [Anthropic](/connecting_anthropic), or [Azure](/connecting_azure).
* An active [AWS integration](/connecting_aws) (cross-account IAM role) exists for the account that owns the S3 bucket. Vantage reads the bucket through this role, so it is required even when the model-provider costs you enrich are not themselves on AWS.
* Cloudflare AI Gateway **Workers Logpush** is available to you (it requires the Workers Paid plan), and logging is enabled on the gateway whose traffic you want to attribute.
* You have an S3 bucket you own where Cloudflare AI Gateway delivers (or will deliver) your Logpush logs.
* You have an RSA key pair for [Workers Logpush encryption](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/): the public key is uploaded to your AI Gateway settings, and you have the matching PEM-encoded private key to provide to Vantage.
* You have a Vantage **Organization Owner** or **Integration Owner** role. See [Role-Based Access Control](/rbac).

## Set Up Cloudflare AI Gateway Enrichment

Setup has two steps: [configure Cloudflare AI Gateway Logpush to your S3 bucket](#step-1-configure-cloudflare-ai-gateway-logpush) and [connect the source in Vantage](#step-2-connect-the-source-in-vantage), where you select the bucket, provide your decryption key, grant Vantage read access, and then review which providers Vantage detected.

### Step 1: Configure Cloudflare AI Gateway Logpush

In Cloudflare, turn on logging for the gateway, then create a [Logpush job](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/) for your AI Gateway that delivers logs to your S3 bucket. When you create the job, select the **AI Gateway** dataset so Vantage receives gateway logs rather than another log type.

* **Destination:** point the job at the S3 bucket you own, and enable **[Organize logs into daily subfolders](https://developers.cloudflare.com/logs/logpush/logpush-job/enable-destinations/aws-s3/)** in the destination setup (or include `{DATE}` in the destination path if you configure the job through the API). This produces the day-partitioned path Vantage requires; without it, Vantage cannot discover your logs. Vantage expects the day partition and gzip log objects at the end of the path:

```text theme={null}
s3://<bucket>/<optional-prefix>/YYYYMMDD/*.log.gz
```

* **Timestamp format:** in the Logpush job's advanced options, keep the timestamp format as **RFC3339** (the default). **Unix** (epoch seconds) is also accepted, but do **not** use **UnixNano**. Vantage reads those values as seconds, which produces invalid dates, so those records will not be enriched.
* **Encryption:** generate an RSA key pair and upload the public key to your AI Gateway settings, as described in [Workers Logpush](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/). Uploading the public key is what enables Workers Logpush; Cloudflare then encrypts the `Metadata`, `RequestBody`, and `ResponseBody` fields of each record, and Vantage decrypts them with the private key you provide in Step 2. The private key is required.
* **Custom metadata:** attach [custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/) to your gateway requests to drive allocation. Cloudflare saves up to five metadata entries per request (extra entries are dropped), accepts only string, number, and boolean values, and reserves keys beginning with `cf.`. Each saved entry that Vantage considers safe becomes a tag on the enriched costs (see the [Enrichment Tag Reference](#enrichment-tag-reference)). Prefer stable keys with a limited set of values, such as `team`, `environment`, and `purpose`.

Each S3 object is a gzip-compressed file of newline-delimited JSON records. A single record has plaintext gateway fields plus encrypted body fields:

```json theme={null}
{
  "Provider": "openai",                          // upstream provider proxied by the gateway
  "Model": "gpt-5.2-2025-12-11",                  // model as reported by the provider
  "Gateway": "my-gateway",                        // gateway name
  "Endpoint": "responses",                        // API path the request used
  "Cached": false,                                // whether the gateway served a cached response
  "RateLimited": false,                           // whether the request was rate limited
  "StatusCode": 200,                              // upstream HTTP status
  "Metadata": { "type": "encrypted", "key": "...", "iv": "...", "data": "..." },
  "RequestBody": { "type": "encrypted", "key": "...", "iv": "...", "data": "..." },
  "ResponseBody": { "type": "encrypted", "key": "...", "iv": "...", "data": "..." }
}
```

After Vantage decrypts the record, it reads the following from each field. Vantage reads only the values below; prompt and completion content are never stored.

| Field                                                                            | What Vantage reads                                                                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Provider`                                                                       | The upstream provider that handled the request. Cloudflare sets this automatically; you do not control it. Vantage routes each record to the matching provider's costs when the provider is supported.                                                                                                               |
| `Model` / `ResponseBody.model` / `RequestBody.model`                             | The model identifier as reported by the provider. Vantage uses the first non-blank value as the default join key against your provider cost data.                                                                                                                                                                    |
| `ResponseBody.usage`                                                             | Token counts for the request: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, and `cache_write_input_tokens`, plus optional `uncached_input_tokens` (Vantage also reads `input_tokens_details.cached_tokens` for cached input). At least one count must be a positive integer for the record to be used. |
| `ResponseBody.id`                                                                | The provider request identifier, used to deduplicate records. When absent, Vantage derives a stable fingerprint from the request ID, timestamp, model, endpoint, and usage.                                                                                                                                          |
| `Metadata.timestamp` / `ResponseBody.created_at` / `Timestamp` / object key      | The request time, which determines the billing day the record joins to. Vantage uses the first available value, in that order, falling back to a timestamp embedded in the object key.                                                                                                                               |
| `RequestBody.batch` (or `RequestBody.is_batch`)                                  | Whether the request used batch processing. Surfaced as `vntg:ai:is_batch`.                                                                                                                                                                                                                                           |
| `Metadata.resourceAccountId` (or `resource_account_id`)                          | The provider account that generated the usage. Set it when one bucket carries logs for multiple integrations of the same provider so Vantage matches each request to the correct costs.                                                                                                                              |
| `Metadata` (other keys)                                                          | Your custom metadata, turned into allocation tags.                                                                                                                                                                                                                                                                   |
| `Metadata.providerRegion` / `region`, `serviceTier`, `ResponseBody.service_tier` | Region and service tier, used to match and allocate costs and available as grouping dimensions.                                                                                                                                                                                                                      |
| `Gateway`, `Endpoint`, `Cached`, `RateLimited`                                   | Gateway request context. `Gateway`, `Cached`, and `RateLimited` are added as tags; `Endpoint` is promoted to `vntg:ai:endpoint`.                                                                                                                                                                                     |
| `StatusCode`, `ResponseBody.error`, `RateLimited`                                | Used to determine whether the request succeeded. Non-successful requests are skipped.                                                                                                                                                                                                                                |

<Note>
  Vantage normalizes model identifiers on both sides so gateway logs and provider billing join even when they differ. For example, OpenAI billing always uses dated model names (`gpt-5.2-2025-12-11`) while your gateway logs may use dated or undated names (`gpt-5.2`); Vantage prefers the dated form and normalizes undated names to match.
</Note>

<Warning>
  Never include API key values, credentials, secrets, or other sensitive authentication material in custom metadata. Prefer keeping request-scoped identifiers out of metadata as well. Request identifiers and `resourceAccountId` / `resource_account_id` are never turned into tags, and high-cardinality keys make reports harder to use.
</Warning>

### Step 2: Connect the Source in Vantage

In Vantage, go to the [Integrations page](https://console.vantage.sh/settings/integrations). Under **LLM Enrichment**, add **Cloudflare AI Gateway**, then select the S3 bucket (or buckets) that hold your Logpush logs and paste your decryption key. The connect screen walks you through granting read access and validating it, and connecting then scans your recent logs so you can review which providers were found.

Connecting reuses your existing [AWS cross-account IAM role](/connecting_aws#how-vantage-connects-with-aws), adding read-only access to the bucket that holds your logs. It does not create a new role or require new credentials.

<Note>
  You can connect one or more buckets per account. After you connect a bucket, Vantage scans your recent logs and you choose which of your supported provider cost integrations should receive enrichment from that bucket; you can change that selection later from the source's **Edit** screen. Provider cost integrations you connect *after* setting up a source are not enriched automatically; open the source's **Edit** screen and enable the new integration there.
</Note>

<Steps>
  <Step title="Select the bucket and provide your decryption key">
    Select the S3 bucket (or buckets) that receive your Logpush logs, and paste the PEM-encoded private key that matches the public key you configured in Cloudflare. Vantage uses this key only to decrypt your records; it does not decrypt without it.

    <Note>
      There is no prefix field to fill in: Vantage discovers the log path from where your objects actually live. To narrow where Vantage looks (for example, if the permission check reports too many objects), point the Logpush job at a dedicated bucket or a more specific destination path rather than sharing the bucket with unrelated data.
    </Note>

    <Note>
      If your bucket does not appear in the list, it may not be synced into Vantage's inventory yet. Newly created buckets can take up to 24 hours to appear.
    </Note>
  </Step>

  <Step title="Grant Vantage read access">
    <Warning>
      Deploy in the **AWS account that owns the bucket**, and make sure the bucket ARN in the policy matches the bucket you connect in Vantage. A mismatch is the most common cause of an `access denied` result when you check permissions.
    </Warning>

    The connect screen offers three ways to grant access, each prefilled for your account.

    <Tabs>
      <Tab title="AWS Console">
        The connect screen provides a CloudFormation quick-create link that opens the AWS Console with the stack name, template URL, and parameters prefilled. Acknowledge that CloudFormation may create IAM resources, then create the stack.

        The stack applies the [Vantage-published template](https://vantage-public.s3.amazonaws.com/vantage-integration-custom-llm-enrichment.json) with two parameters:

        * `VantageCrossAccountRole`: the name of your existing Vantage cross-account IAM role.
        * `CustomLlmEnrichmentBuckets`: a comma-separated list of bucket ARNs, each as `arn:aws:s3:::your-bucket/*`.

        Deploy the stack in the account that owns the bucket, not necessarily your management/payer account.
      </Tab>

      <Tab title="AWS CLI">
        The connect screen provides a ready-to-run `aws cloudformation create-stack` command using the same template and parameters as the Console method:

        ```bash theme={null}
        aws cloudformation create-stack \
          --stack-name CustomLLMEnrichmentSourceAccess-<id> \
          --template-url https://vantage-public.s3.amazonaws.com/vantage-integration-custom-llm-enrichment.json \
          --capabilities CAPABILITY_IAM \
          --region us-east-1 \
          --parameters \
            ParameterKey=VantageCrossAccountRole,ParameterValue=<your-cross-account-role-name> \
            ParameterKey=CustomLlmEnrichmentBuckets,ParameterValue=arn:aws:s3:::your-bucket/*
        ```

        Run the command with credentials for the AWS account that owns the bucket. For multiple buckets, pass a comma-separated list of ARNs as a single `ParameterValue`.
      </Tab>

      <Tab title="Terraform">
        If you manage your Vantage integration with the [`vantage-sh/vantage-integration/aws`](https://registry.terraform.io/modules/vantage-sh/vantage-integration/aws/latest) module, add the log read permissions through the module's `additional_inline_policies` argument:

        ```hcl main.tf theme={null}
        module "vantage-integration" {
          source = "vantage-sh/vantage-integration/aws"

          additional_inline_policies = [
            {
              name = "CustomLLMEnrichmentSource-<access_credential_token>"
              policy = jsonencode({
                Version = "2012-10-17",
                Statement = [
                  {
                    Action   = ["s3:GetObject", "s3:GetObjectAcl"],
                    Resource = ["arn:aws:s3:::your-bucket/*"],
                    Effect   = "Allow"
                  }
                ]
              })
            }
          ]
        }
        ```

        This attaches the policy to your existing cross-account role; it does not create a new role. Bucket listing and location come from the base role, so only object read is added here. See the [Terraform documentation](/terraform#vantage-terraform-integrations-module-for-aws) for base module setup.
      </Tab>
    </Tabs>

    Whichever method you use, Vantage needs the following read-only access to validate and ingest logs:

    | Permission             | When it is needed                                                                                                  |
    | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
    | `s3:GetObject`         | Read the log objects in the bucket you connect.                                                                    |
    | `s3:ListBucket`        | List objects under the log path to find your logs.                                                                 |
    | `s3:GetBucketLocation` | Resolve the bucket's Region.                                                                                       |
    | `kms:Decrypt`          | The bucket or objects are encrypted with AWS KMS (SSE-KMS). Without it, Vantage cannot read KMS-encrypted objects. |

    Your existing Vantage cross-account role already grants `s3:ListBucket` and `s3:GetBucketLocation`. The grant above adds only object read on the log bucket (`s3:GetObject`, and the generated policy always includes `s3:GetObjectAcl`).

    <Note>
      The CloudFormation, CLI, and Terraform options above grant S3 read access only. If your log bucket uses SSE-KMS encryption, you must grant `kms:Decrypt` separately. See [KMS-Encrypted Buckets](#kms-encrypted-buckets). This is separate from Cloudflare Logpush log encryption, which Vantage handles with the private key you provide.
    </Note>
  </Step>

  <Step title="Check permissions and connect">
    Click **Check Permissions**. Vantage validates that it can resolve the bucket Region, list objects, and read a sample object, and that your private key can decrypt a sample log record. Blocking problems (for example, objects that are not under the `YYYYMMDD/*.log.gz` layout, or a private key that cannot decrypt the sample) are shown as errors you must resolve before connecting. Then click **Connect**. Vantage confirms the source and immediately scans your recent gateway logs, from the first day of the previous calendar month through today, to detect which providers and provider accounts appear in the bucket. You are taken to a **Scanning gateway logs** page that refreshes on its own; a large bucket can take several minutes.

    <Check>
      A success message confirms the source is connected and scanning has begun. When the scan finishes, a **Review** action appears for the source. See the [Troubleshooting](#troubleshooting) section for any errors that are displayed.
    </Check>
  </Step>

  <Step title="Review detected providers and confirm">
    When the scan finishes, select **Review** for the source. The review screen has one row per enrichable connected cost integration, grouped by provider, and shows each row's **Valid rows** and **Invalid rows** counts. Providers found in your logs that you have not connected a cost integration for appear as non-selectable rows. The **Notes** column shows **Success** when rows were found, **No rows found** when none were, and flags such as gateway logs missing `resource_account_id` (the `Metadata.resourceAccountId` value after decryption) or **Account identifiers not found: …** when account IDs could not be matched to a cost integration. Logs found for unsupported providers appear in a separate banner, not the Notes column. If more than 75% of the scanned rows failed validation, a warning ("N% of scanned log rows failed validation") suggests confirming that your objects use the expected `YYYYMMDD/*.log.gz` format and contain provider, model, usage, and status data.

    Choose which connected cost integrations should receive token enrichment from this bucket, then select **Confirm**. Vantage enriches costs for the selected integrations on each provider's next data refresh. You can change the selection later from the source's **Edit** screen.
  </Step>
</Steps>

## Manage Cloudflare AI Gateway Enrichment Sources

Manage your connected buckets from the **Cloudflare AI Gateway** integration page. The sources table lists each bucket with its AWS account, creation date, and **Status** (for example, **Pending** before an import starts, **Importing** while one runs, **Stable** once all imports succeed, **Warning** if only some imports fail, **Error** if a scan or all imports fail, or **Paused** if the source is stopped). A shared-bucket warning icon appears next to the bucket name when sibling integrations require `resourceAccountId`; error details appear beneath the status. Each row has a primary button, **Review**, **Edit**, or **Resume**, depending on the source's state, and a **⋮ (More actions)** menu with **View import history** and, for sources that are not paused, **Stop**.

### Choose Which Integrations Are Enriched

After a scan finishes, select **Review** on the source to choose which connected cost integrations receive enrichment (see [Step 2](#step-2-connect-the-source-in-vantage)). To change that selection later, select **Edit** in the source's row. The edit screen lists each provider integration with its import status (for example, **Stable**, **Pending**, **Importing**, **Warning**, **Error**, **Paused**, or **Unenriched**), and for a provider with multiple accounts shows a count like *2 of 3 accounts enabled*. It lets you enable or disable enrichment per integration. Disabling an integration here stops going-forward enrichment for it but leaves its existing enriched history in place.

<Note>
  A given cost integration can have only one active gateway enrichment source. Cloudflare AI Gateway and [Custom LLM Enrichment](/custom_llm_enrichment) cannot both enrich the same provider cost integration at the same time.
</Note>

### View Import History

In the sources table, open the row's **⋮ (More actions)** menu and select **View import history** to open the source's **Import History**. The screen lists one row per provider cost integration and billing period that ran enrichment, with columns for the **Integration** (with its account beneath), **Status**, **Billing Period**, **Last Enriched At**, **Log Match Rate**, **Logs Parsed**, and **Logs Skipped**. When a run needs attention (for example, a shared bucket whose logs lack `resourceAccountId`), the reason appears in the **Status** column. Lifecycle changes also appear as marker rows: **Added** when enrichment is first enabled for an integration, and **Paused** or **Resumed** when you stop, disable, resume, or re-enable it.

### Stop a Source

To stop a source, open the row's **⋮ (More actions)** menu and select **Stop**, then confirm in the **Stop enrichment source** dialog. This is a soft deactivation, not a hard delete: Vantage stops enriching new cost data for that bucket, but your existing enriched history is left unchanged, and the source stays visible in the list so you can bring it back at any time.

<Note>
  Stopping a source does not re-import or roll back already-enriched costs. Going-forward enrichment simply pauses for that bucket until you resume it.
</Note>

### Resume a Source

To resume a stopped source, select **Resume** in its row. Resuming restarts going-forward enrichment for the integrations that were active when you stopped the source (integrations you had already disabled individually stay disabled) and opens the Edit screen so you can adjust the selection. A stopped bucket cannot be re-added through the connect wizard; use **Resume** instead.

## View Enriched Costs on Cost Reports

Once enrichment runs, a single provider cost line is split into multiple rows, each carrying enrichment tags. You can filter and group by these tags anywhere tags are supported: [Cost Reports](/cost_reports), [Virtual Tags](/tagging), [Budgets](/budgets), and [Cost Alerts](/cost_alerts).

Because enrichment splits (allocates) your provider costs, enrichment tags behave like Vantage's [cost allocation tags](/tagging#cost-allocation-tags): you can build a Virtual Tag on them, but a cost can be allocated only once, so an enrichment tag can belong to only [one allocation chain](/tagging#virtual-tag-allocation-rules). Allocated values show the allocated badge (calculator icon) in the console.

### Enrichment Tag Reference

| Tag key                                                                                   | Description                                                                                                                                                                                | Example value                      |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
| *Your metadata keys*                                                                      | Each safe scalar entry in a record's `Metadata` (Cloudflare saves up to five per request) becomes a tag on the enriched rows for that usage slice, with the key normalized to snake\_case. | `team`, `purpose`, `environment`   |
| `gateway`, `cached`, `rate_limited`                                                       | Gateway request context promoted from the top-level `Gateway`, `Cached`, and `RateLimited` fields. (`Endpoint` becomes `vntg:ai:endpoint` instead of a bare `endpoint` tag.)               | `gateway=my-gateway`               |
| `vntg:ai:model`                                                                           | The normalized model identifier that generated the cost, derived from the cost row. Vantage adds it to every enriched row.                                                                 | `vntg:ai:model=gpt-5.2-2025-12-11` |
| `vntg:ai:model_provider`                                                                  | The provider family inferred from the model (for example, OpenAI or Anthropic), added automatically alongside `vntg:ai:model` when the family is recognized.                               | `vntg:ai:model_provider=OpenAI`    |
| `vntg:ai:token_type`                                                                      | The token kind the enriched row covers: `input`, `output`, `cache_read`, or `cache_write`. Added automatically to every enriched row.                                                      | `vntg:ai:token_type=input`         |
| `vntg:ai:service_tier`, `vntg:ai:provider_region`, `vntg:ai:endpoint`, `vntg:ai:is_batch` | Promoted from the matching log fields when present and renamed under the prefix, so you can group and allocate by them too.                                                                | `vntg:ai:service_tier=default`     |
| *Provider's existing tags*                                                                | Any tags Vantage already puts on the provider's cost rows are preserved on the enriched rows. If one of these shares a key with a slice tag, the enrichment value wins.                    | —                                  |

Your own metadata keys are normalized to snake\_case and, unless they are reserved, appear without a provider prefix. The fields Vantage derives and adds itself, such as the model identifier, carry a `vntg:ai:` prefix (for example, `vntg:ai:model`); this keeps them distinct from your keys and consistent with the AI tags Vantage applies to provider costs. Because a key like `team` is not provider-namespaced, it lines up across OpenAI, Anthropic, and Azure, so you can group your entire AI stack by one `team` tag.

Two normalization rules are worth knowing when you choose metadata key names:

* **Keys are lowercased and snake\_cased before they become tags.** So `userEmail`, `UserEmail`, and `user_email` all collapse to `user_email`, and any keys that normalize to the same string are merged into one tag.
* **Keys that normalize to a Vantage-managed name are re-emitted under `vntg:ai:` instead of staying bare.** Those names are `model`, `provider_region`, `service_tier`, `user_email`, `user_name`, `user_id`, `is_batch`, `endpoint`, and `api_key_id` (and `resource_account_id`, which is dropped rather than tagged). For example, the `user` key in the Use Cases table below stays bare as `user`, but a `userId` key would silently move to `vntg:ai:user_id`.
* **A metadata key can overwrite a gateway-context tag.** The `gateway`, `cached`, and `rate_limited` tags come from the top-level gateway fields, but a metadata key that normalizes to one of those names replaces the gateway-derived value. Avoid reusing those names in your metadata.
* **`Metadata.timestamp` does double duty.** It is used to pick the billing day and, because it is not a reserved key, also becomes a `timestamp` tag. Since a timestamp is high-cardinality, avoid emitting it as metadata unless you want it as a tag.

Enrichment tags behave like any other provider tag in the console:

* **To group:** open the **Group By** menu, select **Tag**, and choose the tag key, for example `vntg:ai:model` (values like `gpt-5.2-2025-12-11`) or a metadata key such as `team`.
* **To filter:** open the **Filters** menu, click **New Rule**, select **Tag**, choose the **Tag Key**, then pick an operator and one or more values.

The tag keys appear in the **Tag Key** dropdown once enriched costs exist. If you use a handful of keys often, mark them as [preferred tags](/tagging#preferred-tags) so they sort to the top of these menus.

<Note>
  Enriched rows retain the provider's existing tags and add the Vantage-managed `vntg:ai:*` tags (such as `vntg:ai:model`); split rows also carry the tags from their usage slice. When a slice tag and an existing tag use the same key, the enrichment value wins. The leftover row (usage not covered by logs) keeps the provider's existing tags and the applicable `vntg:ai:*` tags but carries none of the slice tags. Request identifiers and `resourceAccountId` are never turned into tags.
</Note>

## Troubleshooting

Most errors are displayed when you click **Check Permissions** or **Connect**. The tables below list the messages you may see, their cause, and how to resolve them. Expand a section for the relevant category.

<AccordionGroup>
  <Accordion title="Permission and access errors">
    | Message                                                                                                                                                                             | Cause and fix                                                                                                                                                                                                       |
    | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `Vantage cannot determine the location for bucket {bucket}. Grant s3:GetBucketLocation to the cross-account role.`                                                                  | Add `s3:GetBucketLocation` to the cross-account role.                                                                                                                                                               |
    | `Vantage cannot list objects under s3://{bucket}/{prefix}. Grant s3:ListBucket for the configured prefix.`                                                                          | Add `s3:ListBucket` scoped to the bucket (and prefix, if used).                                                                                                                                                     |
    | `Vantage cannot read object s3://{bucket}/{key}. Grant s3:GetObject to the cross-account role.`                                                                                     | Add `s3:GetObject` (and `s3:GetObjectAcl`) for the bucket.                                                                                                                                                          |
    | `Vantage cannot decrypt KMS-encrypted object s3://{bucket}/{key}. Grant kms:Decrypt to the cross-account role.`                                                                     | The bucket uses SSE-KMS. Grant `kms:Decrypt` for the encrypting key.                                                                                                                                                |
    | `Vantage could not assume the AWS cross-account role. Verify that the role exists, its trust policy allows Vantage to call sts:AssumeRole, and the configured external ID matches.` | Vantage cannot assume your [cross-account IAM role](/connecting_aws#how-vantage-connects-with-aws). Confirm the role still exists, its trust policy allows Vantage's `sts:AssumeRole`, and the external ID matches. |

    Confirm the permissions were deployed in the **AWS account that owns the bucket**, and that the bucket ARN in the policy matches the connected bucket.
  </Accordion>

  <Accordion title="Decryption errors">
    | Message                                                                    | Cause and fix                                                                                                                                                     |
    | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `The uploaded private key could not decrypt the Cloudflare sample object.` | The private key does not match the public key configured for Logpush encryption in Cloudflare. Provide the private key that pairs with the configured public key. |
    | `The Cloudflare sample object is not valid gzip JSON Lines.`               | The sampled object is not gzip-compressed JSON Lines, or is not an encrypted Cloudflare Logpush file. Confirm the Logpush job writes `*.log.gz` objects.          |
    | `Vantage cannot read the Cloudflare sample object.`                        | Vantage could list the bucket but was denied reading the sample object. Confirm `s3:GetObject` (and `kms:Decrypt` for SSE-KMS) on the bucket.                     |
    | `Cloudflare decryption private key ...`                                    | The pasted value is not a valid PEM-encoded RSA private key. Paste the full PEM key, including the `BEGIN`/`END` lines.                                           |
  </Accordion>

  <Accordion title="Bucket and log errors">
    | Message                                                                                                                                                                         | Cause and fix                                                                                                                                                                        |
    | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `Bucket {bucket} does not exist or could not be found.`                                                                                                                         | The bucket does not exist or is not reachable by the role. Verify the name and account.                                                                                              |
    | `No objects matching YYYYMMDD/*.log.gz were found in s3://{bucket}/{prefix}. Vantage expects files in that date layout.`                                                        | The bucket has no `.log.gz` objects under the expected `YYYYMMDD` path yet, or they use a different layout. Confirm the Logpush destination path, write a test record, and re-check. |
    | `Vantage scanned 3000 objects in s3://{bucket}/{prefix} but could not find a matching log file. Use a dedicated bucket or a more specific prefix.`                              | Vantage scanned many objects without finding a matching log file. Point the source at a dedicated bucket or a more specific prefix so logs are found quickly.                        |
    | `Permission check timed out. The bucket may be large, or logs may not use the expected date layout.`                                                                            | The permission check did not finish in time. Narrow the source to a dedicated bucket or prefix, confirm the `YYYYMMDD/*.log.gz` layout, then re-check.                               |
    | `Bucket {names} could not be found for your account.`                                                                                                                           | The selected bucket is not in Vantage's synced inventory. Newly created buckets can take up to 24 hours to appear.                                                                   |
    | `Bucket {name} is paused. Resume it from the table below instead of reconnecting.`                                                                                              | The bucket already has a stopped source. Use **Resume** in the sources table instead of connecting it again.                                                                         |
    | `Buckets cannot be configured more than once: {names}.`                                                                                                                         | You selected a bucket that is already connected as a source. Manage the existing source from the sources table instead.                                                              |
    | `Connect an AWS, Anthropic, Azure, GCP, or OpenAI cost integration before connecting Cloudflare AI Gateway Enrichment.`                                                         | No enrichable cost integration exists. Connect a supported provider first.                                                                                                           |
    | `All cost integrations that can carry token costs already have an active gateway enrichment source. Disconnect that source before connecting Cloudflare AI Gateway Enrichment.` | Every eligible cost integration is already claimed by another gateway enrichment source (for example, a Custom LLM Enrichment source). Stop or disable that source first.            |
  </Accordion>

  <Accordion title="My costs are not being split">
    Enrichment can only split costs by dimensions present in your logs. Costs pass through without allocation splits when any of the following is true:

    * There is no active Cloudflare AI Gateway source for the account, or the logs have not been ingested for that billing period.
    * The cost row's model or token type could not be matched to a logged request for that provider, date, and token kind.
    * Records were skipped during ingestion because the decrypted `Metadata` was missing or not an object, the `Provider` was blank or unsupported, the model was blank or could not be matched to your provider cost data, the request was not successful (a missing or empty response body, a non-2xx status, a rate-limited request, or a response error), or no usage value was a positive integer.

    Confirm the request went to a supported provider, carries a matching model, and has at least one positive usage count.
  </Accordion>

  <Accordion title="My totals changed after enabling enrichment">
    Totals should not change. Splits are additive and always sum to the original cost row to the cent, even when multiple sources are connected. If a total appears different, this is not expected behavior; contact [Vantage Support](mailto:support@vantage.sh).
  </Accordion>
</AccordionGroup>

### KMS-Encrypted Buckets

If your log bucket is encrypted with AWS Key Management Service (KMS), **Check Permissions** reports a decryption error until Vantage can read the objects. Either remove encryption on the bucket, or grant the cross-account role `kms:Decrypt` for the encrypting key and allow that role in the KMS key policy. The CloudFormation, CLI, and Terraform options in [Step 2](#step-2-connect-the-source-in-vantage) grant S3 read access only, so KMS access must be added separately. This is distinct from Cloudflare Logpush log encryption, which Vantage handles with the private key you provide.

## Use Cases

Each row below shows example metadata to emit on your gateway requests and what that attribution enables in Vantage. The key names are examples; you choose your own, and Vantage surfaces whatever metadata you send. All rows assume logs are delivered to a connected bucket.

| Use Case                            | Emit in metadata      | What It Enables                                                                                                                                                                    |
| ----------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chargeback by team                  | `team`                | Group a [Cost Report](/cost_reports) by `team` for per-team LLM chargeback, or roll `team` values into a business hierarchy with a [Virtual Tag](/tagging).                        |
| Per-user attribution on shared keys | `user`                | Attribute spend to the individual who made each request even when many callers share one API key, which the provider bill cannot break apart.                                      |
| Per-feature or per-workflow spend   | `feature`, `workflow` | Compare cost per product feature or workflow (for example, `summarization` vs. `classification`).                                                                                  |
| Model and effort mix                | `mode`, `effort`      | Spot macro patterns (which models and effort levels dominate spend) and outliers to guide defaults. The `vntg:ai:model` tag is added automatically, so you do not need to emit it. |
| Environment separation              | `environment`         | Filter non-production inference out of showback reports, or budget each environment (`production`, `staging`) separately.                                                          |

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Which providers are supported?">
    OpenAI, Anthropic, Amazon Bedrock, Azure OpenAI, Google Vertex AI, Google AI Studio, Cloudflare Workers AI, xAI, ElevenLabs, and Baseten. Azure support covers direct Azure integrations; Azure CSP billing accounts are not supported.
  </Accordion>

  <Accordion title="Do I need a Cloudflare cost integration?">
    No. This integration enriches the costs of the model providers your gateway proxies, not Cloudflare's own billing. You only need a supported model-provider cost integration and an AWS integration for the account that owns the log bucket. The [Cloudflare cost integration](/connecting_cloudflare) is separate and optional.
  </Accordion>

  <Accordion title="Do I still need Custom LLM Enrichment?">
    Not for traffic captured by Cloudflare AI Gateway; native enrichment removes the need to transform those logs into the common schema yourself. [Custom LLM Enrichment](/custom_llm_enrichment) remains the right choice for non-Cloudflare sources, mixed pipelines, and teams that already emit the Token Cost Allocation Specification from their own collectors.
  </Accordion>

  <Accordion title="What Vantage permissions do I need to enable this?">
    You need the **Organization Owner** or **Integration Owner** role to connect the source. Viewing the enriched costs does not require a special role; the resulting tags follow the same access rules as other cost data. See [Role-Based Access Control](/rbac).
  </Accordion>

  <Accordion title="What access does Vantage gain in my AWS account?">
    Read-only access to the log bucket you connect: `s3:GetObject`, `s3:ListBucket`, `s3:GetBucketLocation`, and `kms:Decrypt` for KMS-encrypted objects (generated policies may also include `s3:GetObjectAcl`). The policy attaches to your existing cross-account role; no new role is created. See [Step 2: Connect the Source in Vantage](#step-2-connect-the-source-in-vantage).
  </Accordion>

  <Accordion title="Does Vantage have write access to Cloudflare or my S3 bucket?">
    No. Vantage does not use a Cloudflare API token for this integration, does not modify your gateway configuration, and does not write to the connected S3 bucket; it only reads the encrypted log objects. This path is separate from the [Cloudflare cost integration](/connecting_cloudflare), which imports Cloudflare platform spend (such as Workers) through the Cloudflare API.
  </Accordion>

  <Accordion title="Why does Vantage need a private key?">
    Cloudflare AI Gateway Logpush encrypts the `Metadata`, `RequestBody`, and `ResponseBody` fields of each record with your public key. Vantage decrypts those fields with the matching private key you provide so it can read the provider, model, token counts, and your custom metadata. The private key is stored securely and used only for decryption.
  </Accordion>

  <Accordion title="Will this change my totals or break existing reports?">
    No. Splits are additive and always sum to the original cost row. Existing provider-level reports continue to show the same totals; enrichment only makes new dimensions available on the underlying rows.
  </Accordion>

  <Accordion title="Does Vantage store my prompt content?">
    No. Enrichment is metadata-only. To read request identifiers, the provider and model, usage counts, and the custom metadata you emit, Vantage transiently decrypts each record's encrypted fields (including the request and response bodies) in memory, but it extracts only those values. Prompt and completion text are never collected, stored, or written to a Vantage-owned artifact.
  </Accordion>
</AccordionGroup>
