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

# LLM cost tracking

Laminar is an open-source, OpenTelemetry-native observability platform for AI agents. It records token usage and dollar cost on every LLM span and rolls both up to the trace, so every agent run carries what it cost, which model spent it, and where the tokens went.

For [auto-instrumented providers and frameworks](/docs/tracing/integrations/overview) this needs no configuration. Initialize the SDK and every LLM call carries token counts and cost. The rest of this page covers what Laminar prices, how it arrives at the number, and how to instrument calls it does not trace automatically.

## What Laminar prices

Providers do not bill every token at the same rate, so Laminar does not either. Each LLM span carries the token counts the provider reported, priced per kind:

| Token type   | How it is priced                                                                                                                                 |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Input        | The model's input rate                                                                                                                           |
| Output       | The model's output rate                                                                                                                          |
| Cache reads  | The provider's discounted cache-read rate                                                                                                        |
| Cache writes | The cache-write rate, including Anthropic's separate 5-minute and 1-hour tiers                                                                   |
| Reasoning    | Taken out of the output count and charged at the model's reasoning rate, or at its output rate when the provider does not publish a separate one |

Two adjustments to the rate itself also happen automatically. A request that crosses a model's long-context threshold is priced at the above-threshold rate for the whole call, the way `gpt-5.6-sol` doubles its input rate past 272k tokens. And a call made on a cheaper or premium service tier, such as OpenAI's `flex` or `priority`, is priced at that tier's rate rather than the standard one.

<Note>
  `gen_ai.usage.input_tokens` is the **total** input count, cached tokens included. Laminar subtracts the cache read and write counts from it to get the tokens billed at the regular input rate, so if you are instrumenting by hand, do not add cached tokens on top of the input total.
</Note>

Every one of these lands as a column on both `spans` and `traces`: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `reasoning_tokens`, `input_cost`, `output_cost`, and `total_cost`. So "how much of last week's bill was reasoning tokens" is a query, not an estimate.

## Break spend down by user, session, or customer

Costs roll up from each LLM span to the span that contains it and on to the trace, so a run that fans out across tools, subagents, and providers still resolves to one number:

```text theme={null}
support-agent                                              $0.0412
├── classify          openai / gpt-5-mini                  $0.0021
├── search_docs       anthropic / claude-sonnet-4-5        $0.0180
│   └── summarize     openai / gpt-5-mini                  $0.0043
└── draft_reply       anthropic / claude-opus-5            $0.0168
```

The same total is available at every level above the trace too. A [session](/docs/tracing/structure/sessions) shows its cost in the sessions table and in the session header, and a project's spend is a [dashboard](/docs/custom-dashboards/overview) away: the **Total cost** preset charts it over time, while **Expensive traces** and **Expensive spans** rank the individual offenders.

Everything past that comes from ordinary [trace structure](/docs/tracing/structure/overview) primitives, and they work the same whether the spans were auto-instrumented or created by hand:

* **Model and provider**, including a mix of providers in one run
* **Agent or workflow**, through span and trace names
* **[Session](/docs/tracing/structure/sessions)**, a full conversation or multi-step run
* **[User](/docs/tracing/structure/user-id)**, through the user ID on the trace
* **[Metadata](/docs/tracing/structure/metadata)** and **[tags](/docs/tracing/structure/tags)**, so you can slice by environment, customer, feature, or experiment

