OpenRouter

Trace OpenRouter in Python and TypeScript.

OpenRouter gives you 200+ models through one API. The Python SDK (openrouter) and the TypeScript agent SDK (@openrouter/agent) are both traced with provider="openrouter". In Python, system is set to the underlying model vendor (openai / anthropic / google / …) inferred from the vendor/model slug; in TypeScript, system is openrouter.

Prerequisites

pip install -U neatlogs openrouter

Usage

import os
import neatlogs

neatlogs.init(api_key=os.environ["NEATLOGS_API_KEY"], workflow_name="openrouter-demo")

from openrouter import OpenRouter

client = neatlogs.wrap(OpenRouter(api_key=os.environ["OPENROUTER_API_KEY"]))

resp = client.chat.send(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "In one sentence, what is OpenRouter?"}],
    temperature=0.3,
    top_p=0.9,
    max_tokens=256,
)
print(resp.choices[0].message.content)

neatlogs.flush()
neatlogs.shutdown()

wrap() opens a WORKFLOW root for you, so this renders with no extra wrapper. The Python SDK traces Chat Completions (chat.send), the Responses API (beta.responses.send), embeddings, and rerank. The TypeScript @openrouter/agent LLM span is finalized when you consume the result (getText() / getResponse()). Sampling params are recorded as model settings, and streaming is supported.

Making several calls in one run? Group them into one trace with a WORKFLOW root.

Sessions & end-user

Say you're running a chatbot on OpenRouter and want every back-and-forth turn stitched into one conversation and tied to the person on the other end — Neatlogs handles that now. Just wrap each call in identify(); since wrap() already opens the trace root, the OpenRouter call inside picks up the session and end-user automatically:

# Same session_id every turn → one session; end_user_id attributes it to your user.
with neatlogs.identify(session_id=f"conv_{conversation_id}", end_user_id=user_id):
    resp = client.chat.send(
        model="openai/gpt-4o-mini",
        messages=[{"role": "user", "content": message}],
    )

The trick is simply reusing the same session_id for the whole conversation — that's what keeps the turns together.

Not every run is a conversation. A standalone workflow — a one-off job, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:

Warning

A standalone workflow must not carry a session_id. Omit it entirely — Neatlogs sets session_id = trace_id, so the run is its own single-turn session, fully attributed to the end-user. Reusing one session_id across unrelated runs wrongly folds them into a single multi-turn conversation.

def summarize(document, user):
    # Standalone workflow — attribute to the end-user; do NOT pass session_id.
    with neatlogs.identify(
        end_user_id=str(user.id),
        end_user_metadata={"plan": user.plan},
    ):
        resp = client.chat.send(
            model="openai/gpt-4o-mini",
            messages=[{"role": "user", "content": f"Summarize:\n{document}"}],
        )
        return resp

Dig into Sessions and End-User Identity when you want the full picture.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.