OpenTelemetry
Send GenAI spans from the Vercel AI SDK, OpenLLMetry, Logfire or the OpenAI Agents SDK straight to Veqen, with no SDK in your code.
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
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. OpenRouter attribution also accepts trace.metadata.customer, .feature, .project and .environment.
Claude Code uses the separate request-log endpoint, /v1/otlp/v1/logs. Its cumulative metrics are not accepted as calls.
Vercel AI SDK
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.