# OpenTelemetry

If your app already emits OpenTelemetry spans that follow the GenAI semantic conventions, point an OTLP/HTTP exporter at Veqen and every model call is priced and recorded.

## Configure the exporter

```bash
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://veqen-app.com/v1/otlp/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer vq_live_..."
```

Use an **ingest** key. Veqen accepts OTLP **JSON** (`http/json`), gzip or not; protobuf requests get `415` with a hint to switch. If you also export to another backend, add Veqen as a second exporter or fan out with an OpenTelemetry Collector.

## Which spans count

Only spans that carry `gen_ai.usage.input_tokens` or `gen_ai.usage.output_tokens`. Other spans are ignored. Configure one usage-bearing span per request: if a framework puts usage on both a wrapper and its child, filter one out in your Collector. Do not export the same request through a Veqen SDK wrapper too.

| Veqen field | Span attribute |
|---|---|
| Provider | `gen_ai.provider.name`, else `gen_ai.system` (for example `openai.chat` becomes `openai`) |
| Model | `gen_ai.response.model`, else `gen_ai.request.model` |
| Input tokens | `gen_ai.usage.input_tokens` minus cache reads and cache writes |
| Cached input | `gen_ai.usage.cache_read.input_tokens`, `gen_ai.usage.input_tokens.cache_read`, else `ai.usage.cachedInputTokens` |
| Cache writes | `gen_ai.usage.cache_creation.input_tokens` or `gen_ai.usage.input_tokens.cache_write`; OpenRouter's `_5m` and `_1h` suffixes split the total by TTL |
| Output tokens | `gen_ai.usage.output_tokens` |
| Service tier | `gen_ai.openai.response.service_tier` or `gen_ai.request.service_tier` |
| Customer | `veqen.customer` or `ai.telemetry.metadata.customer` |
| Feature | `veqen.feature`, `ai.telemetry.metadata.feature`, else `ai.telemetry.functionId` |
| User | `veqen.user`, `ai.telemetry.metadata.user`, `enduser.id` or `user.id` |
| Project | `veqen.project` or `ai.telemetry.metadata.project` |
| Environment | `veqen.environment`, `ai.telemetry.metadata.environment`, else the resource's `deployment.environment.name` |

The call's id is the trace id plus span id, so retried exports are stored once.

Counts must be nonnegative integers, and cache reads plus writes cannot exceed total input. Invalid records produce partial-success errors while valid siblings are stored. Only allocation fields are retained; arbitrary span content is discarded. Redact content at the exporter as well, before it leaves your application.

Vercel's `vercel.ai_gateway.cost.total`, when its currency is USD, is retained as **reported** cost. Other spans are priced using your custom prices or Veqen's price book. Neither is an invoice reconciliation. See the [Vercel AI Gateway guide](/guides/vercel-ai-gateway-costs). OpenRouter attribution also accepts `trace.metadata.customer`, `.feature`, `.project` and `.environment`.

Claude Code uses the separate [request-log endpoint](/guides/track-claude-code-costs), `/v1/otlp/v1/logs`. Its cumulative metrics are not accepted as calls.

## Vercel AI SDK

```ts
const result = await generateText({
  model: openai("gpt-5-mini"),
  prompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: "support-bot",                 // becomes the feature
    metadata: { customer: org.id, user: user.id },
  },
});
```

## Responses

`200` with `{}` when everything was stored, or `{"partialSuccess": {"rejectedSpans": n, "errorMessage": "..."}}` when some GenAI spans weren't: invalid (for example, too old), unreadable, or over your plan's limit. Requests can be up to 5 MB (25 MB once gzip is inflated), with any number of spans.
