---
title: "Manual Instrumentation"
description: "Learn how to manually instrument your AI agents to capture spans, token usage, and tool execution."
url: https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation/
---

# Manual Instrumentation | Sentry for Python

If your AI library does not have automatic instrumentation, create spans manually. Spans need well-defined names and data attributes so agent data shows up correctly.

The [@sentry\_sdk.trace()](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md#span-templates) decorator can also create these spans via templates.

For conversations and privacy (`set_conversation_id`, `set_user`, `send_default_pii`), see the [Agent Tracing hub](https://docs.sentry.io/platforms/python/agent-tracing.md).

## [Span Hierarchy](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#span-hierarchy)

When instrumenting an agent loop, spans nest like this:

```bash
── invoke_agent My Agent          (gen_ai.invoke_agent)
   ├── chat gpt-4o                (gen_ai.chat)         ← 1st LLM call
   ├── execute_tool get_weather   (gen_ai.execute_tool)  ← tool run
   ├── chat gpt-4o                (gen_ai.chat)         ← 2nd LLM call
   └── ...
```

`gen_ai.invoke_agent` is the container. `gen_ai.chat` and `gen_ai.execute_tool` spans are its children (siblings of each other). A `gen_ai.chat` span can also appear without an agent parent for standalone LLM calls.

## [Common Attributes](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#common-attributes)

Set these **when the span starts** (before the model or tool call), so head-based sampling can see them:

* `gen_ai.operation.name` — required; classifies the span (`chat`, `invoke_agent`, `execute_tool`, …)
* `gen_ai.provider.name` — e.g. `openai`, `anthropic`
* `gen_ai.request.model` — requested model (pass the **raw** provider string)
* `gen_ai.agent.name` / `gen_ai.tool.name` — when applicable

Complex values (messages, tool definitions, arrays) must be **JSON strings** — span attributes only accept primitives.

Prompt, response, and tool content are treated as PII. They are only captured when `send_default_pii=True` (or per-integration `include_prompts`). See [Privacy Controls](https://docs.sentry.io/platforms/python/agent-tracing.md#privacy-controls).

## [AI Request Span](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#ai-request-span)

This span represents a request to an LLM model or service that generates a response based on the input prompt.

```python
import json
import sentry_sdk

messages = [{"role": "user", "parts": [{"type": "text", "content": "Tell me a joke"}]}]
tools = [{"name": "get_weather", "description": "Get weather for a city"}]

with sentry_sdk.start_span(
    op="gen_ai.chat",
    name="chat o3-mini",
    attributes={
        "gen_ai.operation.name": "chat",
        "gen_ai.request.model": "o3-mini",
        "gen_ai.provider.name": "openai",
        "gen_ai.agent.name": "Weather Agent",  # when this call is under an agent
        "gen_ai.system_instructions": "You are a helpful assistant.",
        "gen_ai.tool.definitions": json.dumps(tools),
        "gen_ai.input.messages": json.dumps(messages),
    },
) as span:
    result = client.chat.completions.create(model="o3-mini", messages=messages)

    span.set_data("gen_ai.response.model", result.model)
    span.set_data("gen_ai.response.id", result.id)
    span.set_data("gen_ai.output.messages", json.dumps([
        {"role": "assistant", "parts": [{"type": "text", "content": result.choices[0].message.content}]}
    ]))
    span.set_data("gen_ai.response.finish_reasons", json.dumps([result.choices[0].finish_reason]))
    span.set_data("gen_ai.usage.input_tokens", result.usage.prompt_tokens)
    span.set_data("gen_ai.usage.output_tokens", result.usage.completion_tokens)
    # If the provider reports cached tokens, record them as a subset of input tokens
    if getattr(result.usage, "prompt_tokens_details", None):
        span.set_data(
            "gen_ai.usage.cache_read.input_tokens",
            result.usage.prompt_tokens_details.cached_tokens,
        )
```

Keep system prompts in `gen_ai.system_instructions`, not inside `gen_ai.input.messages`. [Conversation titles](https://docs.sentry.io/product/agents/conversations.md#conversation-titles) are derived from the first user message in the input messages.

AI Request span attributes

The [@sentry\_sdk.trace()](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md#span-templates) decorator can also be used to create this span.

* The span `op` MUST be `"gen_ai.{gen_ai.operation.name}"`. (e.g. `"gen_ai.chat"`)
* The span `name` SHOULD be `"{gen_ai.operation.name} {gen_ai.request.model}"`. (e.g. `"chat o3-mini"`)
* The `gen_ai.operation.name` attribute MUST be `"chat"`, `"embeddings"`, `"generate_content"` or `"text_completion"`.
* The `gen_ai.provider.name` attribute MUST be the Generative AI product as identified by the client or server instrumentation. (e.g. `"openai"`)
* The `gen_ai.request.model` attribute MUST be the requested model. (e.g. `"o3-mini"`)
* The `gen_ai.response.model` attribute MUST be the concrete model that responded. (e.g. `"gpt-4o-2024-08-06"`)
* If the request originates from an agent, `gen_ai.agent.name` SHOULD be set to the agent's name. (e.g. `"Weather Agent"`)
* If relevant, `gen_ai.pipeline.name` SHOULD be set to the name of the AI workflow or pipeline. (e.g. `"weather-pipeline"`)

### [Request Attributes](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#request-attributes)

| Data Attribute                     | Type   | Requirement Level | Description                                                                                                     | Example                                                               |
| ---------------------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `gen_ai.input.messages`            | string | optional          | List of message objects sent to the LLM. **\[0]**, **\[1]**                                                     | `'[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]'` |
| `gen_ai.tool.definitions`          | string | optional          | List of objects describing the available tools. **\[0]**                                                        | `'[{"name": "random_number", "description": "..."}]'`                 |
| `gen_ai.system_instructions`       | string | optional          | The system instructions passed to the model.                                                                    | `"You are a helpful assistant."`                                      |
| `gen_ai.request.frequency_penalty` | float  | optional          | Model configuration parameter.                                                                                  | `0.5`                                                                 |
| `gen_ai.request.max_tokens`        | int    | optional          | Model configuration parameter.                                                                                  | `500`                                                                 |
| `gen_ai.request.seed`              | string | optional          | Seed for reproducible outputs.                                                                                  | `"12345"`                                                             |
| `gen_ai.request.temperature`       | float  | optional          | Model configuration parameter.                                                                                  | `0.1`                                                                 |
| `gen_ai.request.top_k`             | int    | optional          | Limits model to K most likely next tokens.                                                                      | `40`                                                                  |
| `gen_ai.request.top_p`             | float  | optional          | Model configuration parameter.                                                                                  | `0.7`                                                                 |
| `gen_ai.request.presence_penalty`  | float  | optional          | Model configuration parameter.                                                                                  | `0.5`                                                                 |
| `gen_ai.request.reasoning.level`   | string | optional          | The reasoning or thinking effort level requested for a GenAI model. Supported values vary by provider.          | `"medium"`                                                            |
| `gen_ai.request.messages`          | string | optional          | **Deprecated.** Use `gen_ai.input.messages` instead. List of message objects sent to the LLM. **\[0]**          | `'[{"role": "system", "content": "..."}]'`                            |
| `gen_ai.request.available_tools`   | string | optional          | **Deprecated.** Use `gen_ai.tool.definitions` instead. List of objects describing the available tools. **\[0]** | `'[{"name": "random_number", "description": "..."}]'`                 |

### [Response Attributes](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#response-attributes)

| Data Attribute                        | Type    | Requirement Level | Description                                                                                                         | Example                                                                      |
| ------------------------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `gen_ai.response.model`               | string  | required          | The concrete model that responded (may differ from `gen_ai.request.model`).                                         | `"gpt-4o-2024-08-06"`                                                        |
| `gen_ai.output.messages`              | string  | optional          | Stringified array of message objects representing the model's output. **\[0]**, **\[1]**                            | `'[{"role": "assistant", "parts": [{"type": "text", "content": "..."}]}]'`   |
| `gen_ai.response.finish_reasons`      | string  | optional          | Stringified array of reasons the model stopped generating. **\[0]**                                                 | `'["stop"]'`                                                                 |
| `gen_ai.response.id`                  | string  | optional          | Unique identifier for the completion.                                                                               | `"chatcmpl-abc123"`                                                          |
| `gen_ai.response.streaming`           | boolean | optional          | Whether the response was streamed.                                                                                  | `true`                                                                       |
| `gen_ai.response.time_to_first_chunk` | double  | optional          | Seconds until first response chunk in streaming.                                                                    | `0.5`                                                                        |
| `gen_ai.response.text`                | string  | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The text representation of the model's responses.             | `"The weather in Paris is rainy"`                                            |
| `gen_ai.response.tool_calls`          | string  | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The tool calls in the model's response. **\[0]**              | `'[{"name": "random_number", "type": "function_call", "arguments": "..."}]'` |
| `gen_ai.response.time_to_first_token` | double  | optional          | **Deprecated.** Use `gen_ai.response.time_to_first_chunk` instead. Seconds until first response chunk in streaming. | `0.5`                                                                        |

### [Token Usage](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#token-usage)

| Data Attribute                             | Type | Requirement Level | Description                                                                                                                    | Example |
| ------------------------------------------ | ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `gen_ai.usage.input_tokens`                | int  | optional          | The number of tokens used in the AI input (prompt), including cached tokens. **\[2]**                                          | `60`    |
| `gen_ai.usage.cache_read.input_tokens`     | int  | optional          | The number of cached tokens used in the AI input (prompt).                                                                     | `50`    |
| `gen_ai.usage.cache_creation.input_tokens` | int  | optional          | Tokens written to cache when processing input.                                                                                 | `20`    |
| `gen_ai.usage.output_tokens`               | int  | optional          | The number of tokens used in the AI output, including reasoning tokens. **\[3]**                                               | `130`   |
| `gen_ai.usage.reasoning.output_tokens`     | int  | optional          | The number of tokens used for reasoning.                                                                                       | `30`    |
| `gen_ai.usage.total_tokens`                | int  | optional          | The sum of `gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens`.                                                       | `190`   |
| `gen_ai.usage.input_tokens.cached`         | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_read.input_tokens` instead. The number of cached tokens used in the AI input (prompt). | `50`    |
| `gen_ai.usage.input_tokens.cache_write`    | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_creation.input_tokens` instead. Tokens written to cache when processing input.         | `20`    |
| `gen_ai.usage.output_tokens.reasoning`     | int  | optional          | **Deprecated.** Use `gen_ai.usage.reasoning.output_tokens` instead. The number of tokens used for reasoning.                   | `30`    |

* **\[0]:** Span attributes only allow primitive data types. This means you need to use a stringified version of a list of dictionaries. Do NOT set `[{"foo": "bar"}]` but rather the string `'[{"foo": "bar"}]'` (must be parsable JSON).
* **\[1]:** Messages use the format `{role, parts}` where `parts` is an array of typed objects: `[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]`. The `role` must be `"user"`, `"assistant"`, `"tool"`, or `"system"`. Each part has a `type`; common types include `"text"` (user-visible content), `"reasoning"` (internal thinking/chain-of-thought), `"tool_call"`, and `"tool_call_response"`. Use `{"type": "reasoning", "content": "..."}` for the model's thinking output — Sentry surfaces it separately and filters it out of the user-facing Conversations view, so do not represent thinking content as a `"text"` part. For backwards compatibility, the legacy format `{role, content}` is also accepted.
* **\[2]:** Cached tokens are a subset of input tokens; `gen_ai.usage.input_tokens` includes `gen_ai.usage.cache_read.input_tokens`.
* **\[3]:** Reasoning tokens are a subset of output tokens; `gen_ai.usage.output_tokens` includes `gen_ai.usage.reasoning.output_tokens`.

### [Message parts](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#message-parts)

Messages use `{role, parts}` where each part has a `type`. Common types:

* `text` — user-visible content
* `reasoning` — internal thinking (not shown in the user-facing Conversations view)
* `tool_call` / `tool_call_response` — tool invocations linked by a shared `id`

Unknown part types are not shown prominently in the Conversations UI. They remain available only in the raw span attribute values.

#### [Thinking / reasoning](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#thinking--reasoning)

Models with extended thinking (such as Anthropic's `thinking` blocks, Gemini's `thought`, or DeepSeek's `reasoning_content`) produce internal reasoning that isn't part of the user-visible reply. Represent this as a `reasoning` part alongside the user-facing `text` part — don't fold thinking into `text`.

```python
span.set_data("gen_ai.output.messages", json.dumps([
    {
        "role": "assistant",
        "parts": [
            {"type": "reasoning", "content": "6 times 7 is 42."},
            {"type": "text", "content": "The answer is 42."},
        ],
    }
]))
span.set_data("gen_ai.usage.output_tokens", result.usage.completion_tokens)
# Reasoning tokens are a subset of output tokens
span.set_data(
    "gen_ai.usage.reasoning.output_tokens",
    result.usage.completion_tokens_details.reasoning_tokens,
)
```

When previous thinking is fed back into a multi-turn request, include the same `reasoning` parts in assistant messages within `gen_ai.input.messages`.

#### [Tool calls in messages](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#tool-calls-in-messages)

Link a tool request to its result with the same `id`:

```python
# Model asked to call a tool
span.set_data("gen_ai.output.messages", json.dumps([
    {
        "role": "assistant",
        "parts": [
            {
                "type": "tool_call",
                "id": "call_abc",
                "name": "get_weather",
                "arguments": {"location": "Paris"},
            }
        ],
    }
]))

# Later turn: tool result fed back to the model (on input messages)
input_with_tool = [
    {"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]},
    {
        "role": "assistant",
        "parts": [
            {
                "type": "tool_call",
                "id": "call_abc",
                "name": "get_weather",
                "arguments": {"location": "Paris"},
            }
        ],
    },
    {
        "role": "tool",
        "parts": [
            {
                "type": "tool_call_response",
                "id": "call_abc",
                "name": "get_weather",
                "content": "{\"temp_c\": 18}",
            }
        ],
    },
]
```

## [Invoke Agent Span](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#invoke-agent-span)

This span represents the execution of an AI agent, capturing the full lifecycle from receiving a task to producing a final response.

For a complete guide on naming agents across all supported frameworks, see [Naming Your Agents](https://docs.sentry.io/product/agents/naming.md).

```python
import json
import sentry_sdk

messages = [{"role": "user", "parts": [{"type": "text", "content": "What's the weather in Paris?"}]}]
tools = [{"name": "get_weather", "description": "Get weather for a city"}]

with sentry_sdk.start_span(
    op="gen_ai.invoke_agent",
    name="invoke_agent Weather Agent",
    attributes={
        "gen_ai.operation.name": "invoke_agent",
        "gen_ai.agent.name": "Weather Agent",
        "gen_ai.provider.name": "openai",
        "gen_ai.request.model": "o3-mini",
        "gen_ai.system_instructions": "You are a weather assistant.",
        "gen_ai.tool.definitions": json.dumps(tools),
        "gen_ai.input.messages": json.dumps(messages),
    },
) as span:
    final_output = my_agent.run()

    span.set_data("gen_ai.output.messages", json.dumps([
        {"role": "assistant", "parts": [{"type": "text", "content": str(final_output)}]}
    ]))
    span.set_data("gen_ai.usage.input_tokens", final_output.usage.input_tokens)
    span.set_data("gen_ai.usage.output_tokens", final_output.usage.output_tokens)
```

Child `gen_ai.chat` spans should also set `gen_ai.agent.name` so model usage can be attributed per agent.

Invoke Agent span attributes

Describes AI agent invocation.

The [@sentry\_sdk.trace()](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md#span-templates) decorator can also be used to create this span.

* The span `op` MUST be `"gen_ai.invoke_agent"`.
* The span `name` SHOULD be `"invoke_agent {gen_ai.agent.name}"`.
* The `gen_ai.operation.name` attribute MUST be `"invoke_agent"`.
* The `gen_ai.agent.name` attribute SHOULD be set to the agent's name. (e.g. `"Weather Agent"`)
* If relevant, `gen_ai.pipeline.name` SHOULD be set to the name of the AI workflow or pipeline the agent belongs to.

Additional attributes on the span:

### [Request Attributes](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#request-attributes)

| Data Attribute                   | Type   | Requirement Level | Description                                                                                                     | Example                                                               |
| -------------------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `gen_ai.input.messages`          | string | optional          | List of message objects given to the agent. **\[0]**, **\[1]**                                                  | `'[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]'` |
| `gen_ai.tool.definitions`        | string | optional          | List of objects describing the available tools. **\[0]**                                                        | `'[{"name": "random_number", "description": "..."}]'`                 |
| `gen_ai.system_instructions`     | string | optional          | The system instructions passed to the model.                                                                    | `"You are a helpful assistant."`                                      |
| `gen_ai.pipeline.name`           | string | optional          | The name of the AI workflow or pipeline the agent belongs to.                                                   | `"weather-pipeline"`                                                  |
| `gen_ai.request.messages`        | string | optional          | **Deprecated.** Use `gen_ai.input.messages` instead. List of message objects given to the agent. **\[0]**       | `'[{"role": "system", "content": "..."}]'`                            |
| `gen_ai.request.available_tools` | string | optional          | **Deprecated.** Use `gen_ai.tool.definitions` instead. List of objects describing the available tools. **\[0]** | `'[{"name": "random_number", "description": "..."}]'`                 |

### [Response Attributes](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#response-attributes)

| Data Attribute               | Type   | Requirement Level | Description                                                                                            | Example                                                                      |
| ---------------------------- | ------ | ----------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `gen_ai.output.messages`     | string | optional          | Stringified array of message objects representing the agent's output. **\[0]**, **\[1]**               | `'[{"role": "assistant", "parts": [{"type": "text", "content": "..."}]}]'`   |
| `gen_ai.response.text`       | string | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The text representation of the agent's response. | `"The weather in Paris is rainy"`                                            |
| `gen_ai.response.tool_calls` | string | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The tool calls in the model's response. **\[0]** | `'[{"name": "random_number", "type": "function_call", "arguments": "..."}]'` |

### [Token Usage](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#token-usage)

| Data Attribute                             | Type | Requirement Level | Description                                                                                                                    | Example |
| ------------------------------------------ | ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `gen_ai.usage.input_tokens`                | int  | optional          | The number of tokens used in the AI input (prompt), including cached tokens. **\[2]**                                          | `60`    |
| `gen_ai.usage.cache_read.input_tokens`     | int  | optional          | The number of cached tokens used in the AI input (prompt).                                                                     | `50`    |
| `gen_ai.usage.cache_creation.input_tokens` | int  | optional          | Tokens written to cache when processing input.                                                                                 | `20`    |
| `gen_ai.usage.output_tokens`               | int  | optional          | The number of tokens used in the AI output, including reasoning tokens. **\[3]**                                               | `130`   |
| `gen_ai.usage.reasoning.output_tokens`     | int  | optional          | The number of tokens used for reasoning.                                                                                       | `30`    |
| `gen_ai.usage.total_tokens`                | int  | optional          | The sum of `gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens`.                                                       | `190`   |
| `gen_ai.usage.input_tokens.cached`         | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_read.input_tokens` instead. The number of cached tokens used in the AI input (prompt). | `50`    |
| `gen_ai.usage.input_tokens.cache_write`    | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_creation.input_tokens` instead. Tokens written to cache when processing input.         | `20`    |
| `gen_ai.usage.output_tokens.reasoning`     | int  | optional          | **Deprecated.** Use `gen_ai.usage.reasoning.output_tokens` instead. The number of tokens used for reasoning.                   | `30`    |

* **\[0]:** Span attributes only allow primitive data types. This means you need to use a stringified version of a list of dictionaries. Do NOT set `[{"foo": "bar"}]` but rather the string `'[{"foo": "bar"}]'` (must be parsable JSON).
* **\[1]:** Messages use the format `{role, parts}` where `parts` is an array of typed objects: `[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]`. The `role` must be `"user"`, `"assistant"`, `"tool"`, or `"system"`. Each part has a `type`; common types include `"text"` (user-visible content), `"reasoning"` (internal thinking/chain-of-thought), `"tool_call"`, and `"tool_call_response"`. Use `{"type": "reasoning", "content": "..."}` for the model's thinking output — Sentry surfaces it separately and filters it out of the user-facing Conversations view, so do not represent thinking content as a `"text"` part. For backwards compatibility, the legacy format `{role, content}` is also accepted.
* **\[2]:** Cached tokens are a subset of input tokens; `gen_ai.usage.input_tokens` includes `gen_ai.usage.cache_read.input_tokens`.
* **\[3]:** Reasoning tokens are a subset of output tokens; `gen_ai.usage.output_tokens` includes `gen_ai.usage.reasoning.output_tokens`.

## [Execute Tool Span](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#execute-tool-span)

This span represents the execution of a tool or function that was requested by an AI model, including the input arguments and resulting output.

```python
import json
import sentry_sdk

with sentry_sdk.start_span(
    op="gen_ai.execute_tool",
    name="execute_tool get_weather",
    attributes={
        "gen_ai.operation.name": "execute_tool",
        "gen_ai.tool.name": "get_weather",
        "gen_ai.tool.description": "Get weather for a city",
        "gen_ai.tool.call.arguments": json.dumps({"location": "Paris"}),
    },
) as span:
    try:
        result = get_weather(location="Paris")
        span.set_data("gen_ai.tool.call.result", json.dumps(result))
    except Exception as exc:
        span.set_status("internal_error")
        span.set_data("error.type", type(exc).__name__)
        raise
```

Marking failed tools with an error status populates the Tool Errors widget.

Execute Tool span attributes

Describes a tool execution.

The [@sentry\_sdk.trace()](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md#span-templates) decorator can also be used to create this span.

* The span `op` MUST be `"gen_ai.execute_tool"`.
* The span `name` SHOULD be `"execute_tool {gen_ai.tool.name}"`. (e.g. `"execute_tool query_database"`)
* The `gen_ai.operation.name` attribute MUST be `"execute_tool"`.
* The `gen_ai.tool.name` attribute SHOULD be set to the name of the tool. (e.g. `"query_database"`)

Additional attributes on the span:

| Data Attribute               | Type   | Requirement Level | Description                                                                                           | Example                                    |
| ---------------------------- | ------ | ----------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `gen_ai.tool.name`           | string | optional          | Name of the tool executed.                                                                            | `"random_number"`                          |
| `gen_ai.tool.call.arguments` | string | optional          | Arguments of the tool call (stringified JSON).                                                        | `"{\"max\":10}"`                           |
| `gen_ai.tool.call.result`    | string | optional          | Result of the tool call (stringified).                                                                | `"7"`                                      |
| `gen_ai.tool.description`    | string | optional          | Description of the tool executed.                                                                     | `"Tool returning a random number"`         |
| `gen_ai.tool.type`           | string | optional          | The type of the tools.                                                                                | `"function"`; `"extension"`; `"datastore"` |
| `gen_ai.tool.input`          | string | optional          | **Deprecated.** Use `gen_ai.tool.call.arguments` instead. Input given to the executed tool as string. | `"{\"max\":10}"`                           |
| `gen_ai.tool.output`         | string | optional          | **Deprecated.** Use `gen_ai.tool.call.result` instead. The output from the tool.                      | `"7"`                                      |

## [Streaming Responses](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#streaming-responses)

When the model streams tokens, keep the span open until the stream finishes (including when you yield chunks to the client). Set response attributes when you have the final usage and text:

* `gen_ai.response.streaming` — `True`
* `gen_ai.response.time_to_first_chunk` — seconds until the first chunk
* `gen_ai.response.tokens_per_second` — output throughput, if you can measure it
* `gen_ai.output.messages`, token usage, and `gen_ai.response.model` — same as a non-streaming call, once the stream completes

How you hold the span open depends on your stack (generator, async iterator, callback). Instrument that path yourself so the span ends on completion or error.

## [Token Usage and Cost Gotchas](https://docs.sentry.io/platforms/python/agent-tracing/manual-instrumentation.md#token-usage-and-cost-gotchas)

When manually setting token attributes, be aware of how Sentry uses them to [calculate model costs](https://docs.sentry.io/product/agents/costs.md).

**Cached and reasoning tokens are subsets, not separate counts.** `gen_ai.usage.input_tokens` is the **total** input token count that already includes any cached tokens. Similarly, `gen_ai.usage.output_tokens` already includes reasoning tokens. Sentry subtracts the cached/reasoning counts from the totals to compute the "raw" portion, so reporting them incorrectly can produce wrong or negative costs.

For example, say your LLM call uses 100 input tokens total, 90 of which were served from cache. Using a standard rate of $0.01 per token and a cached rate of $0.001 per token:

**Correct** — `input_tokens` is the total (includes cached):

* `gen_ai.usage.input_tokens = 100`
* `gen_ai.usage.cache_read.input_tokens = 90`
* Sentry calculates: `(100 - 90) × $0.01 + 90 × $0.001` = `$0.10 + $0.09` = **$0.19** ✓

**Wrong** — `input_tokens` set to only the non-cached tokens, making cached larger than total:

* `gen_ai.usage.input_tokens = 10`
* `gen_ai.usage.cache_read.input_tokens = 90`
* Sentry calculates: `(10 - 90) × $0.01 + 90 × $0.001` = `−$0.80 + $0.09` = **−$0.71**

Because `input_tokens.cached` (90) is larger than `input_tokens` (10), the subtraction goes negative, resulting in a negative total cost.

The same applies to `gen_ai.usage.output_tokens` and `gen_ai.usage.reasoning.output_tokens`.

Sentry derives [model cost](https://docs.sentry.io/product/agents/costs.md) from the model name and token counts. You do not need to set `gen_ai.cost.*` attributes. Pass the raw provider model string unchanged so pricing can resolve.
