|
2 | 2 |
|
3 | 3 | from openai import AsyncOpenAI, OpenAI |
4 | 4 | from opentelemetry.instrumentation.openai.utils import is_reasoning_supported |
| 5 | +from opentelemetry.instrumentation.openai.v1.responses_wrappers import ( |
| 6 | + get_tools_from_kwargs, |
| 7 | +) |
5 | 8 | from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter |
6 | 9 |
|
7 | 10 |
|
@@ -384,3 +387,79 @@ async def test_responses_streaming_async_with_context_manager( |
384 | 387 | assert span.attributes["gen_ai.prompt.0.role"] == "user" |
385 | 388 | assert span.attributes["gen_ai.completion.0.role"] == "assistant" |
386 | 389 | 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