<Tabs items={['TypeScript', 'Python']}>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Laminar, observe } from '@lmnr-ai/lmnr';

    await observe({ name: 'support-agent' }, async () => {
      Laminar.setTraceSessionId(conversationId);
      Laminar.setTraceUserId(userId);
      Laminar.setTraceMetadata({
        environment: 'production',
        customer: 'acme',
        feature: 'inbox-triage',
      });

      // every LLM call inside this trace now carries those dimensions
      await runAgent();
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from lmnr import Laminar, observe

    @observe(name="support-agent")
    def handle_ticket(conversation_id: str, user_id: str):
        Laminar.set_trace_session_id(conversation_id)
        Laminar.set_trace_user_id(user_id)
        Laminar.set_trace_metadata({
            "environment": "production",
            "customer": "acme",
            "feature": "inbox-triage",
        })

        # every LLM call inside this trace now carries those dimensions
        run_agent()
    ```
  </Tab>
</Tabs>

Metadata is the one to reach for when you need per-customer or per-feature reporting, because it is queryable in the [SQL editor](/docs/platform/sql-editor). "What did this customer cost us last month" is one query rather than an export:

```sql theme={null}
SELECT
    simpleJSONExtractString(metadata, 'customer') AS customer,
    round(sum(total_cost), 2) AS cost
FROM traces
WHERE start_time > now() - INTERVAL 30 DAY
GROUP BY customer
ORDER BY cost DESC
```

## Which models and providers are priced

Laminar prices effectively every model from every major provider, and keeps up as providers change their rates. Prices are resolved on the Laminar server rather than in the SDK, so a model that shipped this morning is priced correctly with the SDK version you already have.

Pricing is per span, so a run that mixes providers needs no special handling: each call is priced against its own model and provider, and the trace total is their sum. A [self-hosted](/docs/self-hosting/overview) Laminar prices spans from the same table as the managed cloud, with one caveat: your deployment refreshes it over the network at startup, so an air-gapped install has no built-in prices and needs custom ones.

You supply a price yourself only when there is no public one to look up, such as a private, self-hosted, or fine-tuned model, or when your rate differs from list price. Define it once per project under **Settings > Model costs**, see [Custom model costs](/docs/platform/custom-model-costs). The alternative, for cases where you already compute cost yourself, is to set it directly on the span, covered in [Instrument calls Laminar does not trace automatically](#instrument-calls-laminar-does-not-trace-automatically).

## How Laminar calculates cost

At ingestion, Laminar resolves a price for each LLM span in this order:

1. **Cost attributes on the span.** A non-zero `gen_ai.usage.input_cost`, `output_cost`, or `cost` is taken as the answer and no price table is consulted.
2. **Your project's custom prices**, matched exactly on model and provider.
3. **The built-in price table**, looked up from the model and provider on the span.

The model name comes from `gen_ai.response.model`, falling back to `gen_ai.request.model`. Either one is enough, and the response model takes priority because it is the model the provider actually billed you for. Without a model name there is nothing to look up, cost stays at zero, and that is the most common reason a span shows no price.

The provider, from `gen_ai.system`, narrows the lookup rather than gating it: Laminar tries the provider and model together first, then the model on its own. It matters for models the table lists only under a provider prefix, such as Azure deployments or Gemini, and not at all for OpenAI and Anthropic models, whose names match on their own.

The built-in table also matches forgivingly. Case is ignored, a trailing date snapshot is stripped so `gpt-5-mini-2026-04-01` resolves to `gpt-5-mini` pricing, and a `provider/model` string is understood. Custom prices are the opposite: they match [exactly](/docs/platform/custom-model-costs#names-must-match-your-span-attributes-exactly), so a date suffix on the span that is missing from your entry means no match.

Prices apply at ingestion, so changing a rate does not re-cost spans you have already recorded.

## Instrument calls Laminar does not trace automatically

Auto-instrumentation covers the providers and frameworks on the [integrations list](/docs/tracing/integrations/overview). Create LLM spans by hand for:

* Self-hosted or fine-tuned models behind your own endpoint
* Providers Laminar does not instrument yet
* Direct HTTP calls to an LLM API
* Custom inference servers

### What the span needs

Create the span with span type `LLM`. Without it the span renders as a generic operation, stays out of transcript view, and is skipped by the cost rollup. Then set:

| Attribute     | SDK constant                      | Notes                                                                                            |
| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------ |
| Model         | `RESPONSE_MODEL`, `REQUEST_MODEL` | Required. Set the response model when the API returns one, the request model otherwise, or both. |
| Provider      | `PROVIDER`                        | Recommended. See the identifiers below.                                                          |
| Input tokens  | `INPUT_TOKEN_COUNT`               | Total input count, cached tokens included.                                                       |
| Output tokens | `OUTPUT_TOKEN_COUNT`              | Total output count, reasoning tokens included.                                                   |

Cache and reasoning token counts have no SDK constants. Set them as raw attribute keys from the [LLM attributes reference](/docs/tracing/structure/span-attribute-reference#llm-attributes) if your provider reports them and you want them priced at their own rates.

### Provider identifiers

If you are hand-instrumenting a provider Laminar knows, use the identifier the price table is keyed by. These are the common ones, with a model string that resolves against each:

| Provider      | `gen_ai.system` | Example model                               |
| ------------- | --------------- | ------------------------------------------- |
| OpenAI        | `openai`        | `gpt-5-mini`                                |
| Anthropic     | `anthropic`     | `claude-sonnet-4-5`                         |
| Google Gemini | `gemini`        | `gemini-3-pro-preview`                      |
| Azure OpenAI  | `azure`         | `gpt-5-mini`                                |
| AWS Bedrock   | `bedrock`       | `anthropic.claude-sonnet-4-5-20250929-v1:0` |
| Mistral       | `mistral`       | `mistral-large-2411`                        |
| Groq          | `groq`          | `llama-3.3-70b-versatile`                   |
| xAI           | `xai`           | `grok-4`                                    |
| OpenRouter    | `openrouter`    | `openai/gpt-5-mini`                         |

For anything else, including your own endpoint, use whatever identifier you like and give it a price under **Settings > Model costs**, or set explicit cost attributes on the span.

### Example

<Tabs items={['TypeScript', 'Python']}>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Laminar, LaminarAttributes } from '@lmnr-ai/lmnr';

    const span = Laminar.startSpan({ name: 'custom_llm_call', spanType: 'LLM' });

    try {
      const response = await fetch('https://api.custom-llm.com/v1/completions', {
        method: 'POST',
        body: JSON.stringify({
          model: 'custom-model-1',
          messages: [{ role: 'user', content: 'What is the longest river in the world?' }],
        }),
      }).then((res) => res.json());

      span.setAttributes({
        [LaminarAttributes.PROVIDER]: 'custom-provider',
        [LaminarAttributes.REQUEST_MODEL]: 'custom-model-1',
        [LaminarAttributes.RESPONSE_MODEL]: response.model,
        [LaminarAttributes.INPUT_TOKEN_COUNT]: response.usage?.input_tokens ?? 0,
        [LaminarAttributes.OUTPUT_TOKEN_COUNT]: response.usage?.output_tokens ?? 0,
        // Optional: explicit costs, which override every price table
        [LaminarAttributes.INPUT_COST]: 0.001,
        [LaminarAttributes.OUTPUT_COST]: 0.002,
        [LaminarAttributes.TOTAL_COST]: 0.003,
      });

      return response;
    } catch (error) {
      span.recordException(error as Error);
      throw error;
    } finally {
      span.end();
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests
    from lmnr import Attributes, Laminar

    span = Laminar.start_span(name="custom_llm_call", span_type="LLM")
    try:
        response = requests.post(
            "https://api.custom-llm.com/v1/completions",
            json={
                "model": "custom-model-1",
                "messages": [
                    {"role": "user", "content": "What is the longest river in the world?"}
                ],
            },
        ).json()

        span.set_attributes({
            Attributes.PROVIDER.value: "custom-provider",
            Attributes.REQUEST_MODEL.value: "custom-model-1",
            Attributes.RESPONSE_MODEL.value: response.get("model"),
            Attributes.INPUT_TOKEN_COUNT.value: response.get("usage", {}).get("input_tokens", 0),
            Attributes.OUTPUT_TOKEN_COUNT.value: response.get("usage", {}).get("output_tokens", 0),
            # Optional: explicit costs, which override every price table
            Attributes.INPUT_COST.value: 0.001,
            Attributes.OUTPUT_COST.value: 0.002,
            Attributes.TOTAL_COST.value: 0.003,
        })
    except Exception as error:
        span.record_exception(error)
        raise
    finally:
        span.end()
    ```
  </Tab>
</Tabs>

### Without the SDK

If you ship OTLP directly to Laminar, set the same fields as raw attribute keys on a span you mark as `LLM`:

<Tabs items={['TypeScript', 'Python']}>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const span = tracer.startSpan("custom_llm_call", {
      attributes: {
        "lmnr.span.type": "LLM",
        "gen_ai.system": "openai",
        "gen_ai.request.model": "gpt-5-mini",
      },
    });
    // ... call the model ...
    span.setAttributes({
      "gen_ai.response.model": "gpt-5-mini-2026-04-01",
      "gen_ai.usage.input_tokens": 1284,
      "gen_ai.usage.output_tokens": 162,
      // Optional explicit costs, which override every price table
      "gen_ai.usage.input_cost": 0.0019,
      "gen_ai.usage.output_cost": 0.0024,
      "gen_ai.usage.cost": 0.0043,
    });
    span.end();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    span = tracer.start_span(
        "custom_llm_call",
        attributes={
            "lmnr.span.type": "LLM",
            "gen_ai.system": "openai",
            "gen_ai.request.model": "gpt-5-mini",
        },
    )
    # ... call the model ...
    span.set_attributes({
        "gen_ai.response.model": "gpt-5-mini-2026-04-01",
        "gen_ai.usage.input_tokens": 1284,
        "gen_ai.usage.output_tokens": 162,
        # Optional explicit costs, which override every price table
        "gen_ai.usage.input_cost": 0.0019,
        "gen_ai.usage.output_cost": 0.0024,
        "gen_ai.usage.cost": 0.0043,
    })
    span.end()
    ```
  </Tab>
</Tabs>

For prompt and completion content, cache and reasoning token keys, and the rest of what Laminar reads off an LLM span, see the [LLM attributes reference](/docs/tracing/structure/span-attribute-reference#llm-attributes).

## Where costs show up

* On **trace details**, as the sum of every LLM call in the run. See [Viewing traces](/docs/platform/viewing-traces).
* On **individual LLM spans**, as the cost of that call.
* On **dashboards**, aggregated by provider, model, or any dimension you set. See [Dashboards](/docs/custom-dashboards/overview).
* In the **[SQL editor](/docs/platform/sql-editor)**, for reporting across traces.

## Find wasted spend

Cost tracking tells you what you spent. To find what you wasted, use [Signals](/docs/signals/introduction): plain-language instructions that read whole agent runs and flag the patterns that quietly drive cost, like a tool call retried until the agent gives up. Because a Signal sees the full run rather than isolated token counts, it catches waste that a per-call number cannot show.

Signals are also how you hear about it without watching a dashboard. Every Signal comes with an [alert](/docs/signals/alerts) that fires on its events, in-app, by email, or into Slack.

## FAQ

**Why is cost showing as zero on my spans?**
Cost needs a model name and token counts. Missing either leaves cost at zero. A model string the price table does not recognize has the same effect.

**Can I see cost per user, per session, or per customer?**
Yes. Set a [user ID](/docs/tracing/structure/user-id), [session ID](/docs/tracing/structure/sessions), or [metadata](/docs/tracing/structure/metadata) on the trace and cost aggregates by any of them.

**Which models and providers are supported?**
Effectively every model from every major provider, with pricing kept current. Private, self-hosted, and fine-tuned models are priced through [custom model costs](/docs/platform/custom-model-costs).

**Are cached and reasoning tokens counted?**
Yes, wherever the provider reports them. Both are recorded as their own token counts and priced at their own rates rather than as ordinary input and output.

**Can I override the calculated cost?**
Yes. Cost attributes on a span take precedence over every price table, including your own custom prices.

**Can Laminar cap or block my spending?**
No. Enforcing a hard limit means refusing a request while it is in flight, which requires sitting in the request path, and Laminar reads your traces rather than proxying your calls. It records what you spent and can flag expensive runs through Signals.

## Troubleshooting

| Symptom                                              | Cause                                                                                                            |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Cost is zero                                         | No model name on the span, no token counts, or a model string the price table does not recognize                 |
| Cost looks too high on a long-context agent          | The provider is not reporting cached tokens, so cached reads are priced as full input                            |
| A custom price is not applying                       | Custom-cost matching is exact on both model and provider. See [Custom model costs](/docs/platform/custom-model-costs) |
| Span shows tokens but renders as a generic operation | Span type was not set to `LLM`                                                                                   |

## What's next

<CardGroup cols={2}>
  <Card title="Custom model costs" icon="dollar-sign" href="/docs/platform/custom-model-costs">
    Set your own prices for private, fine-tuned, or negotiated-rate models.
  </Card>

  <Card title="LLM attributes reference" icon="list" href="/docs/tracing/structure/span-attribute-reference#llm-attributes">
    Every attribute key Laminar reads off an LLM span, including the token and cost keys.
  </Card>

  <Card title="Signals" icon="signal" href="/docs/signals/introduction">
    Catch wasteful and failing runs across your whole trace history.
  </Card>

  <Card title="SQL editor" icon="database" href="/docs/platform/sql-editor">
    Query cost across traces for per-customer and per-feature reporting.
  </Card>
</CardGroup>
