Skip to content

[BUG]: LLMObs: errors creating openai spans - "missing span kind in span context" #19760

Description

@zhammer

Tracer Version(s)

4.9.7

Python Version(s)

3.13

Pip Version(s)

uv==0.8.24 (not pip)

Bug Report

we're seeing a bunch of error logs with span creation and field extration on llmobs for instrumented openai streaming calls. happy to give more info, for now will dump from claude.


Summary

On the OpenAI Responses API with stream=True, an AttributeError in _compute_completion_tokens causes the openai.request LLMObs span to be dropped entirely — its children are orphaned and its token metrics are lost.

Error extracting LLMObs fields for span <Span(... name=openai.request)>, likely due to malformed data
AttributeError: 'tuple' object has no attribute 'get'

Error preparing LLMObs span event for span <Span(... name=openai.request)>, missing span kind in span context.

Root cause

Two independent defects compound.

1. _process_finished_stream doesn't unwrap chunk storage for the response operation.

Streamed chunks accumulate into a defaultdict(list) (contrib/internal/openai/utils.py), but the unwrap is gated on the operation type:

# contrib/internal/openai/utils.py::_process_finished_stream
if isinstance(streamed_chunks, dict) and operation_type != "response":
    streamed_chunks = streamed_chunks.values()

For operation_type == "response" it stays a dict. In _extract_llmobs_metrics_tags, the isinstance(resp, list) guard above the usage fast path is then False, so it falls through to _compute_completion_tokens(resp), which iterates the dict and gets keys:

# llmobs/_integrations/utils.py::_compute_completion_tokens
for choice in completions_or_messages:
    content = choice.get("content", "") or choice.get("text", "")   # AttributeError

This is only reached when the terminal usage chunk never arrived — i.e. a truncated stream. In our traffic that is overwhelmingly a client disconnect (GeneratorExit / TLS read abort mid-stream), which is why the usage early-return normally masks it.

2. _llmobs_set_tags computes metrics before it records the span kind, so the raise loses the span.

# llmobs/_integrations/openai.py::_llmobs_set_tags
106    metrics = self._extract_llmobs_metrics_tags(span, response, span_kind, kwargs)   # raises
...
109    # Set kind before helpers so that input/output messages are routed correctly
110    _annotate_llmobs_span_data(span, name=span_name, kind=span_kind, ...)            # never runs

base.py::llmobs_set_tags swallows the exception, then _llmobs.py::_prepare_llmobs_span_data finds no kind, logs missing span kind in span context, and _on_span_finish drops the span and scrubs its meta_struct.

The comment on line 109 states the intended invariant, but one call was left ahead of the assignment — so the ordering it describes isn't actually enforced. This is the same class of failure as #18840 (which fixed it for google_adk by moving the kind assignment ahead of extraction); the openai integration has that shape already, but this one call defeats it.

Impact

  • LLMObs llm spans silently disappear for aborted/truncated streamed Responses calls.
  • Child spans are orphaned; token metrics are lost, so cost and latency dashboards undercount, biased toward disconnects.
  • High-volume ERROR logs with full tracebacks.

Suggested fix

Either alone stops the drops; both together seem preferable:

  1. Unwrap chunk storage for operation_type == "response" as well, or make _compute_completion_tokens skip non-mapping entries.
  2. Move _extract_llmobs_metrics_tags after the _annotate_llmobs_span_data kind assignment (setting metrics separately), so any future extraction failure degrades to missing metrics rather than a lost span — enforcing the invariant fix(llmobs): keep google_adk agent span when a Gemini part is unhandled [backport 4.10] #18840 established.

Related: #14030 fixed the same function for a 'str' variant by making it dict/object tolerant, but did not address the dict-not-unwrapped case.

Environment

  • ddtrace 4.9.7 (pinned ddtrace>=4.9.0,<4.10)
  • openai Responses API, stream=True, async client
  • Python 3.13, Linux aarch64
  • Installed with uv 0.8.24 (no pip in the environment)
  • LLM Observability enabled with integrations_enabled=True, agent-proxy mode

Reproduction Code

No response

Error Logs

No response

Libraries in Use

No response

Operating System

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions