> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Cost Events

> Monitor normalized LLM usage and cost plus HTTP request costs in real time

The [cost estimation CLI](/costs) shows you costs after a workflow finishes. Metering and cost events expose similar data while a workflow runs:

* **`llm:generation:metering`** - recommended for LLM integrations. It carries normalized usage and cost, including calls without available pricing.
* **`cost:llm:request`** - the compatible legacy LLM cost event. Existing handlers can keep using it.
* **`cost:http:request`** - emitted when you attach a dollar cost to an HTTP response with [`addRequestCost`](/packages/http#attaching-request-cost) from `@outputai/http`.

Use them to log spend and usage to your observability stack, trigger alerts, or aggregate costs per workflow over time.

## Setup

Cost events use the same [hooks system](/operations/error-hooks) as error hooks:

1. Create a hook file and import `on` from `@outputai/core/hooks`.
2. Register a handler for `llm:generation:metering`, `cost:llm:request`, `cost:http:request`, or a combination.
3. Add the file path to `outputai.hookFiles` in `package.json`.

See [Error Hooks - Setup](/operations/error-hooks#setup) for the hook file registration pattern.

```typescript src/llm_metering_hooks.ts theme={null}
import { on } from '@outputai/core/hooks';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';

on<LLMGenerationMeteringEvent>( 'llm:generation:metering', async ( {
  eventId,
  eventDate,
  workflowDetails,
  activityInfo,
  payload
} ) => {
  if (!workflowDetails || !activityInfo || !payload) {
    return;
  }

  console.log( 'LLM call', {
    eventId,
    eventDate,
    workflowId: workflowDetails.workflowId,
    activityId: activityInfo.activityId,
    providerId: payload.usage.providerId,
    modelId: payload.usage.modelId,
    tokens: payload.usage.total,
    cost: payload.cost?.total
  } );
} );
```

```typescript src/http_cost_hooks.ts theme={null}
import { on } from '@outputai/core/hooks';
import type { HttpRequestCostEvent } from '@outputai/http';

on<HttpRequestCostEvent>( 'cost:http:request', async ( { eventId, eventDate, workflowDetails, activityInfo, payload } ) => {
  if (!workflowDetails || !activityInfo || !payload) {
    return;
  }

  console.log( 'HTTP request', {
    eventId,
    eventDate,
    workflowId: workflowDetails.workflowId,
    activityId: activityInfo.activityId,
    requestId: payload.requestId,
    url: payload.url,
    total: payload.total
  } );
} );
```

Handler errors are caught and logged by the framework - they never affect the workflow or the request that triggered them.

## LLM generation metering

### When events fire

`llm:generation:metering` is emitted after generation completes for text, image, and Agent calls whenever usage is available. For direct `streamText()` and `Agent.stream()` usage, it fires when the stream finishes, not when it starts.

### Payload

The handler receives the standard event envelope. `payload.usage` is always present; `payload.cost` is `null` when pricing data is unavailable:

```json theme={null}
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventDate": 1780401600000,
  "activityInfo": {
    "activityId": "activity-1",
    "activityType": "generateSummary"
  },
  "workflowDetails": {
    "workflowId": "workflow-123",
    "runId": "run-123",
    "workflowType": "lead_enrichment"
  },
  "outputActivityKind": "step",
  "payload": {
    "usage": {
      "type": "llm:generation:usage",
      "providerId": "openai",
      "modelId": "gpt-4o",
      "status": "complete",
      "input": 217,
      "output": 9,
      "total": 226,
      "items": [
        { "group": "input", "label": "no_cache", "amount": 217 },
        { "group": "output", "label": "text", "amount": 9 }
      ]
    },
    "cost": {
      "type": "llm:generation:cost",
      "providerId": "openai",
      "modelId": "gpt-4o",
      "status": "precise",
      "input": 0.001085,
      "output": 0.000135,
      "request": null,
      "total": 0.00122,
      "items": [
        { "group": "input", "label": "no_cache", "amount": 217, "ppm": 5, "total": 0.001085, "status": "ok" },
        { "group": "output", "label": "text", "amount": 9, "ppm": 15, "total": 0.000135, "status": "ok" }
      ]
    }
  }
}
```

| Field                | Type                        | Description                                                                                                                                              |
| -------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`            | `string`                    | UUID v4 stamped per emit. Use it as an idempotency key.                                                                                                  |
| `eventDate`          | `number`                    | Millisecond epoch timestamp for when the event was emitted.                                                                                              |
| `activityInfo`       | `object \| undefined`       | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) when the call is made in an activity context.                    |
| `workflowDetails`    | `object \| undefined`       | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo), when available. |
| `outputActivityKind` | `string \| undefined`       | Output activity kind when available. Possible values are `step`, `evaluator`, and `internal_step`.                                                       |
| `payload.usage`      | `LLMGenerationUsage`        | Normalized provider-reported usage.                                                                                                                      |
| `payload.cost`       | `LLMGenerationCost \| null` | Cost calculated from that usage, or `null` when pricing data could not be loaded.                                                                        |

### Normalized usage

`LLMGenerationUsage` records usage independently from pricing. Aggregate `input`, `output`, and `total` fields make common reads direct; `items` preserve the provider breakdown.

| Field        | Type                         | Description                                        |
| ------------ | ---------------------------- | -------------------------------------------------- |
| `type`       | `"llm:generation:usage"`     | Trace attribute type.                              |
| `providerId` | `string`                     | Provider identifier from the loaded prompt.        |
| `modelId`    | `string`                     | Model identifier from the loaded prompt.           |
| `input`      | `number \| null`             | Total reported input usage.                        |
| `output`     | `number \| null`             | Total reported output usage.                       |
| `total`      | `number \| null`             | Sum of available input and output usage.           |
| `status`     | `"complete" \| "incomplete"` | Whether both input and output usage were reported. |
| `items`      | `LLMGenerationUsageItem[]`   | Detailed usage items.                              |

| Field    | Type                               | Description                                                                                                                                                                                                                  |
| -------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group`  | `"input" \| "output" \| "request"` | Aggregate category. `request` covers per-request charges such as grounding with Google Search, billed per request rather than per token.                                                                                     |
| `label`  | `string \| null`                   | Input detail (`no_cache`, `cache_read`, `cache_write`), output detail (`text`, `reasoning`), request detail (`grounding_query`, `grounding_prompt`, or `grounding` when the rate is unknown), or `null` for aggregate usage. |
| `amount` | `number`                           | Count for this dimension: a token count for `input`/`output` items, and a query or grounded-request count for `request` items.                                                                                               |

When the detailed counts do not reconcile with the aggregate count, Output keeps the aggregate input or output item instead of recording a misleading breakdown.

Per-request items are recorded independently from tokens and are excluded from the `input`, `output`, and `total` token aggregates above.

### Normalized cost

`LLMGenerationCost` contains one item for every normalized usage item. Cost is computed from per-million-token pricing fetched from the built-in pricing source and cached for 24 hours. Each item total is `(amount / 1_000_000) * ppm`.

| Field                                 | Type                                       | Description                                                                                                                                                             |
| ------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`, `output`, `request`, `total` | `number \| null`                           | Aggregate dollar costs for available priced items. `request` holds per-request charges (such as grounding), and `total` is the sum of `input`, `output`, and `request`. |
| `status`                              | `"precise" \| "imprecise" \| "incomplete"` | Whether all rates were exact, a fallback rate was used, or usage/pricing was incomplete. A per-request charge with no known rate lands `incomplete`.                    |
| `items[].ppm`                         | `number \| null`                           | Applied price per million tokens.                                                                                                                                       |
| `items[].total`                       | `number \| null`                           | Calculated item cost.                                                                                                                                                   |
| `items[].status`                      | `"ok" \| "fallback" \| "missing"`          | Whether the exact rate, a regular input/output fallback, or no rate was available.                                                                                      |

## Legacy LLM request cost

`cost:llm:request` is emitted on the same completion path when a legacy-priced payload can be calculated. The same payload is retained on the LLM trace as `attributes["llm:usage"]`. Both remain supported with the same payload shape, so existing event handlers and trace readers do not need to migrate immediately. Its `LLMUsageEvent` type is deprecated only to steer new integrations toward the normalized event and attributes.

```typescript src/legacy_llm_cost_hooks.ts theme={null}
import { on } from '@outputai/core/hooks';
import type { LLMUsageEvent } from '@outputai/llm';

on<LLMUsageEvent>( 'cost:llm:request', event => {
  if (!event.payload) {
    return;
  }

  console.log( event.payload.modelId, event.payload.total, event.payload.usage );
} );
```

```json theme={null}
{
  "type": "llm:usage",
  "modelId": "gpt-4o",
  "usage": [
    { "type": "input", "ppm": 5, "amount": 217, "total": 0.001085 },
    { "type": "output", "ppm": 15, "amount": 9, "total": 0.000135 }
  ],
  "total": 0.00122,
  "tokensUsed": 226
}
```

The legacy payload represents priced usage through the historical `input`, `input_cached`, `output`, and `reasoning` line types only. It intentionally retains the old cache-write folding and reasoning fallback behavior, and its shape is frozen: per-request charges such as grounding are never added as new `usage` line types, since existing consumers reduce or map over that array and a new type could break them. Grounding costs are only available on the normalized `llm:generation:cost` attribute and `llm:generation:metering` event described above. The `cost:llm:request` payload and `llm:usage` trace attribute are generated from the same object.

Use `llm:generation:metering` for new integrations. It is more faithful to the provider response because usage is independent from pricing, cache writes and reasoning remain explicit, provider identity is included, and fallback or missing prices are represented rather than hidden.

## HTTP request cost

Events fire only when your code calls [`addRequestCost( response, total )`](/packages/http#attaching-request-cost) with a response returned by `outputFetch` or `createKyClient`. The SDK attaches the cost to the existing HTTP trace event and emits `cost:http:request`. If the response did not originate from this package, `addRequestCost` no-ops (with a console warning) and **no** hook event is emitted.

### Payload

The handler receives an event envelope. The HTTP request cost attribute is available under `payload`:

```json theme={null}
{
  "eventId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "eventDate": 1780401600000,
  "activityInfo": {
    "activityId": "activity-1",
    "activityType": "searchVendors"
  },
  "workflowDetails": {
    "workflowId": "workflow-123",
    "runId": "run-123",
    "workflowType": "lead_enrichment"
  },
  "outputActivityKind": "step",
  "payload": {
    "type": "http:request:cost",
    "requestId": "req-123",
    "url": "https://api.vendor.com/search",
    "total": 0.42
  }
}
```

| Field                | Type                  | Description                                                                                                                                                                                       |
| -------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`            | `string`              | UUID v4 stamped per emit. Use as an idempotency key — `cost:http:request` and `http:request` for the same fetch get distinct `eventId`s, so consumers keying by `eventId` won't collapse the two. |
| `eventDate`          | `number`              | Millisecond epoch timestamp for when the event was emitted.                                                                                                                                       |
| `activityInfo`       | `object \| undefined` | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) when the request is made in an activity context.                                                          |
| `workflowDetails`    | `object \| undefined` | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo), when available.                                          |
| `outputActivityKind` | `string \| undefined` | Output activity kind when available. Possible values are `step`, `evaluator`, and `internal_step`.                                                                                                |
| `payload.type`       | `"http:request:cost"` | Attribute type.                                                                                                                                                                                   |
| `payload.requestId`  | `string`              | Internal id linking this payload to the HTTP trace event for that request.                                                                                                                        |
| `payload.url`        | `string`              | Final response URL (same as `response.url`).                                                                                                                                                      |
| `payload.total`      | `number`              | Dollar cost passed to `addRequestCost`.                                                                                                                                                           |
