> ## 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.

# OpenAI API

> Record prompts, models, tokens, and cost for OpenAI chat completion, embeddings, and transcription calls

Bitfab records chat completion, embeddings, and transcription requests your code sends through the official `openai` client: the prompt, the model, token counts, and latency. Bitfab works out the cost from the model and the token counts, or keeps the cost the response reports when it carries one. For the Responses API, see [OpenAI Responses API](https://docs.bitfab.ai/frameworks/openai-responses.md).

**Canonical signatures:** TypeScript [`wrapOpenAIChat`](https://docs.bitfab.ai/reference/typescript#wrapopenaichat), [`wrapOpenAIEmbeddings`](https://docs.bitfab.ai/reference/typescript#wrapopenaiembeddings), [`wrapOpenAITranscriptions`](https://docs.bitfab.ai/reference/typescript#wrapopenaitranscriptions) · Python [`wrap_openai_chat`](https://docs.bitfab.ai/reference/python#wrap-openai-chat), [`wrap_openai_embeddings`](https://docs.bitfab.ai/reference/python#wrap-openai-embeddings), [`wrap_openai_transcriptions`](https://docs.bitfab.ai/reference/python#wrap-openai-transcriptions)

## Language Support

| Language | API | Status |
| - | - | - |
| TypeScript | `wrapOpenAIChat()`, `wrapOpenAIEmbeddings()`, `wrapOpenAITranscriptions()`, plus automatic capture under `withTrace()` with the build transform | ✅ Supported |
| Python | `wrap_openai_chat()`, `wrap_openai_embeddings()`, `wrap_openai_transcriptions()`, plus automatic capture under `trace()` | ✅ Supported |

## Under a trace root

Beneath a trace root, these calls on an `openai` client are recorded with no wrapper, each as its own `llm` span named after the client class and method (Python's async client adds `Async`, such as `AsyncCompletions.create`). The span attaches under the function that made the call.

| Call | Span |
| - | - |
| `chat.completions.create` | `Completions.create` |
| `chat.completions.parse` | `Completions.parse` |
| `chat.completions.stream()` | `Completions.create` |
| `embeddings.create` | `Embeddings.create` |
| `audio.transcriptions.create` | `Transcriptions.create` |

```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()

async function classify(email: string) {
  const completion = await llm.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "You classify email." },
      { role: "user", content: email },
    ],
  })
  return completion.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()


def classify(email: str) -> str:
    completion = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You classify email."},
            {"role": "user", "content": email},
        ],
    )
    return completion.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, tokens, and cost.

In TypeScript, the [build transform](https://docs.bitfab.ai/typescript-sdk.md#experimental-subtree-tracing) rewrites each of these call sites in your code, so it works in bundled apps and the SDK never loads or patches `openai`. The client's own return value is handed back, so `.withResponse()` keeps working. In Python, the SDK patches the `openai` resource classes when a `trace()` root is set up.

A client whose base URL is `openrouter.ai` is recorded the same way with `provider` `"openrouter"` and the cost OpenRouter reports; see [OpenRouter](https://docs.bitfab.ai/frameworks/openrouter.md).

`exclude` on the root matches the method name (`"create"`, `"parse"`) or the span name, and `withNode()` / `node()` on a wrapper (below) configures the one span.

## Streaming

A streamed call (`stream: true` / `stream=True`, or `chat.completions.stream()`) is recorded as one `llm` span when the stream finishes, is closed, or fails, with usage read from the last chunk. What your code reads from the stream is unchanged. OpenAI sends usage on a chat stream only when the request asks for it:

```typescript TypeScript theme={null}
const stream = await llm.chat.completions.create({
  model: "gpt-4o-mini",
  messages,
  stream: true,
  stream_options: { include_usage: true },
})
```

```python Python theme={null}
stream = llm.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    stream=True,
    stream_options={"include_usage": True},
)
```

Without it, the span carries the model and duration but no token counts. A streamed transcription is recorded the same way, from its `transcript.text.done` event. A stream returned from the trace root keeps the trace open until your code reads it. A stream your code drops without reading to the end is recorded when the runtime frees it, with the request and whatever was read but usually no usage, and its trace finishes then. Streamed calls always run for real during a replay.

A `withNode()` / `node()` placed directly on the client method records the stream on its own span instead of opening a second one. That span ends when the stream does, and the node's `finalize` is not used.

## Wrapping a call

Use a wrapper when you trace with `withSpan()` / `span()` instead of a trace root. It wraps the function that sends the request and writes the prompt and usage into the span that is current when the call starts. It opens no span of its own. Beneath a trace root, a wrapper around a call that is already recorded records nothing more. `onResponse` / `on_response` receives each response after it is recorded. Wrappers never record a stream.

### Chat completions

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

const classify = bitfab.withSpan(
  "classify-email",
  { type: "llm" },
  async (email: string) => {
    const completion = await complete({
      model: "gpt-4o-mini",
      messages: [{ role: "user", content: email }],
    })
    return completion.choices[0].message.content
  },
)
```

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


@bitfab.span("classify-email", type="llm")
def classify(email: str) -> str:
    completion = complete(
        model="gpt-4o-mini", messages=[{"role": "user", "content": email}]
    )
    return completion.choices[0].message.content
```

`chat.completions.parse` can be wrapped the same way.

### Embeddings

```typescript TypeScript theme={null}
const embed = bitfab.wrapOpenAIEmbeddings(llm.embeddings.create.bind(llm.embeddings))

const index = bitfab.withSpan("index-email", async (email: string) => {
  const response = await embed({ model: "text-embedding-3-small", input: email })
  return response.data[0].embedding
})
```

```python Python theme={null}
embed = bitfab.wrap_openai_embeddings(llm.embeddings.create)


@bitfab.span("index-email")
def index(email: str) -> list[float]:
    return embed(model="text-embedding-3-small", input=email).data[0].embedding
```

### Transcriptions

```typescript TypeScript theme={null}
const transcribe = bitfab.wrapOpenAITranscriptions(
  llm.audio.transcriptions.create.bind(llm.audio.transcriptions),
)

const readVoicemail = bitfab.withSpan("read-voicemail", async (audio: File) => {
  const transcription = await transcribe({ model: "gpt-4o-transcribe", file: audio })
  return transcription.text
})
```

```python Python theme={null}
transcribe = bitfab.wrap_openai_transcriptions(llm.audio.transcriptions.create)


@bitfab.span("read-voicemail")
def read_voicemail(audio: bytes) -> str:
    return transcribe(model="gpt-4o-transcribe", file=("voicemail.wav", audio)).text
```

## No duplicate spans

Each model call is recorded once:

* A wrapper around a call the trace root already recorded adds nothing.
* A call the [OpenAI Agents SDK integration](https://docs.bitfab.ai/frameworks/openai-agents.md) is recording as a generation or response span, or the [LangChain callback handler](https://docs.bitfab.ai/frameworks/langgraph.md) is recording as an LLM run, is not recorded a second time. This includes a call made inside your own LangChain chat model class, streamed or not. A call made alongside a LangChain run in the same trace is still recorded, except beside the first LangChain run in a TypeScript process (see [Limitations](#limitations)).
* In TypeScript, calls made inside framework libraries in `node_modules` are never rewritten by the build transform, so only the framework's own integration records them. In Python, the SDK patches the client classes, so a call another library makes through the client is recorded too, unless that library has a Bitfab integration recording it.

## What gets captured

| Field | Chat completions | Embeddings | Transcriptions |
| - | - | - | - |
| Prompt | The request's `messages`, as a JSON list of `role` and `content` | The request's `input`, as `user` turns | The request's `prompt`, when given, as a `user` turn |
| `provider` | `"openai"` (`"openrouter"` for an OpenRouter base URL under a trace root or with Python's wrapper) | `"openai"` (`"openrouter"` for an OpenRouter base URL under a trace root or with Python's wrapper) | `"openai"` |
| `model` | The model from the response, else the request's | The model from the response, else the request's | The model from the response, else the request's |
| `upstreamProvider` | The provider OpenRouter used, for an OpenRouter call | The provider OpenRouter used, for an OpenRouter call | |
| `inputTokens` | `usage.prompt_tokens` | `usage.prompt_tokens` | `usage.input_tokens`, for models billed by token |
| `outputTokens` | `usage.completion_tokens` | `0` | `usage.output_tokens`, for models billed by token |
| `cachedInputTokens` | `usage.prompt_tokens_details.cached_tokens` | | |
| `reasoningTokens` | `usage.completion_tokens_details.reasoning_tokens` | | |
| `audioSeconds` | | | `usage.seconds`, for models billed by duration |
| `costUsd` | `usage.cost`, only when the response reports one | `usage.cost`, only when the response reports one | |
| `durationMs` | How long the request took | How long the request took | 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. Without `costUsd`, Bitfab works out the cost from `model` and the token counts.

## Limitations

* **Transcription models billed by the minute, such as `whisper-1`, have no cost.** Their spans carry `audioSeconds`, and Bitfab prices tokens only.
* **`wrapOpenAIChat` and `wrapOpenAIEmbeddings` in TypeScript always report `provider` `"openai"`.** The wrapper cannot see the client's base URL. Under a trace root, an OpenRouter client is recorded with `"openrouter"`; for a wrapped chat completion on OpenRouter, use [`wrapOpenRouter`](https://docs.bitfab.ai/frameworks/openrouter).
* **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.
* **TypeScript: calls beside the first LangChain run in a process.** The SDK hooks into LangChain in the background when the first Bitfab LangChain handler or `withTrace()` root is created. If a LangChain run starts before that finishes, calls made inside it are still recorded once, but a call made at the same time outside it (for example in `Promise.all`) is not recorded while that first run is open. Later runs are not affected.
* **Python automatic capture needs Python 3.12+**, the same as `trace()` subtree capture.
* **Other endpoints are not recorded**, including image generation, speech, moderation, and the Realtime API.


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