Skip to content
telemetry.dev
Esc
↑↓navigate↵open⌘Jpreview
On this page

OpenRouter

Trace OpenRouter chat, Responses API, embeddings, streams, usage, and cost from TypeScript or Python.

Wrap your client from the official OpenRouter SDK, and each chat, Responses API, and embedding call you make with it becomes a span in telemetry.dev. Your calls and their return values don’t change.

If you call OpenRouter through the OpenAI SDK instead, use the OpenAI integration. It records those calls with the provider openrouter.

What you get

  • Chat and Responses API input, model parameters, output, response IDs, finish reasons, errors, and token usage
  • Embedding input, models, response IDs, and usage
  • Stream output as your code reads it, with time to first chunk
  • The cost OpenRouter reports, as gen_ai.usage.cost

Every span uses the provider openrouter. The integration never records embedding vectors.

Install

npm i @telemetry-dev/sdk @telemetry-dev/openrouter @openrouter/sdk
pip install telemetry-dev-openrouter

The Python package also installs telemetry-dev and openrouter.

Language Requirements
TypeScript Node.js >=20.19.0, @telemetry-dev/sdk ^0.1.8, @openrouter/sdk >=1.1.2 <2
Python Python >=3.10, telemetry-dev >=0.2.6, openrouter >=1.1.0,<2

Trace a call

Set your telemetry.dev project API key and your OpenRouter key. Copy the telemetry.dev key from the project setup page; it starts with td_live_.

export TELEMETRY_DEV_API_KEY=td_live_...
export OPENROUTER_API_KEY=sk-or-v1-...

Call init(), wrap the client, and use it as usual:

import { OpenRouter } from "@openrouter/sdk";
import { flush, init, shutdown } from "@telemetry-dev/sdk";
import { wrapOpenRouter } from "@telemetry-dev/openrouter";

init({ serviceName: "openrouter-app" });
const client = wrapOpenRouter(
  new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }),
);

try {
  const response = await client.chat.send({
    chatRequest: {
      model: "openai/gpt-4o-mini",
      messages: [{ role: "user", content: "Explain OpenTelemetry briefly" }],
    },
  });
  console.log(response.choices[0]?.message.content);
} finally {
  await flush();
  await shutdown();
}
import os

import telemetry_dev
from openrouter import OpenRouter
from telemetry_dev_openrouter import wrap_open_router

telemetry_dev.init(service_name="openrouter-app")
client = wrap_open_router(OpenRouter(api_key=os.environ["OPENROUTER_API_KEY"]))

try:
    response = client.chat.send(
        model="openai/gpt-4o-mini",
        messages=[{"role": "user", "content": "Explain OpenTelemetry briefly"}],
    )
    print(response.choices[0].message.content)
finally:
    telemetry_dev.flush()
    telemetry_dev.shutdown()

Run the script, then open the trace explorer. The trace has one chat openai/gpt-4o-mini generation span with the messages, the reply, token usage, and cost.

Without an API key, init() turns telemetry off and the wrapped client works as before.

Instrument all clients

To trace every client without wrapping each one, call the global function once at startup:

import { instrumentOpenRouter } from "@telemetry-dev/openrouter";

instrumentOpenRouter();
from telemetry_dev_openrouter import instrument_openrouter

instrument_openrouter()

In TypeScript, global instrumentation patches the SDK’s Chat, Responses, and Embeddings resource classes. It doesn’t cover the SDK’s standalone functions, such as chatSend().

API

TypeScript Python What it does
wrapOpenRouter(client) wrap_open_router(client) Instruments one client and returns it. Wrapping the same client again has no effect.
instrumentOpenRouter() instrument_openrouter() Instruments every client.
uninstrumentOpenRouter() uninstrument_openrouter() Removes global instrumentation. Clients you wrapped yourself stay instrumented.

These functions take no options. Note the Python spelling: wrap_open_router, but instrument_openrouter.

Traced calls

OpenRouter call Span name Span type
chat.send() chat {model} generation
responses.send() chat {model} generation
embeddings.generate() embeddings {model} embedding

Python also traces the asynchronous send_async() and generate_async() methods.

Other endpoints aren’t traced. That includes video generation (client.videoGeneration in TypeScript, client.video_generation in Python).

Cost

When OpenRouter returns a cost, the span records it as gen_ai.usage.cost. The integration uses usage.cost first and falls back to the upstream inference cost (usage.costDetails.upstreamInferenceCost in TypeScript, usage.cost_details.upstream_inference_cost in Python). If neither is present, the span has no cost attribute.

Streaming

Chat and Responses streams stay usable as streams and async iterables. The span sees only the events your code reads. It ends when your code reads the stream to the end, cancels or closes it, or the stream fails. A stream you never read never finishes its span.

The integration sends requests unchanged and doesn’t add stream_options, so a stream span has usage and cost only when OpenRouter includes them in the stream.

The span rebuilds chat output from the deltas you read: content, reasoning, refusal text, finish reasons, and tool-call arguments. Responses streams end on a response.completed, response.failed, response.incomplete, or error event. Some other Responses events, such as image generation and web search progress, are kept as telemetry.dev.response_stream_event entries after the output items.

Each stream keeps up to 64 KiB and 1,024 items of output. Past that, the span keeps the start of the output and sets telemetry.dev.capture.truncated to true. Usage and terminal metadata are still recorded.

Captured content

Prompts and responses follow the core SDK’s captureInput, captureOutput, and mask settings (capture_input, capture_output, and mask in Python). With capture off, response IDs, models, finish reasons, usage, cost, timing, and errors are still recorded. See privacy for server-side redaction.

Send telemetry before exit

In a short script, call flush() and then shutdown() before the process exits, as in the example above. In serverless code, call flush() before the handler returns. In TypeScript you can pass a waitUntil function to init() instead. See the TypeScript SDK and Python SDK guides for details.

Last updated on October 18, 2026

Was this page helpful?