Skip to content
Monitoring
Skill

/maple-agent-tracing-openrouter

Trace OpenRouter calls with Maple: route OpenRouter Broadcast (OTLP) to Maple and edit the app's OpenRouter requests to send session_id and trace ids, so each conversation is one Maple Agent Session with model calls, tokens and real cost. Triggers on 'trace my openrouter calls',

BOOST
From plugin
maple
1.8k37 skills
Install
$ npx -y skills add mapletechlabs/maple --skill maple-agent-tracing-openrouter --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/maple-agent-tracing-openrouter

Context preview

The summary Claude sees to decide when to auto-load this skill.

Trace OpenRouter calls with Maple: route OpenRouter Broadcast (OTLP) to Maple and edit the app's OpenRouter requests to send session_id and trace ids, so each conversation is one Maple Agent Session with model calls, tokens and real cost. Triggers on 'trace my openrouter calls',

SKILL.md

maple-agent-tracing-openrouter.SKILL.md
name: maple-agent-tracing-openrouter
description: "Trace OpenRouter calls with Maple: route OpenRouter Broadcast (OTLP) to Maple and edit the app's OpenRouter requests to send session_id and trace ids, so each conversation is one Maple Agent Session with model calls, tokens and real cost. Triggers on 'trace my openrouter calls', 'add Maple to openrouter', 'agent sessions for openrouter', 'OpenTelemetry for openrouter', 'openrouter broadcast to maple'."

Maple agent tracing: OpenRouter Broadcast

Goal: every conversation = one Maple Agent Session with each OpenRouter model call, its tokens, cost (`gen_ai.usage.total_cost`, OpenRouter's real charge) and prompt/completion. If the app already exports its own traces to Maple, the Broadcast spans nest inside them and each call is counted once.

Mechanism: OpenRouter Broadcast, configured in the OpenRouter dashboard, exports one OTLP/HTTP JSON trace per request (scope and `service.name` = `openrouter`, root span `LLM Generation`, children `provider attempt N: <provider>`). Maple reads **`session.id`** as the session key. OpenRouter sets `session.id` from the request's `session_id` body field or `x-session-id` header, and uses `trace.trace_id` / `trace.parent_span_id` from the body verbatim as the OTLP trace id / parent span id.

You cannot change the OpenRouter dashboard. Your job: (1) edit the app's OpenRouter calls, (2) hand the user the exact destination settings.

What Broadcast cannot give (tell the user, don't try to fix it here): tool spans, tool failures, agent names / sub-agent lanes. Those need the app's framework instrumentation (router skill `maple-agent-tracing`).

Step 0: Detect

1. Find every OpenRouter call site: `openrouter.ai` base URLs (`https://openrouter.ai/api/v1`, `https://eu.openrouter.ai/api/v1`), `OPENROUTER_API_KEY`, `@openrouter/ai-sdk-provider`, `@openrouter/sdk`, the `openrouter` PyPI package, `openai` clients with an OpenRouter `baseURL` / `base_url`, LiteLLM `openrouter/` models, framework model classes pointed at OpenRouter. 2. Find where the conversation id lives per request: chat thread id, conversation id, session id, agent run id. It must be per conversation, never a process constant or a per-client default. 3. Existing OTel exporting to Maple? Search for `TracerProvider`, `NodeSDK`, `registerOTel`, `registerTelemetry`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `ingest.maple.dev`, `logfire.configure`, OpenInference / OpenLLMetry instrumentors.

  • Yes → do Step 2 and Step 3 (nesting).
  • No → Step 2 only (Broadcast-only). Mention that the app's framework skill adds tool spans and agent structure.

4. Which endpoint region the app calls (`openrouter.ai` vs `eu.openrouter.ai`), for the destination's data regions.

Step 1: Key and region (for the destination headers)

  • US: `https://ingest.maple.dev`. EU: `https://ingest.eu.maple.dev`.
  • Header: `Authorization=Bearer <key>`.
  • Key given in the prompt → use it.
  • No key → use the literal `MAPLE_TEST` (ingest accepts and discards it) and tell the user to replace it with their key from Settings → Ingestion. Test Connection passes with it, but nothing lands.
  • Never put a private `maple_sk_` key in browser code. The key here goes into OpenRouter's dashboard, not the repo.
  • If the app also exports its own OTel to Maple, follow the repo's existing secret/env convention for that exporter. Ingest keys are write-only, so inline is acceptable if there is none.

Step 2: Send `session_id` on every OpenRouter request

Same id for every request of one conversation, new id per conversation, max 256 characters. If the app also has framework instrumentation, use the SAME value as the framework's conversation/session id (a trace with two different ids is assigned to the lexically larger one, silently).

`openai` npm (>= 7; field is untyped, sent as-is):

const completion = await client.chat.completions.create({
	model,
	messages,
	// @ts-expect-error OpenRouter-only field
	session_id: conversationId,
})

Or per-request header, no type workaround:

await client.chat.completions.create({ model, messages }, { headers: { "x-session-id": conversationId } })

`openai` PyPI (>= 3): `extra_body={"session_id": conversation_id}` on `chat.completions.create(...)` (and on `responses.create(...)` if used).

Vercel AI SDK + `@openrouter/ai-sdk-provider` (>= 3.1): `providerOptions: { openrouter: { session_id: conversationId } }` on `generateText` / `streamText` / agent calls. Everything under `providerOptions.openrouter` is merged into the body.

`@openrouter/sdk`: `openRouter.chat.send({ model, messages, sessionId: conversationId })`. `openrouter` PyPI: `session_id=conversation_id`.

Other clients: find the framework's extra-body or per-request-headers option and set `session_id` / `x-session-id`. Check the outgoing request (log the body once, or a test that captures `fetch`) to confirm the field is actually sent; some wrappers drop unknown fields.

Optional: `user` (<= 128 chars) is forwarded as `user.id`. Maple does not use it for sessions. Never put emails/names in `user`, `session_id` or `trace` metadata: Privacy Mode does not strip them.

Step 3: Nest Broadcast under the app's own traces (only if the app exports OTel to Maple)

Without this, every model call is recorded twice (app span + Broadcast trace) and Broadcast-only turns split one per model call. Put the active span's W3C ids in `trace.trace_id` (32 hex) and `trace.parent_span_id` (16 hex).

TypeScript: wrap `fetch` and pass it to the client (`new OpenAI({ baseURL, apiKey, fetch: openRouterFetch })` or `createOpenRouter({ apiKey, fetch: openRouterFetch })`). Reads the span active when the SDK sends the request, which is the framework's model-call span if it has one:

import { trace } from "@opentelemetry/api"

// Nests each OpenRouter Broadcast trace under the span that made the request.
export const openRouterFetch: typeof fetch = (input, init) => {
	const span = trace.getActiveSpan()?.spa
Read more
Ships withmaple

OpenTelemetry observability platform

Get the whole plugin

Other skills on maple.