Skip to content

Commit 3a50021

Browse files
authored
fix(openai): dynamically import types for 1.99 (#3244)
1 parent feb570b commit 3a50021

File tree

6 files changed

+92
-28
lines changed

6 files changed

+92
-28
lines changed

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import List, Optional, Union
88

99
from opentelemetry import context as context_api
10+
import pydantic
1011
from opentelemetry.instrumentation.openai.shared import (
1112
OPENAI_LLM_USAGE_TOKEN_TYPES,
1213
_get_openai_base_url,
@@ -50,9 +51,6 @@
5051
from opentelemetry.trace.status import Status, StatusCode
5152
from wrapt import ObjectProxy
5253

53-
from openai.types.chat import ChatCompletionMessageToolCall
54-
from openai.types.chat.chat_completion_message import FunctionCall
55-
5654
SPAN_NAME = "openai.chat"
5755
PROMPT_FILTER_KEY = "prompt_filter_results"
5856
CONTENT_FILTER_KEY = "content_filter_results"
@@ -961,8 +959,10 @@ async def _abuild_from_streaming_response(
961959
span.end()
962960

963961

962+
# pydantic.BaseModel here is ChatCompletionMessageFunctionToolCall (as of openai 1.99.7)
963+
# but we keep to a parent type to support older versions
964964
def _parse_tool_calls(
965-
tool_calls: Optional[List[Union[dict, ChatCompletionMessageToolCall]]],
965+
tool_calls: Optional[List[Union[dict, pydantic.BaseModel]]],
966966
) -> Union[List[ToolCall], None]:
967967
"""
968968
Util to correctly parse the tool calls data from the OpenAI API to this module's
@@ -976,12 +976,11 @@ def _parse_tool_calls(
976976
for tool_call in tool_calls:
977977
tool_call_data = None
978978

979-
# Handle dict or ChatCompletionMessageToolCall
980979
if isinstance(tool_call, dict):
981980
tool_call_data = copy.deepcopy(tool_call)
982-
elif isinstance(tool_call, ChatCompletionMessageToolCall):
981+
elif _is_chat_message_function_tool_call(tool_call):
983982
tool_call_data = tool_call.model_dump()
984-
elif isinstance(tool_call, FunctionCall):
983+
elif _is_function_call(tool_call):
985984
function_call = tool_call.model_dump()
986985
tool_call_data = ToolCall(
987986
id="",
@@ -996,6 +995,34 @@ def _parse_tool_calls(
996995
return result
997996

998997

998+
def _is_chat_message_function_tool_call(model: Union[dict, pydantic.BaseModel]) -> bool:
999+
try:
1000+
from openai.types.chat.chat_completion_message_function_tool_call import (
1001+
ChatCompletionMessageFunctionToolCall,
1002+
)
1003+
1004+
return isinstance(model, ChatCompletionMessageFunctionToolCall)
1005+
except Exception:
1006+
try:
1007+
# Since OpenAI 1.99.3, ChatCompletionMessageToolCall is a Union,
1008+
# and the isinstance check will fail. This is fine, because in all
1009+
# those versions, the check above will succeed.
1010+
from openai.types.chat.chat_completion_message_tool_call import (
1011+
ChatCompletionMessageToolCall,
1012+
)
1013+
return isinstance(model, ChatCompletionMessageToolCall)
1014+
except Exception:
1015+
return False
1016+
1017+
1018+
def _is_function_call(model: Union[dict, pydantic.BaseModel]) -> bool:
1019+
try:
1020+
from openai.types.chat.chat_completion_message import FunctionCall
1021+
return isinstance(model, FunctionCall)
1022+
except Exception:
1023+
return False
1024+
1025+
9991026
@singledispatch
10001027
def _parse_choice_event(choice) -> ChoiceEvent:
10011028
has_message = choice.message is not None

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,14 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
447447
merged_tools = existing_data.get("tools", []) + request_tools
448448

449449
try:
450+
parsed_response_output_text = None
451+
if hasattr(parsed_response, "output_text"):
452+
parsed_response_output_text = parsed_response.output_text
453+
else:
454+
try:
455+
parsed_response_output_text = parsed_response.output[0].content[0].text
456+
except Exception:
457+
pass
450458
traced_data = TracedData(
451459
start_time=existing_data.get("start_time", start_time),
452460
response_id=parsed_response.id,
@@ -456,7 +464,7 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
456464
output_blocks={block.id: block for block in parsed_response.output}
457465
| existing_data.get("output_blocks", {}),
458466
usage=existing_data.get("usage", parsed_response.usage),
459-
output_text=existing_data.get("output_text", parsed_response.output_text),
467+
output_text=existing_data.get("output_text", parsed_response_output_text),
460468
request_model=existing_data.get("request_model", kwargs.get("model")),
461469
response_model=existing_data.get("response_model", parsed_response.model),
462470
)
@@ -541,6 +549,15 @@ async def async_responses_get_or_create_wrapper(
541549
merged_tools = existing_data.get("tools", []) + request_tools
542550

543551
try:
552+
parsed_response_output_text = None
553+
if hasattr(parsed_response, "output_text"):
554+
parsed_response_output_text = parsed_response.output_text
555+
else:
556+
try:
557+
parsed_response_output_text = parsed_response.output[0].content[0].text
558+
except Exception:
559+
pass
560+
544561
traced_data = TracedData(
545562
start_time=existing_data.get("start_time", start_time),
546563
response_id=parsed_response.id,
@@ -550,7 +567,7 @@ async def async_responses_get_or_create_wrapper(
550567
output_blocks={block.id: block for block in parsed_response.output}
551568
| existing_data.get("output_blocks", {}),
552569
usage=existing_data.get("usage", parsed_response.usage),
553-
output_text=existing_data.get("output_text", parsed_response.output_text),
570+
output_text=existing_data.get("output_text", parsed_response_output_text),
554571
request_model=existing_data.get("request_model", kwargs.get("model")),
555572
response_model=existing_data.get("response_model", parsed_response.model),
556573
)

packages/opentelemetry-instrumentation-openai/poetry.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opentelemetry-instrumentation-openai/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pytest = "^8.2.2"
3838
pytest-sugar = "1.0.0"
3939
vcrpy = "^6.0.1"
4040
pytest-recording = "^0.13.1"
41-
openai = { extras = ["datalib"], version = ">=1.66.0" }
41+
openai = { extras = ["datalib"], version = "1.99.7" }
4242
opentelemetry-sdk = "^1.27.0"
4343
pytest-asyncio = "^0.23.7"
4444
requests = "^2.31.0"

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import httpx
55
import pytest
66
from openai.types.chat.chat_completion_message_tool_call import (
7-
ChatCompletionMessageToolCall,
8-
Function,
7+
ChatCompletionMessageFunctionToolCall,
98
)
109
from opentelemetry.sdk._logs import LogData
1110
from opentelemetry.semconv._incubating.attributes import (
@@ -375,13 +374,21 @@ def test_chat_tool_calls_with_events_with_no_content(
375374
def test_chat_pydantic_based_tool_calls(
376375
instrument_legacy, span_exporter, log_exporter, openai_client
377376
):
377+
try:
378+
from openai.types.chat.chat_completion_message_function_tool_call import Function
379+
except (ImportError, ModuleNotFoundError, AttributeError):
380+
try:
381+
from openai.types.chat.chat_completion_message_tool_call import Function
382+
except (ImportError, ModuleNotFoundError, AttributeError):
383+
pytest.skip("Could not import Function. Please check your OpenAI version. Skipping test.")
384+
378385
openai_client.chat.completions.create(
379386
model="gpt-3.5-turbo",
380387
messages=[
381388
{
382389
"role": "assistant",
383390
"tool_calls": [
384-
ChatCompletionMessageToolCall(
391+
ChatCompletionMessageFunctionToolCall(
385392
id="1",
386393
type="function",
387394
function=Function(
@@ -440,13 +447,21 @@ def test_chat_pydantic_based_tool_calls(
440447
def test_chat_pydantic_based_tool_calls_with_events_with_content(
441448
instrument_with_content, span_exporter, log_exporter, openai_client
442449
):
450+
try:
451+
from openai.types.chat.chat_completion_message_function_tool_call import Function
452+
except (ImportError, ModuleNotFoundError, AttributeError):
453+
try:
454+
from openai.types.chat.chat_completion_message_tool_call import Function
455+
except (ImportError, ModuleNotFoundError, AttributeError):
456+
pytest.skip("Could not import Function. Please check your OpenAI version. Skipping test.")
457+
443458
openai_client.chat.completions.create(
444459
model="gpt-3.5-turbo",
445460
messages=[
446461
{
447462
"role": "assistant",
448463
"tool_calls": [
449-
ChatCompletionMessageToolCall(
464+
ChatCompletionMessageFunctionToolCall(
450465
id="1",
451466
type="function",
452467
function=Function(
@@ -518,13 +533,21 @@ def test_chat_pydantic_based_tool_calls_with_events_with_content(
518533
def test_chat_pydantic_based_tool_calls_with_events_with_no_content(
519534
instrument_with_no_content, span_exporter, log_exporter, openai_client
520535
):
536+
try:
537+
from openai.types.chat.chat_completion_message_function_tool_call import Function
538+
except (ImportError, ModuleNotFoundError, AttributeError):
539+
try:
540+
from openai.types.chat.chat_completion_message_tool_call import Function
541+
except (ImportError, ModuleNotFoundError, AttributeError):
542+
pytest.skip("Could not import Function. Please check your OpenAI version. Skipping test.")
543+
521544
openai_client.chat.completions.create(
522545
model="gpt-3.5-turbo",
523546
messages=[
524547
{
525548
"role": "assistant",
526549
"tool_calls": [
527-
ChatCompletionMessageToolCall(
550+
ChatCompletionMessageFunctionToolCall(
528551
id="1",
529552
type="function",
530553
function=Function(
@@ -951,7 +974,6 @@ async def test_chat_async_streaming_with_events_with_no_content(
951974

952975

953976
@pytest.mark.vcr
954-
@pytest.mark.asyncio
955977
def test_with_asyncio_run(
956978
instrument_legacy, span_exporter, log_exporter, async_openai_client
957979
):
@@ -981,7 +1003,6 @@ def test_with_asyncio_run(
9811003

9821004

9831005
@pytest.mark.vcr
984-
@pytest.mark.asyncio
9851006
def test_with_asyncio_run_with_events_with_content(
9861007
instrument_with_content, span_exporter, log_exporter, async_openai_client
9871008
):
@@ -1030,7 +1051,6 @@ def test_with_asyncio_run_with_events_with_content(
10301051

10311052

10321053
@pytest.mark.vcr
1033-
@pytest.mark.asyncio
10341054
def test_with_asyncio_run_with_events_with_no_content(
10351055
instrument_with_no_content, span_exporter, log_exporter, async_openai_client
10361056
):

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_responses(instrument_legacy, span_exporter: InMemorySpanExporter, opena
2323
span.attributes["gen_ai.prompt.0.content"] == "What is the capital of France?"
2424
)
2525
assert span.attributes["gen_ai.prompt.0.role"] == "user"
26-
assert span.attributes["gen_ai.completion.0.content"] == response.output_text
26+
assert span.attributes["gen_ai.completion.0.content"] == response.output[0].content[0].text
2727
assert span.attributes["gen_ai.completion.0.role"] == "assistant"
2828

2929

@@ -46,7 +46,7 @@ def test_responses_with_input_history(instrument_legacy, span_exporter: InMemory
4646
"content": [
4747
{
4848
"type": "output_text",
49-
"text": first_response.output_text,
49+
"text": first_response.output[0].content[0].text,
5050
}
5151
],
5252
},
@@ -69,7 +69,7 @@ def test_responses_with_input_history(instrument_legacy, span_exporter: InMemory
6969
assert json.loads(span.attributes["gen_ai.prompt.1.content"]) == [
7070
{
7171
"type": "output_text",
72-
"text": first_response.output_text,
72+
"text": first_response.output[0].content[0].text,
7373
}
7474
]
7575
assert span.attributes["gen_ai.prompt.1.role"] == "assistant"
@@ -78,7 +78,7 @@ def test_responses_with_input_history(instrument_legacy, span_exporter: InMemory
7878
== "Can you explain why you chose that word?"
7979
)
8080
assert span.attributes["gen_ai.prompt.2.role"] == "user"
81-
assert span.attributes["gen_ai.completion.0.content"] == response.output_text
81+
assert span.attributes["gen_ai.completion.0.content"] == response.output[0].content[0].text
8282
assert span.attributes["gen_ai.completion.0.role"] == "assistant"
8383

8484

0 commit comments

Comments
 (0)