Skip to content

instrument_anthropic crashes streaming responses with TypeError when a text block has text=None (web_search/web_fetch citations) #1998

@sarth6

Description

@sarth6

Description

What I'm seeing

logfire.instrument_anthropic() aborts an in-progress streaming response with:

File ".../logfire/_internal/integrations/llm_providers/anthropic.py", line 288, in get_response_data
    return {'combined_chunk_content': ''.join(texts), 'chunk_count': self._chunk_count}
TypeError: sequence item 0: expected str instance, NoneType found

Root cause

AnthropicMessageStreamState.get_response_data joins the text of every text block in the accumulated message:

texts = [block.text for block in self._message.content if isinstance(block, (TextBlock, BetaTextBlock))]
return {'combined_chunk_content': ''.join(texts), ...}

TextBlock.text / BetaTextBlock.text are typed str, but the Anthropic SDK accumulates the streaming snapshot via construct_type(...) (anthropic/lib/streaming/_beta_messages.py), which bypasses validation. A text block that is opened (content_block_start) and only ever receives a citations_delta — no text_delta — keeps text=None. This happens routinely with the web_search / web_fetch builtin tools, whose responses carry citation-bearing text blocks. The result is ''.join([None, ...])TypeError.

Why this is more than a dropped span attribute

The join runs in record_streaming's __exit__, i.e. inside the instrumented stream iterator (llm_providers/llm_provider.py, LogfireInstrumentedAsyncStream.__stream__). So the exception propagates out of the async generator into application code and kills a response the model already produced successfully — it is not contained to telemetry. We saw 50+ aborted end-user chat responses in production from this (model claude-opus-4-6, all using web_search).

Expected

Instrumentation must never raise into the stream. Skipping None text (as the OpenAI path already does in OpenaiCompletionStreamState.record_chunk, which only appends truthy content) resolves it:

texts = [
    block.text
    for block in self._message.content
    if isinstance(block, (TextBlock, BetaTextBlock)) and block.text is not None
]

Still reproduces on main (4.35.0). I'd be happy to open a PR with a fix + test if that's helpful.

Note: convert_response_to_semconv (the 'latest' semconv path) also reads block.text and would store content=None for such a block — doesn't crash, but may warrant the same coalescing.

Python, Logfire & OS Versions, related packages

logfire="4.34.0"  # also reproduces on main (4.35.0)
anthropic="0.96.0"
platform="Linux (prod); also repro'd on macOS-26.0-arm64"
python="3.11"
[related_packages]
pydantic="2.13.4"
opentelemetry-api="1.39.1"
opentelemetry-sdk="1.39.1"
opentelemetry-semantic-conventions="0.60b1"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions