c56bef871b
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
1604 lines
65 KiB
Python
1604 lines
65 KiB
Python
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from unittest.mock import ANY
|
|
|
|
import pytest
|
|
from openai.types import Reasoning, ResponseFormatText
|
|
from openai.types.responses import (
|
|
FunctionTool,
|
|
Response,
|
|
ResponseCompletedEvent,
|
|
ResponseContentPartAddedEvent,
|
|
ResponseContentPartDoneEvent,
|
|
ResponseCreatedEvent,
|
|
ResponseFunctionCallArgumentsDeltaEvent,
|
|
ResponseFunctionCallArgumentsDoneEvent,
|
|
ResponseFunctionToolCall,
|
|
ResponseInProgressEvent,
|
|
ResponseOutputItemAddedEvent,
|
|
ResponseOutputItemDoneEvent,
|
|
ResponseOutputMessage,
|
|
ResponseOutputText,
|
|
ResponseReasoningItem,
|
|
ResponseTextConfig,
|
|
ResponseTextDeltaEvent,
|
|
ResponseTextDoneEvent,
|
|
ResponseUsage,
|
|
)
|
|
from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
|
|
|
|
from haystack.components.generators.chat.openai_responses import (
|
|
_convert_chat_message_to_responses_api_format,
|
|
_convert_response_chunk_to_streaming_chunk,
|
|
_convert_streaming_chunks_to_chat_message,
|
|
)
|
|
from haystack.dataclasses import (
|
|
ChatMessage,
|
|
ChatRole,
|
|
FileContent,
|
|
ImageContent,
|
|
ReasoningContent,
|
|
StreamingChunk,
|
|
TextContent,
|
|
ToolCall,
|
|
ToolCallDelta,
|
|
ToolCallResult,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def openai_responses_streaming_chunks_with_tool_call():
|
|
return [
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [],
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "useful to determine the weather in a given location",
|
|
}
|
|
],
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"usage": None,
|
|
},
|
|
"sequence_number": 0,
|
|
"type": "response.created",
|
|
},
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
start=True,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=1,
|
|
tool_calls=[
|
|
ToolCallDelta(
|
|
index=1,
|
|
tool_name="weather",
|
|
arguments=None,
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={
|
|
"arguments": "",
|
|
"call_id": "call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
"id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"name": "weather",
|
|
"status": "in_progress",
|
|
"type": "function_call",
|
|
},
|
|
)
|
|
],
|
|
start=True,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=1,
|
|
tool_calls=[
|
|
ToolCallDelta(
|
|
index=1,
|
|
tool_name=None,
|
|
arguments='{"city":"Paris"}',
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={
|
|
"item_id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"output_index": 1,
|
|
"sequence_number": 5,
|
|
"type": "response.function_call_arguments.delta",
|
|
"obfuscation": "PySUcQ59ZZRkOm",
|
|
},
|
|
)
|
|
],
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"arguments": '{"city":"Paris"}',
|
|
"item_id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"name": "weather",
|
|
"output_index": 1,
|
|
"sequence_number": 10,
|
|
"type": "response.function_call_arguments.done",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [
|
|
{
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
{
|
|
"arguments": '{"city":"Paris"}',
|
|
"call_id": "call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
"name": "weather",
|
|
"type": "function_call",
|
|
"id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"status": "completed",
|
|
},
|
|
],
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "useful to determine the weather in a given location",
|
|
}
|
|
],
|
|
"top_p": 1.0,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"usage": {
|
|
"input_tokens": 62,
|
|
"input_tokens_details": {"cached_tokens": 0},
|
|
"output_tokens": 83,
|
|
"output_tokens_details": {"reasoning_tokens": 64},
|
|
"total_tokens": 145,
|
|
},
|
|
"store": True,
|
|
},
|
|
"sequence_number": 12,
|
|
"type": "response.completed",
|
|
},
|
|
finish_reason="tool_calls",
|
|
),
|
|
]
|
|
|
|
|
|
class TestConversionToStreamingChunks:
|
|
def test_convert_streaming_chunks_to_chat_message_with_tool_call_empty_reasoning(
|
|
self, openai_responses_streaming_chunks_with_tool_call
|
|
):
|
|
chat_message = _convert_streaming_chunks_to_chat_message(openai_responses_streaming_chunks_with_tool_call)
|
|
assert chat_message == ChatMessage(
|
|
_role="assistant",
|
|
_content=[
|
|
ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
ToolCall(
|
|
tool_name="weather",
|
|
arguments={"city": "Paris"},
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={"call_id": "call_OZZXFm7SLb4F3Xg8a9XVVCvv"},
|
|
),
|
|
],
|
|
_name=None,
|
|
_meta={
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "useful to determine the weather in a given location",
|
|
}
|
|
],
|
|
"top_p": 1.0,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"usage": {
|
|
"input_tokens": 62,
|
|
"input_tokens_details": {"cached_tokens": 0},
|
|
"output_tokens": 83,
|
|
"output_tokens_details": {"reasoning_tokens": 64},
|
|
"total_tokens": 145,
|
|
},
|
|
"store": True,
|
|
},
|
|
)
|
|
|
|
def test_convert_only_text(self):
|
|
openai_chunks = [
|
|
ResponseCreatedEvent(
|
|
response=Response(
|
|
id="resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
created_at=1762418678.0,
|
|
metadata={},
|
|
model="gpt-5-mini-2025-08-07",
|
|
object="response",
|
|
output=[],
|
|
parallel_tool_calls=True,
|
|
temperature=1.0,
|
|
tool_choice="auto",
|
|
tools=[],
|
|
top_p=1.0,
|
|
background=False,
|
|
reasoning=Reasoning(effort="medium", generate_summary=None, summary=None),
|
|
service_tier="auto",
|
|
status="in_progress",
|
|
text=ResponseTextConfig(format=ResponseFormatText(type="text"), verbosity="medium"),
|
|
top_logprobs=0,
|
|
truncation="disabled",
|
|
prompt_cache_retention=None,
|
|
store=True,
|
|
),
|
|
sequence_number=0,
|
|
type="response.created",
|
|
),
|
|
ResponseInProgressEvent(
|
|
response=Response(
|
|
id="resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
created_at=1762418678.0,
|
|
metadata={},
|
|
model="gpt-5-mini-2025-08-07",
|
|
object="response",
|
|
output=[],
|
|
parallel_tool_calls=True,
|
|
temperature=1.0,
|
|
tool_choice="auto",
|
|
tools=[],
|
|
top_p=1.0,
|
|
background=False,
|
|
reasoning=Reasoning(effort="medium", generate_summary=None, summary=None),
|
|
service_tier="auto",
|
|
status="in_progress",
|
|
text=ResponseTextConfig(format=ResponseFormatText(type="text"), verbosity="medium"),
|
|
top_logprobs=0,
|
|
truncation="disabled",
|
|
prompt_cache_retention=None,
|
|
store=True,
|
|
),
|
|
sequence_number=1,
|
|
type="response.in_progress",
|
|
),
|
|
ResponseOutputItemAddedEvent(
|
|
item=ResponseReasoningItem(
|
|
id="rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b", summary=[], type="reasoning"
|
|
),
|
|
output_index=0,
|
|
sequence_number=2,
|
|
type="response.output_item.added",
|
|
),
|
|
ResponseOutputItemDoneEvent(
|
|
item=ResponseReasoningItem(
|
|
id="rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b", summary=[], type="reasoning"
|
|
),
|
|
output_index=0,
|
|
sequence_number=3,
|
|
type="response.output_item.done",
|
|
),
|
|
ResponseOutputItemAddedEvent(
|
|
item=ResponseOutputMessage(
|
|
id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
content=[],
|
|
role="assistant",
|
|
status="in_progress",
|
|
type="message",
|
|
),
|
|
output_index=1,
|
|
sequence_number=4,
|
|
type="response.output_item.added",
|
|
),
|
|
ResponseContentPartAddedEvent(
|
|
content_index=0,
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
output_index=1,
|
|
part=ResponseOutputText(annotations=[], text="", type="output_text", logprobs=[]),
|
|
sequence_number=5,
|
|
type="response.content_part.added",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta="Germany",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=6,
|
|
type="response.output_text.delta",
|
|
obfuscation="EV5gCoyiD",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta=":",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=7,
|
|
type="response.output_text.delta",
|
|
obfuscation="EkdNXp1EE2Cgj8z",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta=" Berlin",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=8,
|
|
type="response.output_text.delta",
|
|
obfuscation="1eS0q9aye",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta="\n",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=9,
|
|
type="response.output_text.delta",
|
|
obfuscation="H9Ict3F41DwGS4a",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta="France",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=10,
|
|
type="response.output_text.delta",
|
|
obfuscation="4vxrblWURx",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta=":",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=11,
|
|
type="response.output_text.delta",
|
|
obfuscation="B1CMJsNGhhqIz5K",
|
|
),
|
|
ResponseTextDeltaEvent(
|
|
content_index=0,
|
|
delta=" Paris",
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=12,
|
|
type="response.output_text.delta",
|
|
obfuscation="ojbz89bS7j",
|
|
),
|
|
ResponseTextDoneEvent(
|
|
content_index=0,
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
logprobs=[],
|
|
output_index=1,
|
|
sequence_number=13,
|
|
text="Germany: Berlin\nFrance: Paris",
|
|
type="response.output_text.done",
|
|
),
|
|
ResponseContentPartDoneEvent(
|
|
content_index=0,
|
|
item_id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
output_index=1,
|
|
part=ResponseOutputText(
|
|
annotations=[], text="Germany: Berlin\nFrance: Paris", type="output_text", logprobs=[]
|
|
),
|
|
sequence_number=14,
|
|
type="response.content_part.done",
|
|
),
|
|
ResponseOutputItemDoneEvent(
|
|
item=ResponseOutputMessage(
|
|
id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
content=[
|
|
ResponseOutputText(
|
|
annotations=[], text="Germany: Berlin\nFrance: Paris", type="output_text", logprobs=[]
|
|
)
|
|
],
|
|
role="assistant",
|
|
status="completed",
|
|
type="message",
|
|
),
|
|
output_index=1,
|
|
sequence_number=15,
|
|
type="response.output_item.done",
|
|
),
|
|
ResponseCompletedEvent(
|
|
response=Response(
|
|
id="resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
created_at=1762418678.0,
|
|
error=None,
|
|
incomplete_details=None,
|
|
instructions=None,
|
|
metadata={},
|
|
model="gpt-5-mini-2025-08-07",
|
|
object="response",
|
|
output=[
|
|
ResponseReasoningItem(
|
|
id="rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b", summary=[], type="reasoning"
|
|
),
|
|
ResponseOutputMessage(
|
|
id="msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
content=[
|
|
ResponseOutputText(
|
|
annotations=[],
|
|
text="Germany: Berlin\nFrance: Paris",
|
|
type="output_text",
|
|
logprobs=[],
|
|
)
|
|
],
|
|
role="assistant",
|
|
status="completed",
|
|
type="message",
|
|
),
|
|
],
|
|
parallel_tool_calls=True,
|
|
temperature=1.0,
|
|
tool_choice="auto",
|
|
tools=[],
|
|
top_p=1.0,
|
|
background=False,
|
|
reasoning=Reasoning(effort="medium", generate_summary=None, summary=None),
|
|
safety_identifier=None,
|
|
service_tier="default",
|
|
status="completed",
|
|
text=ResponseTextConfig(format=ResponseFormatText(type="text"), verbosity="medium"),
|
|
top_logprobs=0,
|
|
truncation="disabled",
|
|
usage=ResponseUsage(
|
|
input_tokens=15,
|
|
input_tokens_details=InputTokensDetails(cached_tokens=0),
|
|
output_tokens=77,
|
|
output_tokens_details=OutputTokensDetails(reasoning_tokens=64),
|
|
total_tokens=92,
|
|
),
|
|
prompt_cache_retention=None,
|
|
store=True,
|
|
),
|
|
sequence_number=16,
|
|
type="response.completed",
|
|
),
|
|
]
|
|
streaming_chunks = []
|
|
for chunk in openai_chunks:
|
|
streaming_chunk = _convert_response_chunk_to_streaming_chunk(chunk, previous_chunks=streaming_chunks)
|
|
streaming_chunks.append(streaming_chunk)
|
|
|
|
assert streaming_chunks == [
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
"created_at": 1762418678.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [],
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [],
|
|
"top_p": 1.0,
|
|
"background": False,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"service_tier": "auto",
|
|
"status": "in_progress",
|
|
"text": {"format": {"type": "text"}, "verbosity": "medium"},
|
|
"top_logprobs": 0,
|
|
"truncation": "disabled",
|
|
"prompt_cache_retention": None,
|
|
"store": True,
|
|
},
|
|
"sequence_number": 0,
|
|
"type": "response.created",
|
|
},
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
"created_at": 1762418678.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [],
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [],
|
|
"top_p": 1.0,
|
|
"background": False,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"service_tier": "auto",
|
|
"status": "in_progress",
|
|
"text": {"format": {"type": "text"}, "verbosity": "medium"},
|
|
"top_logprobs": 0,
|
|
"truncation": "disabled",
|
|
"prompt_cache_retention": None,
|
|
"store": True,
|
|
},
|
|
"sequence_number": 1,
|
|
"type": "response.in_progress",
|
|
},
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
start=True,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"item": {
|
|
"id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"content": [],
|
|
"role": "assistant",
|
|
"status": "in_progress",
|
|
"type": "message",
|
|
},
|
|
"output_index": 1,
|
|
"sequence_number": 4,
|
|
"type": "response.output_item.added",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"content_index": 0,
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"output_index": 1,
|
|
"part": {"annotations": [], "text": "", "type": "output_text", "logprobs": []},
|
|
"sequence_number": 5,
|
|
"type": "response.content_part.added",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="Germany",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": "Germany",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 6,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "EV5gCoyiD",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
start=True,
|
|
),
|
|
StreamingChunk(
|
|
content=":",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": ":",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 7,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "EkdNXp1EE2Cgj8z",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content=" Berlin",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": " Berlin",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 8,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "1eS0q9aye",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="\n",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": "\n",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 9,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "H9Ict3F41DwGS4a",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="France",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": "France",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 10,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "4vxrblWURx",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content=":",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": ":",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 11,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "B1CMJsNGhhqIz5K",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content=" Paris",
|
|
meta={
|
|
"content_index": 0,
|
|
"delta": " Paris",
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 12,
|
|
"type": "response.output_text.delta",
|
|
"obfuscation": "ojbz89bS7j",
|
|
"received_at": ANY,
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"content_index": 0,
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"logprobs": [],
|
|
"output_index": 1,
|
|
"sequence_number": 13,
|
|
"text": "Germany: Berlin\nFrance: Paris",
|
|
"type": "response.output_text.done",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"content_index": 0,
|
|
"item_id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"output_index": 1,
|
|
"part": {
|
|
"annotations": [],
|
|
"text": "Germany: Berlin\nFrance: Paris",
|
|
"type": "output_text",
|
|
"logprobs": [],
|
|
},
|
|
"sequence_number": 14,
|
|
"type": "response.content_part.done",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"item": {
|
|
"id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"content": [
|
|
{
|
|
"annotations": [],
|
|
"text": "Germany: Berlin\nFrance: Paris",
|
|
"type": "output_text",
|
|
"logprobs": [],
|
|
}
|
|
],
|
|
"role": "assistant",
|
|
"status": "completed",
|
|
"type": "message",
|
|
},
|
|
"output_index": 1,
|
|
"sequence_number": 15,
|
|
"type": "response.output_item.done",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_0a8811e62a95217b00690c5ff62c14819596eae387d116f285",
|
|
"created_at": 1762418678.0,
|
|
"error": None,
|
|
"incomplete_details": None,
|
|
"instructions": None,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [
|
|
{
|
|
"id": "rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
{
|
|
"id": "msg_0a8811e62a95217b00690c5ff88f6c8195b037e57d327a1ee0",
|
|
"content": [
|
|
{
|
|
"annotations": [],
|
|
"text": "Germany: Berlin\nFrance: Paris",
|
|
"type": "output_text",
|
|
"logprobs": [],
|
|
}
|
|
],
|
|
"role": "assistant",
|
|
"status": "completed",
|
|
"type": "message",
|
|
},
|
|
],
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [],
|
|
"top_p": 1.0,
|
|
"background": False,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"safety_identifier": None,
|
|
"service_tier": "default",
|
|
"status": "completed",
|
|
"text": {"format": {"type": "text"}, "verbosity": "medium"},
|
|
"top_logprobs": 0,
|
|
"truncation": "disabled",
|
|
"usage": {
|
|
"input_tokens": 15,
|
|
"input_tokens_details": {"cached_tokens": 0},
|
|
"output_tokens": 77,
|
|
"output_tokens_details": {"reasoning_tokens": 64},
|
|
"total_tokens": 92,
|
|
},
|
|
"prompt_cache_retention": None,
|
|
"store": True,
|
|
},
|
|
"sequence_number": 16,
|
|
"type": "response.completed",
|
|
},
|
|
finish_reason="stop",
|
|
),
|
|
]
|
|
|
|
def test_convert_only_function_call(self):
|
|
chunks = [
|
|
ResponseCreatedEvent(
|
|
response=Response(
|
|
id="resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
created_at=1761907188.0,
|
|
metadata={},
|
|
model="gpt-5-mini-2025-08-07",
|
|
object="response",
|
|
output=[],
|
|
parallel_tool_calls=True,
|
|
temperature=1.0,
|
|
tool_choice="auto",
|
|
tools=[
|
|
FunctionTool(
|
|
name="weather",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
strict=False,
|
|
type="function",
|
|
description="useful to determine the weather in a given location",
|
|
)
|
|
],
|
|
reasoning=Reasoning(effort="medium", generate_summary=None, summary=None),
|
|
usage=None,
|
|
),
|
|
sequence_number=0,
|
|
type="response.created",
|
|
),
|
|
ResponseOutputItemAddedEvent(
|
|
item=ResponseReasoningItem(
|
|
id="rs_095b57053855eac100690491f54e308196878239be3ba6133c", summary=[], type="reasoning"
|
|
),
|
|
output_index=0,
|
|
sequence_number=2,
|
|
type="response.output_item.added",
|
|
),
|
|
ResponseOutputItemDoneEvent(
|
|
item=ResponseReasoningItem(
|
|
id="rs_095b57053855eac100690491f54e308196878239be3ba6133c", summary=[], type="reasoning"
|
|
),
|
|
output_index=0,
|
|
sequence_number=3,
|
|
type="response.output_item.done",
|
|
),
|
|
ResponseOutputItemAddedEvent(
|
|
item=ResponseFunctionToolCall(
|
|
arguments="",
|
|
call_id="call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
name="weather",
|
|
type="function_call",
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
status="in_progress",
|
|
),
|
|
output_index=1,
|
|
sequence_number=4,
|
|
type="response.output_item.added",
|
|
),
|
|
ResponseFunctionCallArgumentsDeltaEvent(
|
|
delta='{"city":',
|
|
item_id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
output_index=1,
|
|
sequence_number=5,
|
|
type="response.function_call_arguments.delta",
|
|
obfuscation="PySUcQ59ZZRkOm",
|
|
),
|
|
ResponseFunctionCallArgumentsDeltaEvent(
|
|
delta='"Paris"}',
|
|
item_id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
output_index=1,
|
|
sequence_number=8,
|
|
type="response.function_call_arguments.delta",
|
|
obfuscation="INeMDAi1uAj",
|
|
),
|
|
ResponseFunctionCallArgumentsDoneEvent(
|
|
arguments='{"city":"Paris"}',
|
|
item_id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
name="weather", # added name here because pydantic complains otherwise API returns a none here
|
|
output_index=1,
|
|
sequence_number=10,
|
|
type="response.function_call_arguments.done",
|
|
),
|
|
ResponseCompletedEvent(
|
|
response=Response(
|
|
id="resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
created_at=1761907188.0,
|
|
metadata={},
|
|
model="gpt-5-mini-2025-08-07",
|
|
object="response",
|
|
output=[
|
|
ResponseReasoningItem(
|
|
id="rs_095b57053855eac100690491f54e308196878239be3ba6133c", summary=[], type="reasoning"
|
|
),
|
|
ResponseFunctionToolCall(
|
|
arguments='{"city":"Paris"}',
|
|
call_id="call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
name="weather",
|
|
type="function_call",
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
status="completed",
|
|
),
|
|
],
|
|
parallel_tool_calls=True,
|
|
temperature=1.0,
|
|
tool_choice="auto",
|
|
tools=[
|
|
FunctionTool(
|
|
name="weather",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
strict=False,
|
|
type="function",
|
|
description="useful to determine the weather in a given location",
|
|
)
|
|
],
|
|
top_p=1.0,
|
|
reasoning=Reasoning(effort="medium", generate_summary=None, summary=None),
|
|
usage=ResponseUsage(
|
|
input_tokens=62,
|
|
input_tokens_details=InputTokensDetails(cached_tokens=0),
|
|
output_tokens=83,
|
|
output_tokens_details=OutputTokensDetails(reasoning_tokens=64),
|
|
total_tokens=145,
|
|
),
|
|
store=True,
|
|
),
|
|
sequence_number=12,
|
|
type="response.completed",
|
|
),
|
|
]
|
|
|
|
streaming_chunks = []
|
|
for chunk in chunks:
|
|
streaming_chunk = _convert_response_chunk_to_streaming_chunk(chunk, previous_chunks=streaming_chunks)
|
|
streaming_chunks.append(streaming_chunk)
|
|
|
|
assert streaming_chunks == [
|
|
# TODO Unneeded streaming chunk
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [],
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "useful to determine the weather in a given location",
|
|
}
|
|
],
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"usage": None,
|
|
},
|
|
"sequence_number": 0,
|
|
"type": "response.created",
|
|
},
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
start=True,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=1,
|
|
tool_calls=[
|
|
ToolCallDelta(
|
|
index=1,
|
|
tool_name="weather",
|
|
arguments=None,
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={
|
|
"arguments": "",
|
|
"call_id": "call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
"id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"name": "weather",
|
|
"status": "in_progress",
|
|
"type": "function_call",
|
|
},
|
|
)
|
|
],
|
|
start=True,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=1,
|
|
tool_calls=[
|
|
ToolCallDelta(
|
|
index=1,
|
|
tool_name=None,
|
|
arguments='{"city":',
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={
|
|
"item_id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"output_index": 1,
|
|
"sequence_number": 5,
|
|
"type": "response.function_call_arguments.delta",
|
|
"obfuscation": "PySUcQ59ZZRkOm",
|
|
},
|
|
)
|
|
],
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=1,
|
|
tool_calls=[
|
|
ToolCallDelta(
|
|
index=1,
|
|
tool_name=None,
|
|
arguments='"Paris"}',
|
|
id="fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
extra={
|
|
"item_id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"output_index": 1,
|
|
"sequence_number": 8,
|
|
"type": "response.function_call_arguments.delta",
|
|
"obfuscation": "INeMDAi1uAj",
|
|
},
|
|
)
|
|
],
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"arguments": '{"city":"Paris"}',
|
|
"item_id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"name": "weather",
|
|
"output_index": 1,
|
|
"sequence_number": 10,
|
|
"type": "response.function_call_arguments.done",
|
|
},
|
|
index=1,
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"metadata": {},
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [
|
|
{
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"summary": [],
|
|
"type": "reasoning",
|
|
},
|
|
{
|
|
"arguments": '{"city":"Paris"}',
|
|
"call_id": "call_OZZXFm7SLb4F3Xg8a9XVVCvv",
|
|
"name": "weather",
|
|
"type": "function_call",
|
|
"id": "fc_095b57053855eac100690491f6a224819680e2f9c7cbc5a531",
|
|
"status": "completed",
|
|
},
|
|
],
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"city": {"type": "string"}},
|
|
"required": ["city"],
|
|
"additionalProperties": False,
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "useful to determine the weather in a given location",
|
|
}
|
|
],
|
|
"top_p": 1.0,
|
|
"reasoning": {"effort": "medium", "generate_summary": None, "summary": None},
|
|
"usage": {
|
|
"input_tokens": 62,
|
|
"input_tokens_details": {"cached_tokens": 0},
|
|
"output_tokens": 83,
|
|
"output_tokens_details": {"reasoning_tokens": 64},
|
|
"total_tokens": 145,
|
|
},
|
|
"store": True,
|
|
},
|
|
"sequence_number": 12,
|
|
"type": "response.completed",
|
|
},
|
|
finish_reason="tool_calls",
|
|
),
|
|
]
|
|
|
|
|
|
class TestResponseToChatMessage:
|
|
def test_convert_system_message(self):
|
|
message = ChatMessage.from_system("You are good assistant")
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{"role": "system", "content": "You are good assistant"}
|
|
]
|
|
|
|
def test_convert_user_message(self):
|
|
message = ChatMessage.from_user("I have a question")
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{"role": "user", "content": [{"type": "input_text", "text": "I have a question"}]}
|
|
]
|
|
|
|
def test_convert_multimodal_user_message(self, base64_image_string):
|
|
message = ChatMessage.from_user(
|
|
content_parts=[
|
|
TextContent("I have a question"),
|
|
ImageContent(base64_image=base64_image_string, detail="auto"),
|
|
]
|
|
)
|
|
assert message.to_openai_dict_format() == {
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "I have a question"},
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": f"data:image/png;base64,{base64_image_string}", "detail": "auto"},
|
|
},
|
|
],
|
|
}
|
|
|
|
# image content only should be supported as well
|
|
message = ChatMessage.from_user(content_parts=[ImageContent(base64_image=base64_image_string, detail="auto")])
|
|
assert message.to_openai_dict_format() == {
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": f"data:image/png;base64,{base64_image_string}", "detail": "auto"},
|
|
}
|
|
],
|
|
}
|
|
|
|
def test_convert_user_message_with_file_content(self, base64_pdf_string):
|
|
message = ChatMessage.from_user(
|
|
content_parts=[FileContent(base64_data=base64_pdf_string, mime_type="application/pdf", filename="test.pdf")]
|
|
)
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "input_file",
|
|
"filename": "test.pdf",
|
|
"file_data": f"data:application/pdf;base64,{base64_pdf_string}",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
|
|
def test_convert_user_message_with_file_content_no_filename(self, base64_pdf_string):
|
|
message = ChatMessage.from_user(
|
|
content_parts=[FileContent(base64_data=base64_pdf_string, mime_type="application/pdf")]
|
|
)
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "input_file",
|
|
"filename": "filename",
|
|
"file_data": f"data:application/pdf;base64,{base64_pdf_string}",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
|
|
def test_convert_assistant_message(self):
|
|
message = ChatMessage.from_assistant(text="I have an answer", meta={"finish_reason": "stop"})
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{"role": "assistant", "content": "I have an answer"}
|
|
]
|
|
|
|
def test_convert_assistant_message_w_tool_call(self):
|
|
chat_message = ChatMessage(
|
|
_role=ChatRole.ASSISTANT,
|
|
_content=[
|
|
TextContent(text="I need to use the functions.weather tool."),
|
|
ReasoningContent(
|
|
reasoning_text="I need to use the functions.weather tool.",
|
|
extra={"id": "rs_0d13efdd", "type": "reasoning"},
|
|
),
|
|
ToolCall(
|
|
tool_name="weather",
|
|
arguments={"location": "Berlin"},
|
|
id="fc_0d13efdd",
|
|
extra={"call_id": "call_a82vwFAIzku9SmBuQuecQSRq"},
|
|
),
|
|
],
|
|
_name=None,
|
|
# some keys are removed to keep the test concise
|
|
_meta={
|
|
"id": "resp_0d13efdd97aa4",
|
|
"created_at": 1761148307.0,
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"parallel_tool_calls": True,
|
|
"temperature": 1.0,
|
|
"tool_choice": "auto",
|
|
"tools": [
|
|
{
|
|
"name": "weather",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"location": {"type": "string"}},
|
|
"required": ["location"],
|
|
"additionalProperties": False,
|
|
},
|
|
"strict": False,
|
|
"type": "function",
|
|
"description": "A tool to get the weather",
|
|
}
|
|
],
|
|
"top_p": 1.0,
|
|
"reasoning": {"effort": "low", "summary": "detailed"},
|
|
"usage": {"input_tokens": 59, "output_tokens": 19, "total_tokens": 78},
|
|
"store": True,
|
|
},
|
|
)
|
|
responses_api_format = _convert_chat_message_to_responses_api_format(chat_message)
|
|
assert responses_api_format == [
|
|
{
|
|
"id": "rs_0d13efdd",
|
|
"type": "reasoning",
|
|
"summary": [{"text": "I need to use the functions.weather tool.", "type": "summary_text"}],
|
|
},
|
|
{
|
|
"type": "function_call",
|
|
"name": "weather",
|
|
"arguments": '{"location": "Berlin"}',
|
|
"id": "fc_0d13efdd",
|
|
"call_id": "call_a82vwFAIzku9SmBuQuecQSRq",
|
|
},
|
|
{"content": "I need to use the functions.weather tool.", "role": "assistant"},
|
|
]
|
|
|
|
def test_convert_assistant_message_reasoning_strips_invalid_streaming_fields(self):
|
|
chat_message = ChatMessage(
|
|
_role=ChatRole.ASSISTANT,
|
|
_content=[
|
|
ReasoningContent(
|
|
reasoning_text="Let me think.",
|
|
extra={
|
|
"id": "rs_abc",
|
|
"type": "reasoning",
|
|
"encrypted_content": "enc123",
|
|
"status": "completed",
|
|
"item_id": "some_item",
|
|
"output_index": 0,
|
|
"summary_index": 1,
|
|
"event_id": "ev_xyz",
|
|
"sequence_number": 42,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
result = _convert_chat_message_to_responses_api_format(chat_message)
|
|
assert result == [
|
|
{
|
|
"id": "rs_abc",
|
|
"type": "reasoning",
|
|
"encrypted_content": "enc123",
|
|
"status": "completed",
|
|
"summary": [{"text": "Let me think.", "type": "summary_text"}],
|
|
}
|
|
]
|
|
|
|
def test_convert_tool_message(self):
|
|
tool_call_result = ChatMessage(
|
|
_role=ChatRole.TOOL,
|
|
_content=[
|
|
ToolCallResult(
|
|
result="result",
|
|
origin=ToolCall(
|
|
id="fc_0d13efdd",
|
|
tool_name="weather",
|
|
arguments={"location": "Berlin"},
|
|
extra={"call_id": "call_a82vwFAIzku9SmBuQuecQSRq"},
|
|
),
|
|
error=False,
|
|
)
|
|
],
|
|
)
|
|
|
|
assert _convert_chat_message_to_responses_api_format(tool_call_result) == [
|
|
{
|
|
"call_id": "call_a82vwFAIzku9SmBuQuecQSRq",
|
|
"output": [{"type": "input_text", "text": "result"}],
|
|
"type": "function_call_output",
|
|
}
|
|
]
|
|
|
|
def test_convert_tool_message_list_with_image(self, base64_image_string):
|
|
tool_result = [
|
|
TextContent(text="first result"),
|
|
ImageContent(base64_image=base64_image_string, mime_type="image/png"),
|
|
]
|
|
message = ChatMessage.from_tool(
|
|
tool_result=tool_result,
|
|
origin=ToolCall(
|
|
tool_name="mytool", arguments={}, id="123", extra={"call_id": "call_a82vwFAIzku9SmBuQuecQSRq"}
|
|
),
|
|
error=False,
|
|
)
|
|
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{
|
|
"call_id": "call_a82vwFAIzku9SmBuQuecQSRq",
|
|
"output": [
|
|
{"type": "input_text", "text": "first result"},
|
|
{"type": "input_image", "image_url": f"data:image/png;base64,{base64_image_string}"},
|
|
],
|
|
"type": "function_call_output",
|
|
}
|
|
]
|
|
|
|
def test_convert_tool_message_list_with_file(self, base64_pdf_string):
|
|
tool_result = [
|
|
TextContent(text="first result"),
|
|
FileContent(base64_data=base64_pdf_string, mime_type="application/pdf", filename="guide.pdf"),
|
|
]
|
|
message = ChatMessage.from_tool(
|
|
tool_result=tool_result,
|
|
origin=ToolCall(
|
|
tool_name="mytool", arguments={}, id="123", extra={"call_id": "call_a82vwFAIzku9SmBuQuecQSRq"}
|
|
),
|
|
error=False,
|
|
)
|
|
|
|
assert _convert_chat_message_to_responses_api_format(message) == [
|
|
{
|
|
"call_id": "call_a82vwFAIzku9SmBuQuecQSRq",
|
|
"output": [
|
|
{"type": "input_text", "text": "first result"},
|
|
{
|
|
"type": "input_file",
|
|
"filename": "guide.pdf",
|
|
"file_data": f"data:application/pdf;base64,{base64_pdf_string}",
|
|
},
|
|
],
|
|
"type": "function_call_output",
|
|
}
|
|
]
|
|
|
|
def test_convert_invalid(self):
|
|
message = ChatMessage(_role=ChatRole.ASSISTANT, _content=[])
|
|
with pytest.raises(ValueError):
|
|
_convert_chat_message_to_responses_api_format(message)
|
|
|
|
message = ChatMessage(
|
|
_role=ChatRole.USER,
|
|
_content=[
|
|
TextContent(text="I have an answer"),
|
|
ToolCallResult(
|
|
result="I have another answer",
|
|
origin=ToolCall(id="123", tool_name="mytool", arguments={"a": 1}),
|
|
error=False,
|
|
),
|
|
],
|
|
)
|
|
with pytest.raises(ValueError):
|
|
_convert_chat_message_to_responses_api_format(message)
|
|
|
|
def test_convert_streaming_chunks_to_chat_message_preserves_encrypted_content(self):
|
|
"""Test that encrypted_content in reasoning extra is preserved during streaming conversion."""
|
|
chunks = [
|
|
StreamingChunk(
|
|
content="",
|
|
meta={"received_at": ANY},
|
|
index=0,
|
|
start=True,
|
|
reasoning=ReasoningContent(
|
|
reasoning_text="",
|
|
extra={
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"type": "reasoning",
|
|
"encrypted_content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", # Simulated encrypted reasoning
|
|
"status": "in_progress",
|
|
},
|
|
),
|
|
),
|
|
StreamingChunk(
|
|
content="",
|
|
meta={
|
|
"received_at": ANY,
|
|
"response": {
|
|
"id": "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424",
|
|
"created_at": 1761907188.0,
|
|
"model": "gpt-5-mini-2025-08-07",
|
|
"object": "response",
|
|
"output": [
|
|
{
|
|
"id": "rs_095b57053855eac100690491f54e308196878239be3ba6133c",
|
|
"type": "reasoning",
|
|
"encrypted_content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"status": "completed",
|
|
}
|
|
],
|
|
},
|
|
"sequence_number": 16,
|
|
"type": "response.completed",
|
|
},
|
|
finish_reason="stop",
|
|
),
|
|
]
|
|
|
|
message = _convert_streaming_chunks_to_chat_message(chunks)
|
|
|
|
# Verify reasoning content exists and has the correct structure
|
|
assert message.reasoning is not None
|
|
assert message.reasoning.reasoning_text == ""
|
|
|
|
# Verify encrypted_content is preserved along with id and type
|
|
assert message.reasoning.extra.get("id") == "rs_095b57053855eac100690491f54e308196878239be3ba6133c"
|
|
assert message.reasoning.extra.get("type") == "reasoning"
|
|
assert message.reasoning.extra.get("encrypted_content") == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
assert message.reasoning.extra.get("status") == "in_progress"
|
|
|
|
def test_encrypted_content_preserved_through_full_streaming_pipeline(self):
|
|
"""
|
|
Feeds real OpenAI event objects through the full pipeline:
|
|
|
|
_convert_response_chunk_to_streaming_chunk → _convert_streaming_chunks_to_chat_message
|
|
|
|
to verify encrypted_content survives end-to-end.
|
|
"""
|
|
REASONING_ID = "rs_abc123"
|
|
ENCRYPTED = "eyJhbGciOiJIUzI1NiJ9.encrypted_reasoning"
|
|
|
|
openai_events = [
|
|
# reasoning item starts — encrypted_content not yet available
|
|
ResponseOutputItemAddedEvent(
|
|
item=ResponseReasoningItem(id=REASONING_ID, summary=[], type="reasoning", status="in_progress"),
|
|
output_index=0,
|
|
sequence_number=0,
|
|
type="response.output_item.added",
|
|
),
|
|
# reasoning item finishes — encrypted_content is now populated
|
|
ResponseOutputItemDoneEvent(
|
|
item=ResponseReasoningItem(
|
|
id=REASONING_ID, summary=[], type="reasoning", encrypted_content=ENCRYPTED, status="completed"
|
|
),
|
|
output_index=0,
|
|
sequence_number=1,
|
|
type="response.output_item.done",
|
|
),
|
|
]
|
|
|
|
streaming_chunks = []
|
|
for event in openai_events:
|
|
chunk = _convert_response_chunk_to_streaming_chunk(event, previous_chunks=streaming_chunks)
|
|
streaming_chunks.append(chunk)
|
|
|
|
# The done chunk must carry reasoning so encrypted_content reaches the assembly step
|
|
done_chunk = streaming_chunks[1]
|
|
assert done_chunk.reasoning is not None, (
|
|
"response.output_item.done for reasoning must produce a StreamingChunk with reasoning set; "
|
|
"without this, encrypted_content is silently dropped before assembly"
|
|
)
|
|
assert done_chunk.reasoning.extra.get("encrypted_content") == ENCRYPTED
|
|
|
|
message = _convert_streaming_chunks_to_chat_message(streaming_chunks)
|
|
|
|
assert message.reasoning is not None
|
|
assert message.reasoning.extra.get("id") == REASONING_ID
|
|
assert message.reasoning.extra.get("encrypted_content") == ENCRYPTED, (
|
|
"encrypted_content was dropped — the response.output_item.done event for reasoning items "
|
|
"must be handled in _convert_response_chunk_to_streaming_chunk"
|
|
)
|