OpenAI

Chat completions and embeddings across sync, async, and streaming. Why the SDK injects include_usage, and the limitations to know.

Fully covered for chat completions and embeddings, and verified against the live API on every release, including across the openai 1.x to 2.x major version change.

pip install openai capsera
import openai
import capsera

capsera.init(api_key=os.environ["CAPSERA_API_KEY"])
client = openai.OpenAI()

Coverage

CallRecorded
client.chat.completions.create(...)Yes
await async_client.chat.completions.create(...)Yes
create(..., stream=True), sync and asyncYes
client.embeddings.create(...), sync and asyncYes
provider errors on any of the aboveYes
client.responses.create(...)No
legacy client.completions.create(...)No

For the Responses API, use record().

Streaming and include_usage

A streamed response carries no usage data unless requested, so the SDK injects stream_options={"include_usage": True} into the request. Without it there would be nothing to record, and estimating tokens from the text would put a guess in a cost column.

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Summarise this ticket."}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")

Your own stream_options are preserved. The injection adds include_usage alongside them rather than replacing them.

The extra final chunk carrying usage has empty choices, so guard a loop that indexes choices[0]:

for chunk in stream:
    if chunk.choices:                     # the usage chunk has none
        print(chunk.choices[0].delta.content or "", end="")

Two streaming limitations. A stream abandoned before the end has no usage chunk and is not recorded. Streamed calls record cache tokens as zero, while the non-streaming path reports them correctly.

Embeddings

Recorded and costed, sync and async:

client.embeddings.create(model="text-embedding-3-small", input=chunks)

Embeddings bypass routing, budget enforcement, and prompt analysis. A blocking budget will not stop one and a routing rule will not redirect one. This matters most for indexing jobs, where embeddings are often the dominant cost.

Azure OpenAI

Recorded through the same patch, and labelled separately. The SDK reads the client's base_url and sets the provider to azure rather than openai, so Azure spend does not merge into direct OpenAI spend.

client = openai.AzureOpenAI(azure_endpoint=..., api_key=..., api_version=...)

Other OpenAI-compatible vendors

Groq, DeepSeek, xAI, Together, Fireworks, Perplexity, OpenRouter, Ollama, and vLLM all use this client and are all recorded, each labelled by its base URL. See OpenAI-compatible providers.

Errors

Recorded as zero-cost events carrying the exception type, such as RateLimitError, BadRequestError, or APIStatusError, then re-raised unchanged. Messages are not recorded, because they often quote the prompt.