|
26 | 26 |
|
27 | 27 | if TYPE_CHECKING: |
28 | 28 | from openai.types.responses.response import Response |
| 29 | + from openai.types.responses.response_output_item import ResponseOutputItem |
29 | 30 | from openai.types.responses.response_usage import ResponseUsage |
30 | 31 | from openai.types.responses.tool_param import ToolParam |
31 | 32 |
|
|
71 | 72 | OutputMessage, |
72 | 73 | ReasoningPart, |
73 | 74 | Role, |
| 75 | + ServerToolCallPart, |
| 76 | + ServerToolCallResponsePart, |
74 | 77 | TextPart, |
75 | 78 | ) |
76 | 79 | from opentelemetry.util.genai.types import ( |
|
84 | 87 | OutputMessage = None |
85 | 88 | ReasoningPart = None |
86 | 89 | Role = None |
| 90 | + ServerToolCallPart = None |
| 91 | + ServerToolCallResponsePart = None |
87 | 92 | TextPart = None |
88 | 93 | ToolCall = None |
89 | 94 |
|
@@ -298,6 +303,97 @@ def _extract_reasoning_parts( |
298 | 303 | return parts |
299 | 304 |
|
300 | 305 |
|
| 306 | +_SERVER_TOOL_NAMES = { |
| 307 | + "code_interpreter_call": "code_interpreter", |
| 308 | + "file_search_call": "file_search", |
| 309 | + "image_generation_call": "image_generation", |
| 310 | + "mcp_call": "mcp", |
| 311 | + "mcp_list_tools": "mcp_list_tools", |
| 312 | + "tool_search_call": "tool_search", |
| 313 | + "web_search_call": "web_search", |
| 314 | +} |
| 315 | + |
| 316 | +_SERVER_TOOL_RESPONSE_NAMES = { |
| 317 | + "tool_search_output": "tool_search", |
| 318 | +} |
| 319 | + |
| 320 | + |
| 321 | +def _extract_server_tool_part( |
| 322 | + item: ResponseOutputItem, |
| 323 | +) -> tuple[ServerToolCallPart | ServerToolCallResponsePart, str] | None: |
| 324 | + if ServerToolCallPart is None or ServerToolCallResponsePart is None: |
| 325 | + return None |
| 326 | + |
| 327 | + item_type = item.type |
| 328 | + tool_name = _SERVER_TOOL_NAMES.get(item_type) |
| 329 | + response_name = _SERVER_TOOL_RESPONSE_NAMES.get(item_type) |
| 330 | + if tool_name is None and response_name is None: |
| 331 | + return None |
| 332 | + if ( |
| 333 | + item_type |
| 334 | + in ( |
| 335 | + "tool_search_call", |
| 336 | + "tool_search_output", |
| 337 | + ) |
| 338 | + and item.execution != "server" |
| 339 | + ): |
| 340 | + return None |
| 341 | + |
| 342 | + finish_reason = _server_tool_finish_reason(item) |
| 343 | + if finish_reason is None: |
| 344 | + return None |
| 345 | + |
| 346 | + payload = item.model_dump(exclude_none=True, mode="json") |
| 347 | + |
| 348 | + item_id = payload.pop("id", None) |
| 349 | + call_id = payload.pop("call_id", None) |
| 350 | + part_id = call_id if isinstance(call_id, str) else item_id |
| 351 | + payload.pop("type", None) |
| 352 | + name = payload.pop("name", None) |
| 353 | + canonical_name = tool_name or response_name |
| 354 | + if canonical_name is None: |
| 355 | + return None |
| 356 | + payload["type"] = canonical_name |
| 357 | + if response_name is not None: |
| 358 | + return ( |
| 359 | + ServerToolCallResponsePart( |
| 360 | + server_tool_call_response=payload, |
| 361 | + id=call_id if isinstance(call_id, str) else None, |
| 362 | + ), |
| 363 | + finish_reason, |
| 364 | + ) |
| 365 | + return ( |
| 366 | + ServerToolCallPart( |
| 367 | + name=name if isinstance(name, str) else canonical_name, |
| 368 | + server_tool_call=payload, |
| 369 | + id=part_id if isinstance(part_id, str) else None, |
| 370 | + ), |
| 371 | + finish_reason, |
| 372 | + ) |
| 373 | + |
| 374 | + |
| 375 | +def _server_tool_finish_reason(item: ResponseOutputItem) -> str | None: |
| 376 | + match item.type: |
| 377 | + case "mcp_list_tools": |
| 378 | + return "error" if item.error else "stop" |
| 379 | + case ( |
| 380 | + "code_interpreter_call" |
| 381 | + | "file_search_call" |
| 382 | + | "image_generation_call" |
| 383 | + | "mcp_call" |
| 384 | + | "tool_search_call" |
| 385 | + | "tool_search_output" |
| 386 | + | "web_search_call" |
| 387 | + ): |
| 388 | + return ( |
| 389 | + _finish_reason_from_status(item.status) |
| 390 | + if item.status is not None |
| 391 | + else None |
| 392 | + ) |
| 393 | + case _: |
| 394 | + return None |
| 395 | + |
| 396 | + |
301 | 397 | # `incomplete_details.reason` values that map onto a cross-provider finish |
302 | 398 | # reason; an unrecognized reason is reported as-is. |
303 | 399 | _INCOMPLETE_REASON_TO_FINISH_REASON = { |
@@ -448,6 +544,17 @@ def get_output_messages_from_response( |
448 | 544 | finish_reason=finish_reason, |
449 | 545 | ) |
450 | 546 | ) |
| 547 | + continue |
| 548 | + |
| 549 | + if server_tool := _extract_server_tool_part(item): |
| 550 | + server_tool_part, finish_reason = server_tool |
| 551 | + messages.append( |
| 552 | + OutputMessage( |
| 553 | + role=Role.ASSISTANT.value, |
| 554 | + parts=[server_tool_part], |
| 555 | + finish_reason=finish_reason, |
| 556 | + ) |
| 557 | + ) |
451 | 558 |
|
452 | 559 | return messages |
453 | 560 |
|
|
0 commit comments