Skip to content

openai-v2: ChoiceBuffer crashes on streaming tool-call deltas with arguments=None #4344

@iamemilio

Description

@iamemilio

Describe your environment

OS: macOS (also reproduced on Linux)
Python version: Python 3.12+
Package version: opentelemetry-instrumentation-openai-v2 v2.3b0

What happened?

ChoiceBuffer.append_tool_call() unconditionally appends tool_call.function.arguments to its internal buffer list. During BaseStreamWrapper.cleanup(), the buffer is serialized with "".join(...). If any provider sends arguments=None on a tool-call delta chunk (instead of arguments=""), this crashes with TypeError: sequence item 0: expected str instance, NoneType found.

The OpenAI API sends arguments="" on the first tool-call delta, but many OpenAI-compatible providers (vLLM, TGI, etc.) send arguments=None. This is a known pattern across the ecosystem — see vllm#9693, pydantic-ai#1654, gptel#1283.

In llama-stack, this kills the stream mid-flight and causes silent failures (no conversation storage, no assistant message). We've added a workaround normalizing arguments=None"" before yielding chunks (see llama-stack#5200), but the fix belongs here.

Steps to Reproduce

from opentelemetry.instrumentation.openai_v2.patch import ChoiceBuffer
from openai.types.chat.chat_completion_chunk import ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction

buf = ChoiceBuffer(0)
buf.append_tool_call(ChoiceDeltaToolCall(
    index=0,
    id="call_1",
    type="function",
    function=ChoiceDeltaToolCallFunction(name="get_weather", arguments=None),
))
buf.append_tool_call(ChoiceDeltaToolCall(
    index=0,
    function=ChoiceDeltaToolCallFunction(arguments='{"city": "NYC"}'),
))

# This crashes:
"".join(buf.tool_call_buffers[0].arguments)

Expected Result

ChoiceBuffer should handle arguments=None gracefully — either skip it or coerce it to "" before appending. The stream should complete without error.

Actual Result

TypeError: sequence item 0: expected str instance, NoneType found

The stream is killed, and any downstream consumer (e.g., llama-stack's StreamingResponseOrchestrator) receives an unexpected exception instead of StopAsyncIteration.

Additional context

A suggested fix is a one-line null check in ChoiceBuffer.append_tool_call():

# Before:
self.tool_call_buffers[index].arguments.append(tool_call.function.arguments)

# After:
if tool_call.function.arguments is not None:
    self.tool_call_buffers[index].arguments.append(tool_call.function.arguments)

This also affects the Responses API instrumentation work (#3436, #4166, #4280, #4337) since it uses the same BaseStreamWrapper code path.

Would you like to implement a fix?

No

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