Trace OpenRouter calls in Maple with Broadcast
Send every OpenRouter model call to Maple with OpenRouter Broadcast, grouped into one Agent Session per conversation.
OpenRouter Broadcast sends a trace of every request on your OpenRouter account to Maple, with tokens, cost, prompt and completion. You set it up once in the OpenRouter dashboard, then add a session_id to your requests. Without it, every model call is its own session.
Quick setup with a coding agent
Copy this prompt into a coding agent that can run shell commands, such as Claude Code, Codex or Cursor. It installs the maple-agent-tracing-openrouter skill and follows it.
Set up Maple agent tracing for OpenRouter in this project.
Install the skill with `npx skills add MapleTechLabs/maple/skills --skill maple-agent-tracing-openrouter -y`, then follow it.
My Maple ingest key is maple_pk_... and my organization is in the US region.
Your ingest key is in Settings → Ingestion. If your organization is in the EU region, change US to EU in the prompt. The agent prints the values for the OpenRouter dashboard, which you enter yourself as described in the next section.
Point Broadcast at Maple
-
In OpenRouter, open Settings → Observability and turn on Enable Broadcast. In an organization account, only an admin can change this.
-
Click the edit icon next to OpenTelemetry Collector and set Endpoint to the full traces URL. OpenRouter doesn’t append
/v1/traces. EU organizations usehttps://ingest.eu.maple.dev/v1/traces.https://ingest.maple.dev/v1/traces -
Set Headers to your Maple ingest key:
{ "Authorization": "Bearer YOUR_INGEST_KEY" } -
Click Test Connection. OpenRouter only saves the destination if the test passes.
Leave the sampling rate at 1.0, since a lower rate drops whole conversations. Leave the API key filter empty. If your app calls eu.openrouter.ai, add the Europe data region.
Send a session id with every request
Send a session_id field in the request body (or an x-session-id header). Use the same id on every request of a conversation, such as your chat thread id, and a new one for each conversation.
With the openai SDK in TypeScript, the field isn’t in the types, so it needs a @ts-expect-error:
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
})
const completion = await client.chat.completions.create({
model: "openai/gpt-4o-mini",
messages,
// @ts-expect-error OpenRouter-only field
session_id: conversationId,
})With the Vercel AI SDK, pass it under providerOptions.openrouter:
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { streamText } from "ai"
const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY })
const result = streamText({
model: openrouter("openai/gpt-4o-mini"),
messages,
providerOptions: { openrouter: { session_id: conversationId } },
}) In Python, use extra_body:
import os
from openai import OpenAI
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
completion = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=messages,
extra_body={"session_id": conversation_id},
) OpenRouter’s own SDKs have a typed field: sessionId in @openrouter/sdk and session_id= in the openrouter Python package.
Nest Broadcast under your own traces
Skip this if OpenRouter is your only source of traces. If your app also sends its own traces to Maple, every model call is recorded twice. Pass the active span’s ids in the trace field so OpenRouter places its spans inside your trace.
In TypeScript, wrap fetch and pass it to the client (new OpenAI({ baseURL, apiKey, fetch: openRouterFetch }) or createOpenRouter({ apiKey, fetch: openRouterFetch })):
import { trace } from "@opentelemetry/api"
export const openRouterFetch: typeof fetch = (input, init) => {
const span = trace.getActiveSpan()?.spanContext()
if (span && typeof init?.body === "string") {
const body = JSON.parse(init.body)
body.trace = { ...body.trace, trace_id: span.traceId, parent_span_id: span.spanId }
init = { ...init, body: JSON.stringify(body) }
}
return fetch(input, init)
} In Python, build the body next to session_id:
from opentelemetry import trace
def openrouter_extra_body(conversation_id: str) -> dict:
body = {"session_id": conversation_id}
ctx = trace.get_current_span().get_span_context()
if ctx.is_valid:
body["trace"] = {
"trace_id": format(ctx.trace_id, "032x"),
"parent_span_id": format(ctx.span_id, "016x"),
}
return body
client.chat.completions.create(model=model, messages=messages, extra_body=openrouter_extra_body(conversation_id)) Use the same value for session_id as your framework’s conversation id.
Broadcast has no tool calls or agent names. For those, trace your app with its framework guide and nest Broadcast under it as shown here.
Check that it works
Run a conversation of three turns with the same session_id. After about a minute, open Agent Sessions and filter by service openrouter.
You should see one session named after your session_id, with vendor OpenRouter, an LLM Generation span per model call, and tokens and cost in USD. To keep content out of Maple, turn on Privacy Mode on the destination.
Troubleshooting
- Test Connection fails. Use the full
https://ingest.maple.dev/v1/tracesURL and valid JSON headers. - Test Connection passes but nothing arrives. Check the destination’s API key filter and data regions against the key and endpoint your app uses, and that you’re not using a placeholder key like
MAPLE_TEST. - Every call is its own session, named
trace:<id>. The request has nosession_id. Log the outgoing body, since some wrappers drop unknown fields. - Tokens or LLM calls are about double. Your app and Broadcast both record each call. Nest Broadcast with the
tracefield as shown above.
Related
- Agent Sessions overview
- OpenRouter Broadcast
- Vercel AI SDK, if you call OpenRouter through
@openrouter/ai-sdk-provider