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

# LLM auto-capture

> Record the token usage of every LLM call made through an *http.Client, from any provider.

`nozle.WrapHTTPClient` returns a copy of an `*http.Client` that records the token usage of every successful LLM response it carries. Pass it to any provider SDK that accepts an HTTP client, or use it for your own requests. Requests and responses pass through unchanged, and prompts and completions are never sent to Nozle.

`WrapHTTPClient` and `RecordUsage` require `v0.12.0`. The [provider-specific wrappers](#provider-specific-wrappers) are available in earlier versions.

Usage is recognised by the shape of the response, not by provider name: an OpenAI-style `usage` (OpenAI chat completions, the Responses API and any OpenAI-compatible API), an Anthropic `usage`, or Gemini `usageMetadata`, for JSON responses and streams.

> [!NOTE]
>
> Capture records usage events; they bill only when a metered Feature has the same event code. See [Set up AI token billing](/guides/ai/token-billing) for the Feature, charge and cost model to create first.

## Capture any client

```go theme={null}
import nozle "github.com/nozle-dev/nozle-go"

providerHTTP, err := nozle.WrapHTTPClient(&http.Client{}, client, nozle.CaptureOptions{
  FeatureCode: "llm_tokens",
  OnError:     func(err error) { log.Printf("Nozle usage: %v", err) },
})
if err != nil { return err }
openaiClient := openai.NewClient(openaiopt.WithHTTPClient(providerHTTP))

// The customer travels with the request context, so one client serves every customer.
ctx = nozle.WithCustomer(ctx, workspace.ID)
response, err := openaiClient.Chat.Completions.New(ctx, params)
```

The customer for a request is, in order: the `X-Nozle-Customer-Id` request header (removed before the request is sent), `WithCustomer` on the request context, `CaptureOptions.CustomerIDFunc`, then `CaptureOptions.CustomerID`. A request without a customer passes through and nothing is recorded. `X-Nozle-Feature` (also removed) or `Feature` sets the `feature` property; `CostMeterCode` adds categorized Cost Events. Chat completion streams are sent with `stream_options.include_usage`; set `SkipStreamUsage` for a server that rejects the field. `OnError` also receives, once per event code, a `*nozle.SetupError` with `Code` `feature_not_found` when no Feature has the event code.

`client.RecordUsage(ctx, nozle.ProviderCapture{CustomerID: ..., Model: ..., RequestID: ...}, nozle.Usage{InputTokens: ..., OutputTokens: ...})` records usage your code read itself.

## Provider-specific wrappers

`WrapOpenAIHTTPClient` and `WrapAnthropicHTTPClient` observe only `/chat/completions` and `/messages`. They are kept for existing integrations.

### OpenAI

```bash theme={null}
go get github.com/openai/openai-go/v3@v3.44.0
```

```go theme={null}
providerHTTP, err := nozle.WrapOpenAIHTTPClient(&http.Client{}, client, nozle.ProviderWrapOptions{
  CustomerID: "workspace_123", SubscriptionID: "workspace_123_subscription",
  FeatureCode: "llm_tokens", CostMeterCode: "ai_tokens", Feature: "assistant_reply",
  OnError: func(err error) { log.Printf("Nozle usage tracking: %v", err) },
})
if err != nil { return err }
openaiClient := openai.NewClient(openaiopt.WithHTTPClient(providerHTTP))

response, err := openaiClient.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
  Model: "example-chat-model",
  Messages: []openai.ChatCompletionMessageParamUnion{openai.UserMessage("Hello")},
})
if err != nil { return err }
_ = response
```

Import `github.com/openai/openai-go/v3` as `openai` and its `/option` package as `openaiopt`. The provider SDK reads `OPENAI_API_KEY` from the environment. `NewStreaming` is also captured automatically: the wrapper requests `stream_options.include_usage` and records the final usage-bearing chunk after the stream is consumed. Closing a stream before final usage arrives cannot produce a usage event.

### Anthropic

```bash theme={null}
go get github.com/anthropics/anthropic-sdk-go@v1.5.0
```

```go theme={null}
providerHTTP, err := nozle.WrapAnthropicHTTPClient(&http.Client{}, client, nozle.ProviderWrapOptions{
  CustomerID: "workspace_123", SubscriptionID: "workspace_123_subscription",
  FeatureCode: "llm_tokens", CostMeterCode: "ai_tokens", Feature: "assistant_reply",
  OnError: func(err error) { log.Printf("Nozle usage tracking: %v", err) },
})
if err != nil { return err }
anthropicClient := anthropic.NewClient(anthropicopt.WithHTTPClient(providerHTTP))

message, err := anthropicClient.Messages.New(ctx, anthropic.MessageNewParams{
  Model: "example-anthropic-model", MaxTokens: 1024,
  Messages: []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock("Hello"))},
})
if err != nil { return err }
_ = message
```

Import `github.com/anthropics/anthropic-sdk-go` as `anthropic` and its `/option` package as `anthropicopt`. The provider SDK reads `ANTHROPIC_API_KEY` from the environment. `NewStreaming` is captured after its `message_start` input usage and `message_delta` output usage have been read.

## Wrapper options and delivery

| Option | Purpose |
| - | - |
| `CustomerID` | External customer ID; required. |
| `SubscriptionID` | Explicit external subscription target; recommended when more than one subscription is active. |
| `FeatureCode` | Feature Event code; defaults to `llm_tokens`. |
| `CostMeterCode` | Optional Cost Event Meter for categorized token costs. |
| `Feature` | Optional Feature Event property. |
| `DeliveryTimeout` | Timeout for background telemetry; defaults to 15 seconds. |
| `OnError` | Called with the Nozle delivery error when sending a captured event fails. It does not change the provider response. These wrappers do not check that a Feature has the event code; only `WrapHTTPClient` reports a `*nozle.SetupError` with `Code` `feature_not_found`. |
| `OnResult` | `func(error)`, called after every delivery attempt: with `nil` on success, with the error on failure. |

The wrapper copies the supplied `*http.Client`; it does not mutate the original. It captures only request paths ending in `/chat/completions` (OpenAI) and `/messages` (Anthropic); other routes, such as `/v1/responses`, pass through without an event. Usage delivery runs in a goroutine after the provider response or stream completes, without delaying the provider result. Use `OnError` to log or queue failures. The wrapper does not automatically retry telemetry.

The callbacks fire only when there was usage to deliver. A provider non-2xx response, a body that is not valid JSON, a response without `usage`, or a stream closed before its final usage produces no event and calls neither `OnError` nor `OnResult`. Because delivery is in the background, a process that exits right after the provider call loses the event; wait for `OnResult` before exiting a short-lived program. A `DeliveryTimeout` that is too short fails delivery: with 1 ms, both callbacks receive a context deadline error.

## Manual capture

Use the direct capture methods when your provider client cannot accept an `*http.Client`, or when your application needs to choose a transaction ID or coordinate durable delivery. Pass usage from the completed provider response; these methods return Nozle telemetry errors to your code.

### OpenAI usage

Map `prompt_tokens` to `PromptTokens`, `completion_tokens` to `CompletionTokens`, and the cached-input and reasoning details when available.

```go theme={null}

client := nozle.New("") // reads NOZLE_SECRET_KEY

err := client.CaptureOpenAIUsage(ctx, nozle.ProviderCapture{
  CustomerID: "workspace_123",
  SubscriptionID: "workspace_123_subscription",
  FeatureCode: "llm_tokens",
  CostMeterCode: "ai_tokens",
  Feature: "assistant_reply",
  Model: "example-chat-model",
  RequestID: "provider-request-123",
  TransactionID: nozle.CreateTransactionID(),
}, nozle.OpenAIUsage{
  PromptTokens: 320,
  CachedInputTokens: 20,
  CompletionTokens: 84,
  ReasoningTokens: 4,
})
if err != nil { /* report or queue telemetry failure */ }
```

The Go SDK clamps negative counts to zero and caps cached input and reasoning at their respective totals. `PromptTokens` and `CompletionTokens` remain the total Feature Event counts; detailed Cost Events split those totals by category.

### Anthropic usage

Map `input_tokens` to `InputTokens`, `cache_read_input_tokens` to `CachedInputTokens`, `cache_creation_input_tokens` to `CacheWriteTokens`, and `output_tokens` to `OutputTokens`. For manual stream capture, collect input usage from `message_start` and output usage from `message_delta`, then capture after the stream completes.

```go theme={null}
err := client.CaptureAnthropicUsage(ctx, nozle.ProviderCapture{
  CustomerID: "workspace_123",
  SubscriptionID: "workspace_123_subscription",
  FeatureCode: "llm_tokens",
  CostMeterCode: "ai_tokens",
  Feature: "assistant_reply",
  Model: "example-anthropic-model",
  RequestID: "provider-request-456",
  TransactionID: nozle.CreateTransactionID(),
}, nozle.AnthropicUsage{
  InputTokens: 240,
  CachedInputTokens: 40,
  CacheWriteTokens: 12,
  OutputTokens: 90,
})
if err != nil { /* report or queue telemetry failure */ }
```

Anthropic's Feature Event `input_tokens` includes regular input, cached input, and cache-write input. Negative token counts are clamped to zero.

## Tracked properties

Automatic and manual capture create one Feature Event through Engine with these properties:

```json theme={null}
{
  "provider": "openai",
  "model": "example-chat-model",
  "input_tokens": 320,
  "output_tokens": 84,
  "latency_ms": 742,
  "feature": "assistant_reply"
}
```

The automatic wrapper measures latency. For manual capture, set `Latency` on `ProviderCapture`; otherwise `latency_ms` is zero. `FeatureCode` defaults to `llm_tokens`. Supplying `SubscriptionID` avoids a lookup when a customer has more than one active subscription.

When `CostMeterCode` is set, each non-zero token category creates a linked Engine Cost Event with properties `{ tokens, provider, model, type }`:

* OpenAI: `input`, `cached_input`, `output`, and `reasoning`.
* Anthropic: `input`, `cached_input`, `cache_write`, and `output`.

Configure the Cost Event Meter and effective-dated provider/model/category Cost Rules to rate those events. The parent Feature Event's transaction ID is `llm-<format>-<response id>` when the response has an id, so the same response is never recorded twice; otherwise a UUIDv7 is generated. In manual capture, you can supply `TransactionID`; category Cost Event IDs derive from that same ID, so reuse it when retrying the same capture.

## Delivery and errors

Go sends the Feature Event first, then attempts each non-zero Cost Event category. If one Cost Event fails, it attempts the remaining categories. Automatic capture reports delivery errors through `OnError` and `OnResult` (see the cases above where neither fires); manual capture returns them. If the Feature Event fails, Cost Events are not sent. Neither path retries automatically.

For strict or durable telemetry delivery, use manual capture through your queue with a stable transaction ID. The automatic wrapper preserves successful provider responses when Nozle tracking fails.

The Go wrappers, like the Node and Python wrappers, capture completed provider calls and streams automatically. Go connects at the provider HTTP client because Go does not support replacing another package's methods at runtime. The Feature Event fields and Cost Event categories match the published SDKs.


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