---
title: "Trace model clients"
description: "Trace direct calls to a model provider with wrap() or an OpenTelemetry instrumentor."
sidebar:
  label: "Model clients"
seo:
  title: "Trace model clients | Judgment Docs"
  description: "Trace direct calls to a model provider with wrap() or an OpenTelemetry instrumentor."
---

If a framework (LangGraph, OpenAI Agents SDK, Claude Agent SDK, Vercel AI SDK) makes model calls for you, use that framework's page instead, so each call is recorded once.

## `wrap()` a supported client

`wrap()` patches the client in place. Every call becomes an LLM span with the model name, token counts, and cost.

| Client | Python `wrap()` | TypeScript `wrap()` |
|---|---|---|
| OpenAI (`OpenAI`, `AsyncOpenAI`) | Yes | Yes |
| Anthropic (`Anthropic`, `AsyncAnthropic`) | Yes | No, use an instrumentation below |
| Together (`Together`, `AsyncTogether`) | Yes | No |
| Google GenAI (`google.genai.Client`) | Yes | No |

**Python**

```bash
pip install judgeval openai
```

```python title="agent.py"
from openai import OpenAI
from judgeval import Tracer, wrap

Tracer.init(project_name="lunch-order-agent")
client = wrap(OpenAI())

@Tracer.observe(span_type="agent")
def handle_request(message: str) -> str:
    response = client.chat.completions.create(
        model="gpt-5.2",
        messages=[{"role": "user", "content": message}],
    )
    return response.choices[0].message.content

print(handle_request("order me mediterranean food to feed 30 hungry engineers"))
Tracer.force_flush()
```

**TypeScript**

```bash
npm install judgeval openai
```

```typescript title="agent.ts"
import OpenAI from "openai";
import { Tracer, wrap } from "judgeval";

await Tracer.init({ projectName: "lunch-order-agent" });
const client = wrap(new OpenAI());

const handleRequest = Tracer.observe(
  async function handleRequest(message: string) {
    const response = await client.chat.completions.create({
      model: "gpt-5.2",
      messages: [{ role: "user", content: message }],
    });
    return response.choices[0].message.content;
  },
  { spanType: "agent" },
);

console.log(
  await handleRequest("order me mediterranean food to feed 30 hungry engineers"),
);
await Tracer.shutdown();
```

The other Python clients wrap the same way:

```python
from anthropic import Anthropic
from google import genai
from together import Together
from judgeval import Tracer, wrap

Tracer.init(project_name="lunch-order-agent")

anthropic_client = wrap(Anthropic())
together_client = wrap(Together())
gemini_client = wrap(genai.Client())
```

For Google, wrap `google.genai.Client`. The older `google.generativeai` package (`genai.GenerativeModel(...)`) isn't detected: `wrap()` logs "Unknown client type", tries to wrap it as an OpenAI client, and raises `TypeError` because it isn't one.

## OpenAI-compatible endpoints

OpenRouter, Ollama's OpenAI-compatible server, vLLM, and similar endpoints work with `wrap(OpenAI(base_url=...))`, because the client is still an `OpenAI` instance. For OpenRouter, use `base_url="https://openrouter.ai/api/v1"` and pass `extra_body={"usage": {"include": True}}` on each call so the response carries the cost (OpenRouter's usage accounting).

## Other providers: register an instrumentor

For clients `wrap()` doesn't support, register an OpenTelemetry instrumentor with Judgment. `Tracer.registerOTELInstrumentation()` calls the instrumentor with Judgment's tracer provider, so the spans go to your project.

```python
from judgeval import Tracer
from opentelemetry.instrumentation.bedrock import BedrockInstrumentor

Tracer.init(project_name="lunch-order-agent")
Tracer.registerOTELInstrumentation(BedrockInstrumentor())
```

| Provider | Python package | Import |
|---|---|---|
| AWS Bedrock | `opentelemetry-instrumentation-bedrock` | `from opentelemetry.instrumentation.bedrock import BedrockInstrumentor` |
| Vertex AI | `opentelemetry-instrumentation-vertexai` | `from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor` |
| Groq | `opentelemetry-instrumentation-groq` | `from opentelemetry.instrumentation.groq import GroqInstrumentor` |
| Ollama | `opentelemetry-instrumentation-ollama` | `from opentelemetry.instrumentation.ollama import OllamaInstrumentor` |
| Mistral | `opentelemetry-instrumentation-mistralai` | `from opentelemetry.instrumentation.mistralai import MistralAiInstrumentor` |
| Hugging Face Transformers (local) | `opentelemetry-instrumentation-transformers` | `from opentelemetry.instrumentation.transformers import TransformersInstrumentor` |
| LiteLLM | `opentelemetry-instrumentation-litellm` | `from opentelemetry.instrumentation.litellm import LiteLLMInstrumentor` |
| Together (instead of `wrap()`) | `openinference-instrumentation-together` | `from openinference.instrumentation.together import TogetherInstrumentor` |

These packages come from OpenLLMetry (`opentelemetry-instrumentation-*`) and OpenInference (`openinference-instrumentation-*`). Judgment didn't write them. See [OpenTelemetry instrumentation libraries](/documentation/connect/integrations/otel-instrumentation-libraries) for how each family maps into Judgment.

LiteLLM has no client object to wrap, so the instrumentor is the only option. When sub-agents each call `litellm.completion` inside their own `@Tracer.observe(span_type="agent")` function, the cost lands on the model spans under each sub-agent, not on the orchestrator span above them.

In TypeScript, `wrap()` covers OpenAI only. Passing it another client doesn't throw: the SDK logs an error and returns the client unchanged, so its calls aren't traced. For Anthropic, register `new AnthropicInstrumentation()` from `@traceloop/instrumentation-anthropic` with `Tracer.registerOTELInstrumentation()` in a file your app loads first. The TypeScript tab on [OpenTelemetry instrumentation libraries](/documentation/connect/integrations/otel-instrumentation-libraries) shows the pattern. For Claude Managed Agents sessions, use `wrapAnthropicManagedAgents()`; see [Claude Managed Agents](/documentation/connect/integrations/claude-agent-sdk#claude-managed-agents-typescript).

## If something looks wrong

- If you see two LLM spans per call, you wrapped the client and also registered an instrumentor (or a framework integration) for it. Keep one.
- In Python, "Unknown client type" in the logs, then a `TypeError`, means the client isn't one of the four detected types. Use an instrumentor from the table.
- In TypeScript, an error logged from `wrapOpenAI` with no LLM spans means the client isn't an OpenAI client. Use an instrumentation instead.
- If spans have model and tokens but no input or output, the instrumentor writes prompts under keys Judgment doesn't map yet. Add a mapper; see [Attribute keys and mappers](/documentation/reference/attribute-keys-and-mappers).
