> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitfab.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenRouter

> Record prompts, models, tokens, and cost for direct OpenRouter calls

Bitfab records chat completions your code sends to [OpenRouter](https://openrouter.ai) through the official `openai` client: the prompt, the model OpenRouter routed to, token counts, latency, and the cost OpenRouter reports for that call.

**Canonical signatures:** [TypeScript `wrapOpenRouter`](/reference/typescript#wrapopenrouter) · [Python `wrap_openrouter`](/reference/python#wrap-openrouter)

## Language Support

| Language | API | Status |
| - | - | - |
| TypeScript | `wrapOpenRouter()`, plus automatic capture under `withTrace()` with the build transform | ✅ Supported |
| Python | `wrap_openrouter()`, plus automatic capture under `trace()` | ✅ Supported |

## Under a trace root

Beneath a trace root, every non-streaming `chat.completions.create` call on an `openai` client whose base URL is `openrouter.ai` is recorded with no wrapper, as its own `llm` span named `Completions.create` (Python's async client: `AsyncCompletions.create`). The span attaches under the function that made the call.

```typescript TypeScript theme={null}
import OpenAI from "openai"
import { Bitfab } from "@bitfab/sdk"

const bitfab = new Bitfab({ apiKey: process.env.BITFAB_API_KEY })
const llm = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
})

async function classify(email: string) {
  const response = await llm.chat.completions.create({
    model: "anthropic/claude-sonnet-4.5",
    messages: [{ role: "user", content: email }],
  })
  return response.choices[0]?.message.content
}

export const detect = bitfab.withTrace("inbox-detection", (email: string) =>
  classify(email),
)
```

```python Python theme={null}
from openai import OpenAI
from bitfab import Bitfab

bitfab = Bitfab()
llm = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=OPENROUTER_API_KEY)


def classify(email: str) -> str:
    response = llm.chat.completions.create(
        model="anthropic/claude-sonnet-4.5",
        messages=[{"role": "user", "content": email}],
    )
    return response.choices[0].message.content


@bitfab.trace("inbox-detection", type="llm")
def detect(email: str) -> str:
    return classify(email)
```

The trace shows `detect`, then `classify`, then an `llm` span named `Completions.create` carrying the prompt and cost.

In TypeScript, the [build transform](/typescript-sdk#experimental-subtree-tracing) rewrites each `.chat.completions.create(...)` call site in your code, so it works in bundled apps and the SDK never loads or patches `openai`. The client's own `APIPromise` is handed back, so `.withResponse()` keeps working. In Python, the SDK patches the `openai` completions classes when a `trace()` root is set up.

`exclude: ["create"]` on the root skips these calls, and `withNode()` / `node()` on a wrapper (below) configures the one span.

## Wrapping a call

Use `wrapOpenRouter()` / `wrap_openrouter()` when you trace with `withSpan()` / `span()` instead of a trace root, or when your client reaches OpenRouter through a proxy whose host is not `openrouter.ai`. It wraps the function that sends the request and writes the prompt, usage, and cost into the span that is current when the call starts. It opens no span of its own.

```typescript TypeScript theme={null}
const complete = bitfab.wrapOpenRouter(
  llm.chat.completions.create.bind(llm.chat.completions),
)

const classify = bitfab.withSpan(
  "classify-email",
  { type: "llm" },
  async (email: string) => {
    const response = await complete({
      model: "anthropic/claude-sonnet-4.5",
      messages: [{ role: "user", content: email }],
    })
    return response.choices[0]?.message.content
  },
)
```

```python Python theme={null}
complete = bitfab.wrap_openrouter(llm.chat.completions.create)


@bitfab.span("classify-email", type="llm")
def classify(email: str) -> str:
    response = complete(
        model="anthropic/claude-sonnet-4.5",
        messages=[{"role": "user", "content": email}],
    )
    return response.choices[0].message.content
```

Beneath a trace root, a wrapper around a call that is already recorded as `Completions.create` records nothing more. `onResponse` / `on_response` receives each completion after it is recorded.

## No duplicate spans

Each model call is recorded once:

* A wrapper around a call the trace root already recorded adds nothing.
* In TypeScript, calls made inside framework libraries (LangChain, the OpenAI Agents SDK) live in `node_modules`, which the build transform never rewrites, so only the framework's own integration records them.
* In Python, a call the [OpenAI Agents SDK integration](/frameworks/openai-agents) is recording as a generation or response span, or the [LangChain callback handler](/frameworks/langgraph) is recording as an LLM run, is not recorded a second time.

## What gets captured

| Field | Value |
| - | - |
| Prompt | The request's messages, as a JSON list of `role` and `content` |
| `provider` | `"openrouter"` |
| `model` | The model OpenRouter routed to, from the response |
| `upstreamProvider` | The provider OpenRouter used, such as `"Anthropic"` |
| `inputTokens` | `usage.prompt_tokens` |
| `outputTokens` | `usage.completion_tokens` |
| `cachedInputTokens` | `usage.prompt_tokens_details.cached_tokens` |
| `reasoningTokens` | `usage.completion_tokens_details.reasoning_tokens` |
| `costUsd` | `usage.cost`, the cost in US dollars OpenRouter charged for the call |
| `durationMs` | How long the request took |

The context fields go in one context entry on the span. A key the response does not carry is left out.

## Limitations

* **Streaming calls are not recorded.** A call with `stream: true` / `stream=True` runs normally and records nothing.
* **Calls outside any trace are not recorded.** A wrapper used outside a span or trace records nothing and never starts a trace of its own.
* **TypeScript automatic capture needs the build transform.** Code the transform does not compile (for example a call made from a dependency) is not rewritten; wrap it instead.
* **Python automatic capture needs Python 3.12+**, the same as `trace()` subtree capture.
* **Only chat completions.** The Responses API and structured-output `parse` helpers are not recorded.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.