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

# Model Metrics

> Query Gateway model metrics for usage, cost, and performance analytics via API.

The **Gateway Model Metrics Query API** provides a flexible way to query Gateway model and virtual-model metrics for usage, performance, cost, and user activity. You can retrieve either **distribution** (aggregated) or **timeseries** results with powerful filtering and grouping.

<Info>
  This page covers `datasource: "modelMetrics"`. For other datasources, see [MCP](/docs/ai-gateway/fetch-mcp-metrics), [Guardrail](/docs/ai-gateway/fetch-guardrail-metrics), [Cache](/docs/ai-gateway/fetch-cache-metrics), [Routing](/docs/ai-gateway/fetch-routing-metrics), and [Agent](/docs/ai-gateway/fetch-agent-metrics) metrics.
</Info>

All requests go to a single endpoint:

```
POST https://{your_control_plane_url}/api/svc/v1/llm-gateway/metrics/query
```

Send JSON with `Authorization: Bearer <your_api_key>` and `Content-Type: application/json`.

## Access control

Access to metrics is governed by the **data access rules configured by your tenant**. The server applies these rules automatically based on the caller's identity—you don't pass any RBAC or scoping fields in the request. What a caller can query (their own data, their team's data, or tenant-wide data) depends entirely on the rules an admin has set up.

See [Configure Data Access](/docs/ai-gateway/data-access) for how these rules are defined and evaluated.

## Authentication

<Accordion title="Get your API key">
  Authenticate with your TrueFoundry API key. You can use either a Personal Access Token **(PAT)** or Virtual Account Token **(VAT)**.

  1. **Personal Access Token (PAT)**: Go to Access → Personal Access Tokens in your TrueFoundry dashboard
  2. **Virtual Account Token (VAT)**: Go to Access → Virtual Account Tokens (requires admin permissions)

  For detailed authentication setup, see our [Authentication guide](/docs/ai-gateway/authentication).
</Accordion>

## Quick start

<Warning>
  By default, the API returns metrics for **both models and virtual models**. To restrict to one, add `{"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}` for model-only metrics, or `value: false` for virtual-model-only metrics.
</Warning>

<Note>
  The virtual-model column has two aliases. In `groupBy` and `aggregations[].column` use `virtualModel`. In `filters[].fieldName` and in response keys, the name is `virtualModelName`. They refer to the same underlying database column.
</Note>

<Tabs>
  <Tab title="Distribution query">
    Aggregated model metrics including request counts, token totals, p99 latency, and cost grouped by model:

    ```python theme={"dark"}
    import requests

    response = requests.post(
        "https://{your_control_plane_url}/api/svc/v1/llm-gateway/metrics/query",
        headers={
            "Authorization": "Bearer <your_api_key>",
            "Content-Type": "application/json"
        },
        json={
            "startTs": "2026-04-21T00:00:00.000Z",
            "endTs": "2026-04-22T00:00:00.000Z",
            "datasource": "modelMetrics",
            "type": "distribution",
            "aggregations": [
                {"type": "count", "column": "modelName"},
                {"type": "sum", "column": "inputTokens"},
                {"type": "sum", "column": "outputTokens"},
                {"type": "p99", "column": "latencyMs"},
                {"type": "sum", "column": "costInUSD"}
            ],
            "groupBy": ["modelName"],
            "filters": [
                {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
            ]
        }
    )

    print(response.json())
    ```
  </Tab>

  <Tab title="Timeseries query">
    The same shape bucketed hourly:

    ```python theme={"dark"}
    import requests

    response = requests.post(
        "https://{your_control_plane_url}/api/svc/v1/llm-gateway/metrics/query",
        headers={
            "Authorization": "Bearer <your_api_key>",
            "Content-Type": "application/json"
        },
        json={
            "startTs": "2026-04-21T00:00:00.000Z",
            "endTs": "2026-04-22T00:00:00.000Z",
            "datasource": "modelMetrics",
            "type": "timeseries",
            "interval": "1 hour",
            "aggregations": [
                {"type": "count", "column": "modelName"},
                {"type": "sum", "column": "inputTokens"},
                {"type": "p99", "column": "latencyMs"}
            ],
            "groupBy": ["modelName"],
            "filters": [
                {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
            ]
        }
    )

    print(response.json())
    ```
  </Tab>
</Tabs>

## API reference

Post JSON to the endpoint above with `Authorization: Bearer <your_api_key>` and `Content-Type: application/json`.

### Request parameters

<ParamField path="startTs" type="string" required>
  ISO 8601 timestamp marking the **inclusive** lower bound of the query window (e.g. `"2026-04-21T00:00:00.000Z"`).
</ParamField>

<ParamField path="endTs" type="string" required>
  ISO 8601 timestamp marking the **exclusive** upper bound of the query window (e.g. `"2026-04-22T00:00:00.000Z"`).
</ParamField>

<ParamField path="datasource" type="string" required>
  The data source to query. Use `"modelMetrics"` for Gateway model metrics.
</ParamField>

<ParamField path="type" type="string" required>
  The type of query to execute:

  * `"distribution"`: returns aggregated rows (one row per `groupBy` combination).
  * `"timeseries"`: returns time-bucketed rows (one row per bucket per `groupBy` combination). Requires `interval`.
</ParamField>

<ParamField path="aggregations" type="array">
  Array of `{ type, column }` objects describing the aggregations to compute. When omitted, only the implicit `total = COUNT(*)` is returned.

  ```json theme={"dark"}
  "aggregations": [
      {"type": "count", "column": "modelName"},
      {"type": "sum", "column": "inputTokens"},
      {"type": "p99", "column": "latencyMs"}
  ]
  ```

  <Accordion title="Supported aggregation types">
    | Type                                                          | Description                                                   |
    | ------------------------------------------------------------- | ------------------------------------------------------------- |
    | `sum`                                                         | Sum of values                                                 |
    | `count`                                                       | Non-null count of the column                                  |
    | `countDistinct`                                               | Distinct count                                                |
    | `min`                                                         | Minimum value                                                 |
    | `max`                                                         | Maximum value                                                 |
    | `avg`                                                         | Average                                                       |
    | `p5`, `p10`, `p25`, `p50`, `p75`, `p90`, `p95`, `p99`, `p999` | Percentiles (approximate)                                     |
    | `rateSum`                                                     | `sum` normalised by the interval in seconds (timeseries only) |
    | `rateAvg`                                                     | `avg` normalised by the interval in seconds (timeseries only) |
    | `rateMin`                                                     | `min` normalised by the interval in seconds (timeseries only) |
    | `rateMax`                                                     | `max` normalised by the interval in seconds (timeseries only) |
    | `ratePerMinute`                                               | Value divided by the interval in minutes (timeseries only)    |
  </Accordion>

  <Accordion title="Supported aggregation columns">
    | Column                        | Notes                                             |
    | ----------------------------- | ------------------------------------------------- |
    | `costInUSD`                   | Cost incurred (USD)                               |
    | `inputTokens`                 | Number of input tokens                            |
    | `outputTokens`                | Number of output tokens                           |
    | `latencyMs`                   | Total request latency (ms)                        |
    | `timeToFirstTokenMs`          | Time to the first generated token (ms)            |
    | `interTokenLatencyMs`         | Latency between consecutive generated tokens (ms) |
    | `timePerOutputTokenLatencyMs` | Latency per output token (ms)                     |

    All scalar and percentile aggregation types apply to every column above.
  </Accordion>
</ParamField>

<ParamField path="groupBy" type="array">
  Array of field names to group results by. Custom metadata keys are supported with a `metadata.` prefix (e.g. `"metadata.environment"`).

  ```json theme={"dark"}
  "groupBy": ["modelName", "team", "metadata.environment"]
  ```

  <Accordion title="Available group-by fields">
    | Field                  | Notes                                                                         |
    | ---------------------- | ----------------------------------------------------------------------------- |
    | `modelName`            | The underlying model name                                                     |
    | `virtualModel`         | The virtual-model name (when the request was routed through one)              |
    | `requestType`          | Type of request, e.g. `ChatCompletion`, `Embedding`                           |
    | `providerModelName`    | Underlying provider model name                                                |
    | `providerAccountType`  | Account type of the provider (e.g. `model`, `mcp-server`, `guardrail-config`) |
    | `errorCode`            | HTTP error code returned, when applicable                                     |
    | `userEmail`            | Group by user (response key: `createdBySubjectSlug`)                          |
    | `virtualaccount`       | Group by virtual account (response key: `createdBySubjectSlug`)               |
    | `team`                 | Unnests the `Teams` array                                                     |
    | `createdBySubjectType` | Distinguishes `user` vs `virtualaccount`                                      |
    | `metadata.<key>`       | Group by a custom metadata key                                                |

    When `groupBy` contains `userEmail` (without `virtualaccount`), the server auto-injects `WHERE CreatedBySubjectType = 'user'`. `virtualaccount` alone auto-injects `'virtualaccount'`. When both appear, scope it yourself with `createdBySubjectType` if needed.
  </Accordion>
</ParamField>

<ParamField path="filters" type="array">
  Array of filter objects, AND-combined. See [Filtering](#filtering) below for the full operator reference and the per-field allow-list.
</ParamField>

<ParamField path="interval" type="string">
  **Required for timeseries queries.** Bucket size as `<positive integer> <unit>`, where `<unit>` is one of `second`, `minute`, `hour`, `day`, `week`, `month`, `year` (with or without a trailing `s`). Examples: `"30 second"`, `"5 minute"`, `"1 hour"`, `"1 day"`. Compound expressions like `"1 hour 30 minute"` are rejected.
</ParamField>

<ParamField path="intervalInSeconds" type="number" deprecated>
  **Deprecated alias for `interval`.** Accepts a positive integer number of seconds (e.g. `3600` for hourly). Prefer `interval` in new code. If both are provided, `interval` wins.
</ParamField>

## Filtering

Filters narrow down the rows that go into each aggregation and group. They are AND-combined; there is no OR-group support. The server enforces a per-field operator allow-list, so the exact subset of operators you can use depends on the field.

<Tabs>
  <Tab title="Field filters">
    For standard datasource fields, use `fieldName`:

    ```json theme={"dark"}
    {
        "fieldName": "modelName",
        "operator": "IN",
        "value": ["gpt-4", "gpt-3.5-turbo"]
    }
    ```
  </Tab>

  <Tab title="Metadata filters">
    For custom request-metadata keys, use `metadataKey`. Works on every datasource:

    ```json theme={"dark"}
    {
        "metadataKey": "environment",
        "operator": "IN",
        "value": ["production"]
    }
    ```
  </Tab>
</Tabs>

<Accordion title="Filterable fields and allowed operators">
  Most string fields accept the full string operator set: `EQUAL`, `NOT_EQUAL`, `IN`, `NOT_IN`, `STRING_CONTAINS`, `STRING_NOT_CONTAINS`, `STRING_STARTS_WITH`, `STRING_NOT_STARTS_WITH`, `STRING_ENDS_WITH`, `STRING_NOT_ENDS_WITH`. A few fields are narrower: `providerAccountType` and `createdBySubjectType` are comparison-only (no `STRING_*` operators), `traceId` is equality-only, and `virtualModelName` additionally supports `IS_NULL`.

  | Field                            | Type    | Allowed operators                                                                                                                                                          |
  | -------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `modelName`                      | string  | `EQUAL`, `NOT_EQUAL`, `IN`, `NOT_IN`, `STRING_CONTAINS`, `STRING_NOT_CONTAINS`, `STRING_STARTS_WITH`, `STRING_NOT_STARTS_WITH`, `STRING_ENDS_WITH`, `STRING_NOT_ENDS_WITH` |
  | `requestType`                    | string  | same as `modelName`                                                                                                                                                        |
  | `virtualModelName`               | string  | same as `modelName`, plus `IS_NULL`                                                                                                                                        |
  | `providerModelName`              | string  | same as `modelName`                                                                                                                                                        |
  | `errorCode`                      | string  | same as `modelName`                                                                                                                                                        |
  | `providerAccountType`            | string  | `EQUAL`, `NOT_EQUAL`, `IN`, `NOT_IN`                                                                                                                                       |
  | `createdBySubjectType`           | string  | `EQUAL`, `NOT_EQUAL`, `IN`, `NOT_IN`                                                                                                                                       |
  | `traceId`                        | string  | `EQUAL`                                                                                                                                                                    |
  | `userEmail`                      | string  | same as `modelName`                                                                                                                                                        |
  | `virtualAccount`                 | string  | same as `modelName`                                                                                                                                                        |
  | `conversationID`                 | string  | same as `modelName`                                                                                                                                                        |
  | `team`                           | array   | `ARRAY_HAS_ANY`, `ARRAY_HAS_NONE`                                                                                                                                          |
  | `httpStatusCode`                 | number  | `EQUAL`, `NOT_EQUAL`, `IN`, `NOT_IN`, `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_EQUAL`, `LESS_THAN_EQUAL`                                                                 |
  | `latencyMs`                      | number  | `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_EQUAL`, `LESS_THAN_EQUAL`, `BETWEEN`                                                                                            |
  | `inputTokens`                    | number  | `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_EQUAL`, `LESS_THAN_EQUAL`, `BETWEEN`                                                                                            |
  | `outputTokens`                   | number  | `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_EQUAL`, `LESS_THAN_EQUAL`, `BETWEEN`                                                                                            |
  | `costInUSD`                      | number  | `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_EQUAL`, `LESS_THAN_EQUAL`, `BETWEEN`                                                                                            |
  | `isFailure`                      | boolean | `EQUAL`                                                                                                                                                                    |
  | `metadataKey` / `metadata.<key>` | string  | same as `modelName`                                                                                                                                                        |

  For cache-specific fields (`cacheType`, `cacheNamespace`, `cacheLookupStatus`, and the cache token columns), see [Cache Metrics](/docs/ai-gateway/fetch-cache-metrics).
</Accordion>

<Accordion title="Filter operators reference">
  **String field operators**

  | Operator                 | Description                                                                        | Example value                |
  | ------------------------ | ---------------------------------------------------------------------------------- | ---------------------------- |
  | `EQUAL`                  | Exact match                                                                        | `"alice@example.com"`        |
  | `NOT_EQUAL`              | Not equal to value                                                                 | `"bot@example.com"`          |
  | `IN`                     | Match any value in the list                                                        | `["gpt-4", "gpt-3.5-turbo"]` |
  | `NOT_IN`                 | Exclude values in the list                                                         | `["deprecated-model"]`       |
  | `STRING_CONTAINS`        | Contains substring                                                                 | `"gpt"`                      |
  | `STRING_NOT_CONTAINS`    | Does not contain substring                                                         | `"deprecated"`               |
  | `STRING_STARTS_WITH`     | Starts with prefix                                                                 | `"gpt-"`                     |
  | `STRING_NOT_STARTS_WITH` | Does not start with prefix                                                         | `"internal-"`                |
  | `STRING_ENDS_WITH`       | Ends with suffix                                                                   | `"-turbo"`                   |
  | `STRING_NOT_ENDS_WITH`   | Does not end with suffix                                                           | `"-deprecated"`              |
  | `IS_NULL`                | `true` matches rows where the field is unset; `false` matches rows where it is set | `true`                       |

  **Numeric field operators**

  | Operator             | Description                                                                        | Example value     |
  | -------------------- | ---------------------------------------------------------------------------------- | ----------------- |
  | `EQUAL`              | Exact match                                                                        | `1000`            |
  | `NOT_EQUAL`          | Not equal to value                                                                 | `0`               |
  | `IN`                 | Match any value in the list                                                        | `[100, 200, 300]` |
  | `NOT_IN`             | Exclude values in the list                                                         | `[0]`             |
  | `GREATER_THAN`       | Strictly greater than                                                              | `1000`            |
  | `LESS_THAN`          | Strictly less than                                                                 | `5000`            |
  | `GREATER_THAN_EQUAL` | Greater than or equal to                                                           | `100`             |
  | `LESS_THAN_EQUAL`    | Less than or equal to                                                              | `1000`            |
  | `BETWEEN`            | Between two values (inclusive)                                                     | `[500, 5000]`     |
  | `IS_NULL`            | `true` matches rows where the field is unset; `false` matches rows where it is set | `true`            |

  **Boolean field operators**

  | Operator  | Description                                                                        | Example value |
  | --------- | ---------------------------------------------------------------------------------- | ------------- |
  | `EQUAL`   | Exact match                                                                        | `true`        |
  | `IS_NULL` | `true` matches rows where the field is unset; `false` matches rows where it is set | `false`       |

  **Array field operators (used by `team`)**

  | Operator         | Description                                    | Example value                 |
  | ---------------- | ---------------------------------------------- | ----------------------------- |
  | `ARRAY_HAS_ANY`  | Match if the array contains any of the values  | `["team-alpha", "team-beta"]` |
  | `ARRAY_HAS_NONE` | Match if the array contains none of the values | `["excluded-team"]`           |
</Accordion>

<Accordion title="Custom metadata, team unnesting, and combining filters">
  **Custom metadata filtering and grouping.** Every datasource supports filtering and grouping by custom request-metadata keys:

  * **Filter:** `{ "metadataKey": "environment", "operator": "EQUAL", "value": "prod" }`
  * **Group:** include `"metadata.environment"` in the `groupBy` array (string literal, prefix is `metadata.`).

  Metadata fields are treated as strings; use the string field operators above.

  **Implicit team unnesting.** When `team` is in `groupBy` (or used as the column of an aggregation), the server transparently UNNESTs the `Teams` array CTE before applying RBAC. Callers don't need to do anything extra. Rows whose `Teams` array is NULL or empty drop out naturally.

  **Combining multiple filters.** Filters are AND-combined:

  ```json theme={"dark"}
  {
      "startTs": "2026-04-21T00:00:00.000Z",
      "endTs": "2026-04-22T00:00:00.000Z",
      "datasource": "modelMetrics",
      "type": "distribution",
      "filters": [
          {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
          {"fieldName": "modelName", "operator": "IN", "value": ["gpt-4", "gpt-3.5-turbo"]},
          {"fieldName": "latencyMs", "operator": "LESS_THAN", "value": 5000},
          {"fieldName": "team", "operator": "ARRAY_HAS_ANY", "value": ["team-alpha"]},
          {"metadataKey": "environment", "operator": "IN", "value": ["production"]}
      ],
      "groupBy": ["modelName", "team"]
  }
  ```
</Accordion>

## Query examples

Every example posts a JSON body to the endpoint above. To keep the snippets short, only the `json` body is shown; the request wrapper is identical to the [Quick start](#quick-start).

<Note>
  By default, model metrics include **both models and virtual models**. The examples pin the model side with `{"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}`. To target virtual models, flip the value to `false` and swap `groupBy: ["modelName"]` for `groupBy: ["virtualModel"]` (the alias used in `groupBy`/`aggregations`).
</Note>

### Distribution examples

<AccordionGroup>
  <Accordion title="Count by model name">
    Request counts grouped by model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Sum tokens by model">
    Total input and output tokens per model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [
            {"type": "sum", "column": "inputTokens"},
            {"type": "sum", "column": "outputTokens"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Latency percentiles by model">
    p50, p90, and p99 latency grouped by model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [
            {"type": "p50", "column": "latencyMs"},
            {"type": "p90", "column": "latencyMs"},
            {"type": "p99", "column": "latencyMs"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Group by metadata">
    Group by model and a custom metadata key:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName", "metadata.environment"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Multi-dimensional grouping">
    Group by multiple dimensions (model + subject):

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName", "userEmail"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Filter high-latency requests">
    Requests slower than 1 second, grouped by model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "latencyMs", "operator": "GREATER_THAN", "value": 1000}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Filter by latency range">
    Requests within a latency band:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "latencyMs", "operator": "BETWEEN", "value": [500, 5000]}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Filter by token counts">
    Combine input and output token thresholds:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "inputTokens", "operator": "GREATER_THAN", "value": 100},
            {"fieldName": "outputTokens", "operator": "LESS_THAN_EQUAL", "value": 1000}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Filter by team">
    Filter to specific teams using array operators:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["team", "modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "team", "operator": "ARRAY_HAS_ANY", "value": ["team-alpha", "team-beta"]}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Filter by metadata">
    Restrict by a custom metadata value:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"metadataKey": "environment", "operator": "IN", "value": ["production"]}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Virtual-model metrics only">
    Only requests routed through a virtual model. Note `virtualModel` in `groupBy`/`aggregations` but `virtualModelName` in `filters`:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [
            {"type": "sum", "column": "inputTokens"},
            {"type": "sum", "column": "outputTokens"}
        ],
        "groupBy": ["virtualModel"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": false}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Complex filter combination">
    Combine multiple filter types:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "distribution",
        "aggregations": [],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "modelName", "operator": "IN", "value": ["gpt-4", "gpt-3.5-turbo"]},
            {"fieldName": "latencyMs", "operator": "BETWEEN", "value": [100, 10000]},
            {"fieldName": "inputTokens", "operator": "GREATER_THAN", "value": 50},
            {"fieldName": "outputTokens", "operator": "LESS_THAN", "value": 2000}
        ]
    }
    ```
  </Accordion>
</AccordionGroup>

### Timeseries examples

Every timeseries query must include `interval` (or the deprecated `intervalInSeconds`). Buckets are expressed as `<positive integer> <unit>` strings like `"5 minute"`, `"1 hour"`, or `"1 day"`.

<AccordionGroup>
  <Accordion title="Basic hourly counts">
    Hourly request counts:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="5-minute intervals">
    Fine-grained traffic with 5-minute buckets:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-21T06:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "5 minute",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Hourly counts by model">
    Hourly counts grouped by model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Hourly p99 latency by model">
    Track p99 latency regressions per model:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "p99", "column": "latencyMs"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Hourly counts by team">
    Track per-team adoption over time:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "team"}
        ],
        "groupBy": ["team"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Timeseries with model + latency filter">
    Restrict to specific models and a latency threshold:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "modelName", "operator": "IN", "value": ["gpt-4", "gpt-3.5-turbo"]},
            {"fieldName": "latencyMs", "operator": "GREATER_THAN", "value": 500}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Hourly cost by model">
    Cost burn-down per model over time:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "sum", "column": "costInUSD"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Hourly metadata breakdown">
    Group hourly counts by a custom metadata key:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "groupBy": ["metadata.environment"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Daily over a week">
    Daily traffic across a 7-day window:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-14T00:00:00.000Z",
        "endTs": "2026-04-21T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 day",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true}
        ]
    }
    ```
  </Accordion>

  <Accordion title="Complex timeseries query">
    Filters + groupBy + metadata together:

    ```python theme={"dark"}
    json={
        "startTs": "2026-04-21T00:00:00.000Z",
        "endTs": "2026-04-22T00:00:00.000Z",
        "datasource": "modelMetrics",
        "type": "timeseries",
        "interval": "1 hour",
        "aggregations": [
            {"type": "count", "column": "modelName"}
        ],
        "groupBy": ["modelName"],
        "filters": [
            {"fieldName": "virtualModelName", "operator": "IS_NULL", "value": true},
            {"fieldName": "modelName", "operator": "IN", "value": ["gpt-4", "gpt-3.5-turbo"]},
            {"metadataKey": "environment", "operator": "IN", "value": ["production"]}
        ]
    }
    ```
  </Accordion>
</AccordionGroup>

## Response format

Every successful response has the same outer shape:

```json theme={"dark"}
{
  "data": {
    "dataPoints": [
      {
        "startTimestamp": "2026-04-29T12:00:00.000Z",
        "endTimestamp": "2026-04-29T13:00:00.000Z",
        "total": 1234,
        "<aggregationKey>": 0,
        "<groupByKey>": "value-or-null"
      }
    ]
  }
}
```

* **`total`**: implicit `COUNT(*)` for the row. Always present.
* **`<aggregationKey>`**: one key per requested aggregation. The key is `<type><Column>` in camelCase (e.g. `sumLatencyMs`, `p99LatencyMs`, `countModelName`, `countDistinctToolName`).
* **`<groupByKey>`**: one key per `groupBy` entry. The key is the lowerCamelCase form of the underlying column. Two special mappings:
  * `userEmail` and `virtualaccount` both map to `createdBySubjectSlug` in the response (the underlying column is `CreatedBySubjectSlug`, differentiated by `CreatedBySubjectType`).
  * `team` maps to `team` (the value is a single unnested scalar, not an array).
    All other `groupBy` keys preserve their lowerCamelCase name.
* **`startTimestamp`** / **`endTimestamp`**: present only for timeseries responses. Bucket start and end as ISO 8601 timestamp strings; `endTimestamp` equals the next bucket's `startTimestamp`. Distribution responses omit both.

<AccordionGroup>
  <Accordion title="Distribution response example">
    ```json theme={"dark"}
    {
      "data": {
        "dataPoints": [
          {
            "modelName": "gpt-4o",
            "total": 1240,
            "countModelName": 1240,
            "sumInputTokens": 125000,
            "sumOutputTokens": 45000,
            "p99LatencyMs": 2450.5,
            "sumCostInUSD": 8.42
          },
          {
            "modelName": "gpt-3.5-turbo",
            "total": 860,
            "countModelName": 860,
            "sumInputTokens": 89000,
            "sumOutputTokens": 32000,
            "p99LatencyMs": 1820.3,
            "sumCostInUSD": 1.78
          }
        ]
      }
    }
    ```
  </Accordion>

  <Accordion title="Timeseries response example">
    ```json theme={"dark"}
    {
      "data": {
        "dataPoints": [
          {
            "startTimestamp": "2026-04-21T00:00:00.000Z",
            "endTimestamp": "2026-04-21T01:00:00.000Z",
            "modelName": "gpt-4o",
            "total": 25,
            "countModelName": 25,
            "sumInputTokens": 15000,
            "p99LatencyMs": 2100.5
          },
          {
            "startTimestamp": "2026-04-21T01:00:00.000Z",
            "endTimestamp": "2026-04-21T02:00:00.000Z",
            "modelName": "gpt-4o",
            "total": 30,
            "countModelName": 30,
            "sumInputTokens": 18500,
            "p99LatencyMs": 2350.2
          }
        ]
      }
    }
    ```
  </Accordion>
</AccordionGroup>

<Info>
  If `groupBy` is empty or omitted, the response collapses to a single row (or one row per timeseries bucket) summarising every request inside the window.
</Info>

<Note>
  Virtual-model rows surface under the `virtualModelName` key in the response (not `virtualModel`), because the response key is the lowerCamelCase of the underlying database column.
</Note>

### Error responses

A malformed query returns `400 Bad Request`:

```json theme={"dark"}
{
  "statusCode": 400,
  "message": "Invalid query",
  "details": ["..."]
}
```

<Accordion title="Common causes and other status codes">
  Common causes of `400`:

  * Operator not allowed on this field, for example, `EQUAL` on a field that supports only `IN`/`NOT_IN`.
  * Missing required `value` (or wrong shape, e.g. scalar where array is expected for `IN` / `BETWEEN`).
  * Unknown field name for the datasource.
  * Invalid `interval` format (compound expressions, unrecognised unit, non-positive integer).
  * Missing required `interval` for a timeseries query.

  Other status codes:

  * `401 Unauthorized`: missing or invalid bearer token.
  * `403 Forbidden`: caller does not have permission for the requested scope.
  * `500 Internal Server Error`: unexpected server error while executing the query.
</Accordion>
