dd-trace-py LLMObs Integrations
LLMObs integrations enable Datadog LLM Observability for AI/LLM libraries. They extract model inputs, outputs, token usage, and tool calls from traced spans. This skill should be used in addition to the apm-integrations skill.
Two-Layer Architecture
LLMObs integrations consist of two cooperating layers:
- Patch Layer (
ddtrace/contrib/internal/{name}/patch.py) -- wraps library functions. Standard request/response LLM integrations construct LlmRequestEvent and use core.context_with_event() so the LLM tracing subscriber owns span lifecycle and LLMObs tag extraction.
- Integration Layer (
ddtrace/llmobs/_integrations/{name}.py) -- extends BaseLLMIntegration, implements _set_base_span_tags() and _llmobs_set_tags() to extract and set provider-specific messages, tools, metadata, and token metrics.
Both layers must work together. The patch layer identifies the operation and passes request/response data through the event; the integration layer controls what data is extracted.
Active Patch Patterns
- Event-based request spans: Use
LlmRequestEvent with core.context_with_event() for new standard request/response LLM integrations. Anthropic is the canonical reference. This is the preferred pattern.
- Direct integration spans: Some existing or specialized integrations still call
integration.trace() and integration.llmobs_set_tags() directly, especially for child spans, agent/tool spans, or integrations not yet migrated. Google GenAI, OpenAI tool spans, and Claude Agent SDK are useful references.
Key Files
| Purpose |
File |
| Base LLM integration class |
ddtrace/llmobs/_integrations/base.py (BaseLLMIntegration) |
| Stream handler base classes |
ddtrace/llmobs/_integrations/base_stream_handler.py (BaseStreamHandler, StreamHandler, AsyncStreamHandler) |
| Shared utilities |
ddtrace/llmobs/_integrations/utils.py |
| LLMObs annotation helper |
ddtrace/llmobs/_utils.py (_annotate_llmobs_span_data) |
| LLMObs constants |
ddtrace/llmobs/_constants.py |
| LLMObs types |
ddtrace/llmobs/types.py (Message, AudioPart, ToolCall, ToolResult, ToolDefinition) |
| Integration registry |
ddtrace/llmobs/_integrations/__init__.py |
Reference Integrations
Always read 1-2 references before writing or modifying LLMObs code.
| Provider |
Patch File |
LLMObs Integration |
LLMObs Tests |
| Anthropic (canonical) |
ddtrace/contrib/internal/anthropic/patch.py |
ddtrace/llmobs/_integrations/anthropic.py |
tests/contrib/anthropic/test_anthropic_llmobs.py |
| Claude Agent SDK (latest, agent pattern) |
ddtrace/contrib/internal/claude_agent_sdk/patch.py |
ddtrace/llmobs/_integrations/claude_agent_sdk.py |
tests/contrib/claude_agent_sdk/test_claude_agent_sdk_llmobs.py |
| OpenAI |
ddtrace/contrib/internal/openai/patch.py |
ddtrace/llmobs/_integrations/openai.py |
tests/contrib/openai/test_openai_llmobs.py |
| Google GenAI |
ddtrace/contrib/internal/google_genai/patch.py |
ddtrace/llmobs/_integrations/google_genai.py |
tests/contrib/google_genai/test_google_genai_llmobs.py |
Use Anthropic as the canonical reference for standard LLM integrations. Use Claude Agent SDK for agent-pattern integrations (agent spans, tool child spans, thinking blocks).
Abstract Methods to Implement
Subclass BaseLLMIntegration and implement:
_set_base_span_tags(span, **kwargs)
Set provider-specific APM tags on the span (e.g., {name}.request.model).
_llmobs_set_tags(span, args, kwargs, response, operation)
Extract and annotate all LLMObs fields on the span:
| Field |
Description |
kind |
"llm" for LLM calls, "agent" for agent calls, "tool" for tool calls |
model_name |
Model identifier (e.g., "claude-3-sonnet-20240229") |
model_provider |
Provider name (e.g., "anthropic", "openai") |
input_messages |
List of Message objects from request |
output_messages |
List of Message objects from response |
metadata |
Dict of sanitized request parameters (temperature, top_p, etc.), plus response-derived scalars such as finish_reason (see Response Metadata below) |
metrics |
Token usage dict with INPUT_TOKENS_METRIC_KEY, OUTPUT_TOKENS_METRIC_KEY, TOTAL_TOKENS_METRIC_KEY |
tool_definitions |
List of ToolDefinition objects if tools are passed |
Fields are usually set via _annotate_llmobs_span_data(...), not raw span._set_ctx_items(...).
Response Metadata
metadata is not request-only. Scalars the provider reports on the response belong there too, so
long as they are not already covered by metrics (token counts) or output_messages.
The stop reason is the established case. Record it under the key finish_reason for every provider,
so one facet answers "why did generation stop" regardless of integration, and keep each provider's
own vocabulary as the value (stop/length/content_filter/tool_calls for the OpenAI family,
end_turn/max_tokens/refusal/tool_use for Anthropic). Read it from:
| Provider |
Source |
| openai/litellm chat + legacy completions |
choice.finish_reason (per choice) |
| openai responses API |
incomplete_details.reason |
| anthropic |
response.stop_reason, falling back to response.finish_reason for the streamed dict the aggregator rebuilds |
Rules:
- Merge, do not replace. Build the request metadata first, then update it with the
response-derived keys (
parameters.update(...)), so response data never clobbers request params.
- Omit the key when absent. If the provider reports no reason, leave
finish_reason out
entirely rather than writing None or "".
- Keep the value a single string. When a request returns multiple choices (
n > 1), comma-join
the per-choice reasons in choice order ("stop,length") instead of emitting a list, so the key
never changes type. _openai_finish_reason_metadata() in _integrations/utils.py does this.
- Guard on the response, not
span.error. A response can exist on an errored span (see the AI
Guard case in _integrations/anthropic.py); gate on response is not None.
Note two already-shipped integrations predate this key: bedrock and the claude-agent-sdk both write
metadata["stop_reason"]. Renaming those is a breaking change and has not been done — follow
finish_reason for new work.
Key Constraints
submit_to_llmobs=True must be set on LlmRequestEvent for event-based request spans or passed to integration.trace() for direct LLMObs spans
ctx.dispatch_ended_event() must run on success and error paths for event-based patch wrappers
- Streaming must use
BaseStreamHandler/AsyncStreamHandler -- never consume streams directly
- Event-based patch wrappers should not call
span.set_exc_info(), span.finish(), or integration.llmobs_set_tags() directly; the tracing subscriber handles that when the event ends
- Direct integration spans must keep
integration.llmobs_set_tags() and span lifecycle handling aligned with the closest current reference
- Integration instance must be stored on the module:
module._datadog_integration = MyLibIntegration(integration_config=config.mylib)
Message Types
from ddtrace.llmobs.types import AudioPart, ImagePart, Message, ToolCall, ToolResult, ToolDefinition
# Input/output messages
Message(content="text", role="user")
Message(content="response", role="assistant", tool_calls=[...])
# Audio attachments in multimodal messages
AudioPart(mime_type="audio/wav", content="<base64-audio>")
Message(content="", role="user", audio_parts=[...])
# Image attachments in multimodal messages
ImagePart(mime_type="image/png", content="<base64-image>")
Message(content="", role="user", image_parts=[...])
# Tool calls (in output messages)
ToolCall(name="get_weather", arguments={"city": "NYC"}, tool_id="toolu_123", type="tool")
# Tool results (in input messages)
ToolResult(result="72F sunny", tool_id="toolu_123", type="tool_result")
# Tool definitions (from request parameters)
ToolDefinition(name="get_weather", description="...", schema={...})
Debugging Quick Tips
- No LLMObs spans -- check
submit_to_llmobs=True, ctx.dispatch_ended_event(), and llmobs_enabled
- Wrong messages -- check message extraction handles multi-part content and tool blocks
- Wrong tokens -- check field name mapping (libraries use different names for token counts)
- Streaming broken -- verify
BaseStreamHandler subclass, check finalize_stream() dispatches the ended event or finishes direct-trace spans according to the reference pattern
- DD_TRACE_DEBUG=true to see patching activity and span creation
See Failure Modes for detailed debugging guide.
Reference Files
- Implementation Guide -- LLM-specific steps (references apm-integrations guide for the full workflow)
- Failure Modes -- All 8 failure modes with causes and fixes
- Testing Guide -- LLMObs test patterns, VCR cassettes, suitespec
1---2name: llmobs-integrations3description: dd-trace-py LLMObs integration development guide. Use when creating, modifying, or debugging LLMObs integrations for LLM/AI libraries in the Python tracer. Covers BaseLLMIntegration, stream handling, message extraction, token counting, tool call parsing, and VCR-based testing patterns. Triggers: "llmobs", "LLMObs", "BaseLLMIntegration", "llmobs_set_tags", "_llmobs_set_tags", "BaseStreamHandler", "submit_to_llmobs", "integration.trace", "LLM span", "VCR", "cassette", "anthropic", "openai", "google_genai", "claude_agent_sdk", "generative-ai", "LLM integration", "llmobs_enabled".4---56# dd-trace-py LLMObs Integrations78LLMObs integrations enable Datadog LLM Observability for AI/LLM libraries. They extract model inputs, outputs, token usage, and tool calls from traced spans. This skill should be used in addition to the `apm-integrations` skill.910## Two-Layer Architecture1112LLMObs integrations consist of two cooperating layers:13141. **Patch Layer** (`ddtrace/contrib/internal/{name}/patch.py`) -- wraps library functions. Standard request/response LLM integrations construct `LlmRequestEvent` and use `core.context_with_event()` so the LLM tracing subscriber owns span lifecycle and LLMObs tag extraction.152. **Integration Layer** (`ddtrace/llmobs/_integrations/{name}.py`) -- extends `BaseLLMIntegration`, implements `_set_base_span_tags()` and `_llmobs_set_tags()` to extract and set provider-specific messages, tools, metadata, and token metrics.1617Both layers must work together. The patch layer identifies the operation and passes request/response data through the event; the integration layer controls what data is extracted.1819## Active Patch Patterns2021- **Event-based request spans**: Use `LlmRequestEvent` with `core.context_with_event()` for new standard request/response LLM integrations. Anthropic is the canonical reference. This is the preferred pattern.22- **Direct integration spans**: Some existing or specialized integrations still call `integration.trace()` and `integration.llmobs_set_tags()` directly, especially for child spans, agent/tool spans, or integrations not yet migrated. Google GenAI, OpenAI tool spans, and Claude Agent SDK are useful references.2324## Key Files2526| Purpose | File |27|---------|------|28| Base LLM integration class | `ddtrace/llmobs/_integrations/base.py` (`BaseLLMIntegration`) |29| Stream handler base classes | `ddtrace/llmobs/_integrations/base_stream_handler.py` (`BaseStreamHandler`, `StreamHandler`, `AsyncStreamHandler`) |30| Shared utilities | `ddtrace/llmobs/_integrations/utils.py` |31| LLMObs annotation helper | `ddtrace/llmobs/_utils.py` (`_annotate_llmobs_span_data`) |32| LLMObs constants | `ddtrace/llmobs/_constants.py` |33| LLMObs types | `ddtrace/llmobs/types.py` (`Message`, `AudioPart`, `ToolCall`, `ToolResult`, `ToolDefinition`) |34| Integration registry | `ddtrace/llmobs/_integrations/__init__.py` |3536## Reference Integrations3738**Always read 1-2 references before writing or modifying LLMObs code.**3940| Provider | Patch File | LLMObs Integration | LLMObs Tests |41|----------|-----------|-------------------|--------------|42| **Anthropic** (canonical) | `ddtrace/contrib/internal/anthropic/patch.py` | `ddtrace/llmobs/_integrations/anthropic.py` | `tests/contrib/anthropic/test_anthropic_llmobs.py` |43| **Claude Agent SDK** (latest, agent pattern) | `ddtrace/contrib/internal/claude_agent_sdk/patch.py` | `ddtrace/llmobs/_integrations/claude_agent_sdk.py` | `tests/contrib/claude_agent_sdk/test_claude_agent_sdk_llmobs.py` |44| OpenAI | `ddtrace/contrib/internal/openai/patch.py` | `ddtrace/llmobs/_integrations/openai.py` | `tests/contrib/openai/test_openai_llmobs.py` |45| Google GenAI | `ddtrace/contrib/internal/google_genai/patch.py` | `ddtrace/llmobs/_integrations/google_genai.py` | `tests/contrib/google_genai/test_google_genai_llmobs.py` |4647Use **Anthropic** as the canonical reference for standard LLM integrations. Use **Claude Agent SDK** for agent-pattern integrations (agent spans, tool child spans, thinking blocks).4849## Abstract Methods to Implement5051Subclass `BaseLLMIntegration` and implement:5253### `_set_base_span_tags(span, **kwargs)`54Set provider-specific APM tags on the span (e.g., `{name}.request.model`).5556### `_llmobs_set_tags(span, args, kwargs, response, operation)`57Extract and annotate all LLMObs fields on the span:5859| Field | Description |60|-------------|-------------|61| `kind` | `"llm"` for LLM calls, `"agent"` for agent calls, `"tool"` for tool calls |62| `model_name` | Model identifier (e.g., `"claude-3-sonnet-20240229"`) |63| `model_provider` | Provider name (e.g., `"anthropic"`, `"openai"`) |64| `input_messages` | List of `Message` objects from request |65| `output_messages` | List of `Message` objects from response |66| `metadata` | Dict of sanitized request parameters (temperature, top_p, etc.), plus response-derived scalars such as `finish_reason` (see Response Metadata below) |67| `metrics` | Token usage dict with `INPUT_TOKENS_METRIC_KEY`, `OUTPUT_TOKENS_METRIC_KEY`, `TOTAL_TOKENS_METRIC_KEY` |68| `tool_definitions` | List of `ToolDefinition` objects if tools are passed |6970Fields are usually set via `_annotate_llmobs_span_data(...)`, not raw `span._set_ctx_items(...)`.7172### Response Metadata7374`metadata` is not request-only. Scalars the provider reports on the *response* belong there too, so75long as they are not already covered by `metrics` (token counts) or `output_messages`.7677The stop reason is the established case. Record it under the key `finish_reason` for every provider,78so one facet answers "why did generation stop" regardless of integration, and keep each provider's79own vocabulary as the value (`stop`/`length`/`content_filter`/`tool_calls` for the OpenAI family,80`end_turn`/`max_tokens`/`refusal`/`tool_use` for Anthropic). Read it from:8182| Provider | Source |83|---|---|84| openai/litellm chat + legacy completions | `choice.finish_reason` (per choice) |85| openai responses API | `incomplete_details.reason` |86| anthropic | `response.stop_reason`, falling back to `response.finish_reason` for the streamed dict the aggregator rebuilds |8788Rules:8990- **Merge, do not replace.** Build the request metadata first, then update it with the91 response-derived keys (`parameters.update(...)`), so response data never clobbers request params.92- **Omit the key when absent.** If the provider reports no reason, leave `finish_reason` out93 entirely rather than writing `None` or `""`.94- **Keep the value a single string.** When a request returns multiple choices (`n > 1`), comma-join95 the per-choice reasons in choice order (`"stop,length"`) instead of emitting a list, so the key96 never changes type. `_openai_finish_reason_metadata()` in `_integrations/utils.py` does this.97- **Guard on the response, not `span.error`.** A response can exist on an errored span (see the AI98 Guard case in `_integrations/anthropic.py`); gate on `response is not None`.99100Note two already-shipped integrations predate this key: bedrock and the claude-agent-sdk both write101`metadata["stop_reason"]`. Renaming those is a breaking change and has not been done — follow102`finish_reason` for new work.103104## Key Constraints105106- **`submit_to_llmobs=True`** must be set on `LlmRequestEvent` for event-based request spans or passed to `integration.trace()` for direct LLMObs spans107- **`ctx.dispatch_ended_event()`** must run on success and error paths for event-based patch wrappers108- **Streaming** must use `BaseStreamHandler`/`AsyncStreamHandler` -- never consume streams directly109- **Event-based patch wrappers** should not call `span.set_exc_info()`, `span.finish()`, or `integration.llmobs_set_tags()` directly; the tracing subscriber handles that when the event ends110- **Direct integration spans** must keep `integration.llmobs_set_tags()` and span lifecycle handling aligned with the closest current reference111- **Integration instance** must be stored on the module: `module._datadog_integration = MyLibIntegration(integration_config=config.mylib)`112113## Message Types114115```python116from ddtrace.llmobs.types import AudioPart, ImagePart, Message, ToolCall, ToolResult, ToolDefinition117118# Input/output messages119Message(content="text", role="user")120Message(content="response", role="assistant", tool_calls=[...])121122# Audio attachments in multimodal messages123AudioPart(mime_type="audio/wav", content="<base64-audio>")124Message(content="", role="user", audio_parts=[...])125126# Image attachments in multimodal messages127ImagePart(mime_type="image/png", content="<base64-image>")128Message(content="", role="user", image_parts=[...])129130# Tool calls (in output messages)131ToolCall(name="get_weather", arguments={"city": "NYC"}, tool_id="toolu_123", type="tool")132133# Tool results (in input messages)134ToolResult(result="72F sunny", tool_id="toolu_123", type="tool_result")135136# Tool definitions (from request parameters)137ToolDefinition(name="get_weather", description="...", schema={...})138```139140## Debugging Quick Tips141142- **No LLMObs spans** -- check `submit_to_llmobs=True`, `ctx.dispatch_ended_event()`, and `llmobs_enabled`143- **Wrong messages** -- check message extraction handles multi-part content and tool blocks144- **Wrong tokens** -- check field name mapping (libraries use different names for token counts)145- **Streaming broken** -- verify `BaseStreamHandler` subclass, check `finalize_stream()` dispatches the ended event or finishes direct-trace spans according to the reference pattern146- **DD_TRACE_DEBUG=true** to see patching activity and span creation147148See [Failure Modes](references/failure-modes.md) for detailed debugging guide.149150## Reference Files151152- [Implementation Guide](references/implementation-guide.md) -- LLM-specific steps (references apm-integrations guide for the full workflow)153- [Failure Modes](references/failure-modes.md) -- All 8 failure modes with causes and fixes154- [Testing Guide](references/testing-guide.md) -- LLMObs test patterns, VCR cassettes, suitespec