Skip to content

Commit 204be3e

Browse files
authored
fix(openai): safe handle None tools value in responses api (#3447)
1 parent b5d265e commit 204be3e

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def parse_response(response: Union[LegacyAPIResponse, Response]) -> Response:
144144

145145
def get_tools_from_kwargs(kwargs: dict) -> list[ToolParam]:
146146
tools_input = kwargs.get("tools", [])
147+
# Handle case where tools key exists but value is None
148+
# (e.g., when wrappers like openai-guardrails pass tools=None)
149+
if tools_input is None:
150+
tools_input = []
147151
tools = []
148152

149153
for tool in tools_input:

packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from openai import AsyncOpenAI, OpenAI
44
from opentelemetry.instrumentation.openai.utils import is_reasoning_supported
5+
from opentelemetry.instrumentation.openai.v1.responses_wrappers import (
6+
get_tools_from_kwargs,
7+
)
58
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
69

710

@@ -384,3 +387,79 @@ async def test_responses_streaming_async_with_context_manager(
384387
assert span.attributes["gen_ai.prompt.0.role"] == "user"
385388
assert span.attributes["gen_ai.completion.0.role"] == "assistant"
386389
assert span.attributes["gen_ai.completion.0.content"] == full_text
390+
391+
392+
def test_get_tools_from_kwargs_with_none():
393+
"""Test that get_tools_from_kwargs handles None tools value correctly.
394+
395+
This reproduces the bug reported when openai-guardrails or other wrappers
396+
pass tools=None explicitly, causing TypeError: 'NoneType' object is not iterable.
397+
"""
398+
# Test case 1: tools key exists but value is None
399+
kwargs_with_none = {"tools": None, "model": "gpt-4", "input": "test"}
400+
result = get_tools_from_kwargs(kwargs_with_none)
401+
assert result == [], "Should return empty list when tools is None"
402+
403+
# Test case 2: tools key doesn't exist
404+
kwargs_without_tools = {"model": "gpt-4", "input": "test"}
405+
result = get_tools_from_kwargs(kwargs_without_tools)
406+
assert result == [], "Should return empty list when tools key is missing"
407+
408+
# Test case 3: tools is an empty list
409+
kwargs_empty_list = {"tools": [], "model": "gpt-4", "input": "test"}
410+
result = get_tools_from_kwargs(kwargs_empty_list)
411+
assert result == [], "Should return empty list when tools is empty list"
412+
413+
# Test case 4: tools with valid function tools
414+
kwargs_with_tools = {
415+
"tools": [{"type": "function", "name": "test_func", "description": "test"}],
416+
"model": "gpt-4",
417+
"input": "test",
418+
}
419+
result = get_tools_from_kwargs(kwargs_with_tools)
420+
assert len(result) == 1, "Should return list with one tool"
421+
422+
423+
def test_response_stream_init_with_none_tools():
424+
"""Test ResponseStream initialization when tools=None is in request_kwargs.
425+
426+
This reproduces the customer issue where openai-guardrails wraps the client
427+
and may pass tools=None, causing TypeError in ResponseStream.__init__.
428+
"""
429+
from unittest.mock import MagicMock
430+
from opentelemetry.instrumentation.openai.v1.responses_wrappers import (
431+
ResponseStream,
432+
)
433+
434+
# Create a mock response object
435+
mock_response = MagicMock()
436+
437+
# Create a mock span
438+
mock_span = MagicMock()
439+
440+
# Create a mock tracer
441+
mock_tracer = MagicMock()
442+
443+
# Test that ResponseStream can be initialized with tools=None
444+
# This should not raise TypeError: 'NoneType' object is not iterable
445+
request_kwargs_with_none_tools = {
446+
"model": "gpt-4",
447+
"input": "test",
448+
"tools": None, # This is what causes the bug
449+
"stream": True,
450+
}
451+
452+
# This should not raise an exception
453+
stream = ResponseStream(
454+
span=mock_span,
455+
response=mock_response,
456+
start_time=1234567890,
457+
request_kwargs=request_kwargs_with_none_tools,
458+
tracer=mock_tracer,
459+
)
460+
461+
# Verify the stream was created successfully
462+
assert stream is not None
463+
assert stream._traced_data is not None
464+
# Tools should be an empty list, not None
465+
assert stream._traced_data.tools == [] or stream._traced_data.tools is None

0 commit comments

Comments
 (0)