Skip to content

OpenRouter

OpenRouter speaks the OpenAI protocol, so you reach it with an ordinary OpenAI client pointed at https://openrouter.ai/api/v1. Metergraph recognizes that host, so the integration is still one wrap() call.

app/llm.py
import os
import metergraph
from openai import OpenAI
client = metergraph.wrap(OpenAI(
api_key=os.environ["OPENROUTER_API_KEY"],
base_url="https://openrouter.ai/api/v1",
))

Detection is an exact hostname match on a parsed HTTPS URL. The host has to be openrouter.ai. The path does not matter, a plain HTTP URL is never detected, and a lookalike host is not detected.

If you front OpenRouter with your own domain, the host will not match. Declare the gateway:

client = metergraph.wrap(openai_client, gateway="openrouter")

openrouter is currently the only supported gateway name. Any other value raises before a single provider call is made, and the error never echoes the value you passed, because a wrong value may itself be a secret.

The override requires an OpenAI-compatible client. Combining it with provider="anthropic" or provider="google" is rejected, and so is passing it a client that does not look OpenAI-compatible. A consistent provider="openai" alongside it is fine.

The call is still captured as an OpenAI call. The gateway does not change the provider on the row. It adds evidence:

FieldWhere it comes from
gatewayopenrouter
served_modelThe model field of the response
reported_cost_usdusage.cost
reported_cost_sourceopenrouter.usage.cost
reported_upstream_cost_usdusage.cost_details.upstream_inference_cost
reported_upstream_cost_sourceopenrouter.usage.cost_details.upstream_inference_cost

served_model matters because it can differ from what you asked for. After routing or a fallback, a request for one model may be served by another, and the row keeps both: your requested model and the served_model OpenRouter actually used.

Cost is evidence, not a price. The SDK computes nothing and fetches no catalog. It transports a small allowlist of scalars from a response it was already observing, and a provenance string saying exactly which field each number came from. The server keeps that alongside its own catalog price rather than overwriting either with the other. See Model catalog and pricing.

Malformed values are omitted rather than invented. A cost that is not a finite, non-negative number is dropped, and a provenance string never appears without the value it describes. Booleans are rejected. Zero is kept.

Gateway identity (gateway and served_model) is recorded for any detected gateway call. Cost evidence is recorded only on chat.completions, which is the endpoint whose response shape is qualified for it. A Responses API call through OpenRouter still gets identity, and no reported_cost_usd.

Streaming works through the same wrap(), with one difference worth knowing: OpenRouter sends its final usage event on its own, so Metergraph does not inject stream_options into your request and does not hide any chunk. Your for await or for loop still sees the usage event exactly where OpenRouter put it. Capture reads the cost from it once.

That is the opposite of the direct OpenAI path, where Metergraph does inject and does suppress. See the usage chunk.