19 integrations

Whatever writes it, one table reads it.

Four shapes reach the same log: wrap a client, hand us a finish handler, point an OpenTelemetry exporter here, or drive it yourself. Every card below shows the row that arrives — the action names are the ones the code actually writes, not an illustration.

Wrap the client

7 of them.

One line at construction. Every call through the client is timed, priced and written — including the ones that throw, because a failed call still costs latency and often tokens.

  • OpenAI

    Chat, responses and embeddings. Reads both token namings.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const openai = nitroping.observeOpenAI(new OpenAI())

    Writes

    openai.chat$0.00421204ms
  • Anthropic

    Cache writes count as input; cache reads bill at the cache rate.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const anthropic = nitroping.observeAnthropic(new Anthropic())

    Writes

    anthropic.chat6.7k cached2870ms
  • Google Gemini

    The cached share is subtracted so a hit isn't billed twice.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const genai = nitroping.observeGoogleGenAI(new GoogleGenAI({ apiKey }))

    Writes

    google.chat$0.01181980ms
  • Amazon Bedrock

    Converse and InvokeAgent. InvokeModel passes through untraced.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const bedrock = nitroping.observeBedrock(new BedrockRuntimeClient({ region }))

    Writes

    bedrock.chat2140ms
  • OpenRouter

    Uses OpenRouter's own reported cost over an estimate.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const client = nitroping.observeOpenRouter(new OpenAI({ baseURL, apiKey }))

    Writes

    openrouter.chat$0.0006890ms
  • LiteLLM

    The proxy speaks OpenAI's protocol; only the label differs.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const litellm = nitroping.observeLiteLLM(new OpenAI({ baseURL, apiKey }))

    Writes

    litellm.chat740ms
  • Azure OpenAI

    Same wire shape as OpenAI; the label keeps the spend separable.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const azure = nitroping.observeOpenAI(new AzureOpenAI({ ... }), "azure")

    Writes

    azure.chat1310ms

Finish handler

4 of them.

The one path that covers streaming: the usage totals only exist once the stream has ended, so the handler runs there. A tool loop is counted as one call per step.

  • Vercel AI SDK

    Counts every step of a tool loop. More than one step is an agent run.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    await streamText({ model, prompt, onFinish: nitroping.onFinish() })

    Writes

    vercel-ai.chat1 step3050ms
    vercel-ai.agent4 steps8410ms
  • TanStack AI

    Compatible finish event — the same handler.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    onFinish: nitroping.onFinish()

    Writes

    vercel-ai.chat620ms
  • Eve

    Compatible finish event — the same handler.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    onFinish: nitroping.onFinish()

    Writes

    vercel-ai.chat580ms
  • Mastra

    Runs on the AI SDK underneath, so usage arrives the same way.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    onFinish: nitroping.onFinish({ provider: "mastra" })

    Writes

    mastra.chat2410ms

OpenTelemetry

7 of them.

No SDK at all: two environment variables, sometimes a third, and the spans and metrics become audit events on arrival. The action on the row is whatever the exporter named the span.

.env — folded into every card below
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

The key is an ingest key from Settings → API keys — the same one the SDK uses. The endpoint ends at /v1/otlp on purpose: an exporter appends /v1/traces and /v1/metrics itself, so the doubled path on the wire is expected rather than a typo. Logs are accepted and discarded, so a tool exporting all three won't error. This line is shown once for reference — the card for your tool below already has it, so there is nothing to combine by hand.

Never sent anywhere — this page has no backend. It only replaces $NITROPING_KEY in what the copy buttons below put on your clipboard, so the block you paste into your shell is already complete.

If a card below doesn't already have this

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

This endpoint speaks OTLP over HTTP — http/protobuf or http/json, both read the same. It does not speak gRPC at all. Add this line yourself only if your tool's card doesn't include it and nothing arrives after setup — that means its exporter picked gRPC, or has no default and picked nothing.

  • Claude Code

    Reports tokens and cost as metrics, not spans. Both are handled.

    Its docs state it has no default protocol, so it stays inert until one is named — the line below is already in the snippet. Traces are off until you ask for them separately.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"
    
    OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
    
    export CLAUDE_CODE_ENABLE_TELEMETRY=1
    export OTEL_METRICS_EXPORTER=otlp
    export OTEL_LOGS_EXPORTER=otlp
    
    # optional: per-tool spans as well as the token metrics
    export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
    export OTEL_TRACES_EXPORTER=otlp

    Writes

    claude_code.token.usage5.0k input
    claude_code.token.usage900 cached
  • oh-my-pi

    Nothing beyond the shared block: export starts as soon as an endpoint is set.

    Does not need the protocol line: its runtime enables http/protobuf and nothing else, so it is already right. Naming any other protocol disables that signal rather than switching it. OTEL_SDK_DISABLED=true turns the whole thing off.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

    Writes

    agent turnroot span4000ms
    chat claude-opus-54.2k / 8103000ms
  • OpenClaw

    Standard GenAI semantic-convention spans.

    If nothing arrives, name the protocol: an exporter that defaults to gRPC has nowhere to send it here.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

    Writes

    chat gpt-51620ms
  • Hermes Agent

    Standard GenAI semantic-convention spans.

    If nothing arrives, name the protocol: an exporter that defaults to gRPC has nowhere to send it here.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

    Writes

    chat claude-sonnet-52260ms
  • Pydantic AI

    Built-in OTel support — just point it at the endpoint.

    Python's OTLP exporter is commonly wired for gRPC, so this is the one most likely to need the protocol line.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

    Writes

    agent run5120ms
  • LangChain / LangGraph

    Via OpenInference or OpenLIT. Graph parent links are kept.

    Same Python exporter caveat: name the protocol if the traces never show up.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"
    
    pip install openinference-instrumentation-langchain

    Writes

    ChatOpenAInested1440ms
  • CrewAI

    Via OpenLIT, or use the manual agent API for finer control.

    Same Python exporter caveat: name the protocol if the traces never show up.

    ready to paste — .env above included
    OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
    OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"
    
    pip install openlit

    Writes

    crew.kickoff12800ms

Manual

1 of them.

For a loop you wrote yourself. The run opens a trace, every tool call nests under it, and closing the run records its total duration — so the waterfall shows where the time went rather than one long span.

  • Custom agents

    Any hand-rolled loop. Tool calls nest under the run as one trace.

    setup
    import { NitroPing } from "nitroping"
    
    const nitroping = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    const run = nitroping.startAgentRun({ name: "researcher" })
    await run.tool("web_search", () => search(query))
    run.end()

    Writes

    agent.tool.web_searchchild820ms
    agent.agent.researcherroot6340ms

Nothing listed

Then send the event yourself.

The list above is convenience, not capability. Anything that can POST JSON writes to the log, and the panel treats a hand-written event exactly like a wrapped one.

POST /v1/events
await fetch("https://ingest.nitroping.dev/v1/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NITROPING_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify([{ actor_id: "u_8f31c2", action: "invoice.void" }]),
})

The full field list is in the API reference and in llms.txt, which is written for an agent to read on your behalf.

Missing one

Ask for it in the open.

The wrappers, the finish handlers and the OTel conventions all live in one repository. That is where an integration that is missing, a snippet on this page that no longer works, or a provider whose token naming we read wrong should be raised — in public, against the code, rather than in a support thread nobody else can read.

Pull requests are the fastest route for a new provider: a wrapper is a usage reader and a price table, and the existing ones are the template.