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.
import osimport metergraphfrom openai import OpenAI
client = metergraph.wrap(OpenAI( api_key=os.environ["OPENROUTER_API_KEY"], base_url="https://openrouter.ai/api/v1",))import * as mg from "metergraph";import OpenAI from "openai";
const client = mg.wrap(new OpenAI({ apiKey: process.env.OPENROUTER_API_KEY, baseURL: "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.
A custom domain or reverse proxy
Section titled “A custom domain or reverse proxy”If you front OpenRouter with your own domain, the host will not match. Declare the gateway:
client = metergraph.wrap(openai_client, gateway="openrouter")const client = mg.wrap(openaiClient, { 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.
What the gateway adds to a row
Section titled “What the gateway adds to a row”The call is still captured as an OpenAI call. The gateway does not change the provider on the row. It adds evidence:
| Field | Where it comes from |
|---|---|
gateway | openrouter |
served_model | The model field of the response |
reported_cost_usd | usage.cost |
reported_cost_source | openrouter.usage.cost |
reported_upstream_cost_usd | usage.cost_details.upstream_inference_cost |
reported_upstream_cost_source | openrouter.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.
Only chat completions carry cost
Section titled “Only chat completions carry cost”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
Section titled “Streaming”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.
- OpenAI for the seam table, tools and batches, which are unchanged here
- Vercel AI Gateway for the other supported gateway
- Runnable examples: Python, Node