---
title: "Cost Events"
description: "Monitor LLM and HTTP request costs in real time — cost:llm:request and cost:http:request hook events for logging, alerting, and aggregation"
---

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

- **`cost:llm:request`** — emitted when an LLM call finishes (`generateText` / `streamText`), with an LLM usage attribute.
- **`cost:http:request`** — emitted when you attach a dollar cost to an HTTP response with [`addRequestCost`](/packages/http#attaching-request-cost) from `@outputai/http`, with an HTTP request cost attribute.

Use either or both to log spend to your observability stack, trigger alerts, or aggregate cost 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 `cost:llm:request`, `cost:http:request`, or both.
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_cost_hooks.ts
import { on } from '@outputai/core/hooks';
import type { LLMUsageEvent } from '@outputai/llm';

on<LLMUsageEvent>( 'cost:llm:request', async ( { eventId, eventDate, workflowDetails, activityInfo, modelId, total, tokensUsed, usage } ) => {
  console.log( 'LLM call', {
    eventId,
    eventDate,
    workflowId: workflowDetails.workflowId,
    activityId: activityInfo.activityId,
    modelId,
    tokens: tokensUsed,
    cost: total,
    usage
  } );
} );
```

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

on<HttpRequestCostEvent>( 'cost:http:request', async ( { eventId, eventDate, workflowDetails, activityInfo, requestId, url, total } ) => {
  console.log( 'HTTP request', { eventId, eventDate, workflowId: workflowDetails.workflowId, activityId: activityInfo.activityId, requestId, url, total } );
} );
```

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

## LLM request cost

### When events fire

An `cost:llm:request` event is emitted after every `generateText` and `streamText` call completes. For streaming, the event fires when the stream finishes, not when it starts.

### Payload

The handler receives a single LLM usage attribute object:

```json
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventDate": 1780401600000,
  "type": "llm:usage",
  "activityInfo": {
    "activityId": "activity-1",
    "activityType": "generateSummary"
  },
  "workflowDetails": {
    "workflowId": "workflow-123",
    "runId": "run-123",
    "workflowType": "lead_enrichment"
  },
  "outputActivityKind": "step",
  "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
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventId` | `string` | UUID v4 stamped per emit. Use as an idempotency key — `cost:llm:request` and `http:request` for the same fetch get distinct `eventId`s. |
| `eventDate` | `number` | Millisecond epoch timestamp for when the event was emitted. |
| `type` | `"llm:usage"` | Attribute type. |
| `activityInfo` | `object` | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) for the activity that made the call. |
| `workflowDetails` | `object` | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo). |
| `outputActivityKind` | `string` | Output activity kind. Possible values are `step`, `evaluator`, and `internal_step`. |
| `modelId` | `string` | Model identifier (e.g. `claude-sonnet-4-20250514`, `gpt-4o`). From the prompt file config. |
| `usage` | `array` | Priced usage entries for this call. See [Usage entries](#usage-entries). |
| `total` | `number` | Total estimated cost in dollars. |
| `tokensUsed` | `number` | Total number of tokens represented by `usage`. |

### Usage entries

Cost is computed from the model's per-million-token pricing, fetched from a built-in pricing source and cached for 24 hours. For each priced dimension, the dollar amount is `(tokens / 1_000_000) * pricePerMillion`. Non-cached input tokens use `inputTokens - (cachedInputTokens ?? 0)`.

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string` | Usage dimension. |
| `ppm` | `number` | Price per million tokens for this dimension. |
| `amount` | `number` | Token count for this dimension. |
| `total` | `number` | Cost for this dimension. |

`usage[].type` values:

| `type` | Description |
|--------|-------------|
| `input` | Non-cached prompt tokens. |
| `input_cached` | Cached prompt read tokens (`cachedInputTokens`). |
| `output` | Completion tokens. |
| `reasoning` | Reasoning tokens, only when the model defines separate reasoning pricing. |

Only available, finite usage dimensions are included. For example, `reasoning` is omitted when the model does not define separate reasoning pricing, and `input_cached` is omitted when there are no cached input tokens.

## HTTP request cost

Events fire only when your code calls [`addRequestCost( response, total )`](/packages/http#attaching-request-cost) with a **response object created by `@outputai/http`** (or its exported `fetch`). 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 a single HTTP request cost attribute object:

```json
{
  "eventId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "eventDate": 1780401600000,
  "type": "http:request:cost",
  "activityInfo": {
    "activityId": "activity-1",
    "activityType": "searchVendors"
  },
  "workflowDetails": {
    "workflowId": "workflow-123",
    "runId": "run-123",
    "workflowType": "lead_enrichment"
  },
  "outputActivityKind": "step",
  "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. |
| `type` | `"http:request:cost"` | Attribute type. |
| `activityInfo` | `object` | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) for the activity that made the request. |
| `workflowDetails` | `object` | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo). |
| `outputActivityKind` | `string` | Output activity kind. Possible values are `step`, `evaluator`, and `internal_step`. |
| `requestId` | `string` | Internal id linking this payload to the HTTP trace event for that request. |
| `url` | `string` | Final response URL (same as `response.url`). |
| `total` | `number` | Dollar cost passed to `addRequestCost`. |
