@@ -97,18 +97,16 @@ def openai_responses_streaming_chunks_with_tool_call():
9797 ),
9898 StreamingChunk (
9999 content = "" ,
100- meta = {
101- "item" : {
100+ meta = {"received_at" : ANY },
101+ index = 0 ,
102+ reasoning = ReasoningContent (
103+ reasoning_text = "" ,
104+ extra = {
102105 "id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" ,
103106 "summary" : [],
104107 "type" : "reasoning" ,
105108 },
106- "output_index" : 0 ,
107- "sequence_number" : 3 ,
108- "type" : "response.output_item.done" ,
109- "received_at" : ANY ,
110- },
111- index = 0 ,
109+ ),
112110 ),
113111 StreamingChunk (
114112 content = "" ,
@@ -233,7 +231,11 @@ def test_convert_streaming_chunks_to_chat_message_with_tool_call_empty_reasoning
233231 _content = [
234232 ReasoningContent (
235233 reasoning_text = "" ,
236- extra = {"id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" , "type" : "reasoning" },
234+ extra = {
235+ "id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" ,
236+ "summary" : [],
237+ "type" : "reasoning" ,
238+ },
237239 ),
238240 ToolCall (
239241 tool_name = "weather" ,
@@ -609,18 +611,16 @@ def test_convert_only_text(self):
609611 ),
610612 StreamingChunk (
611613 content = "" ,
612- meta = {
613- "received_at" : ANY ,
614- "item" : {
614+ meta = {"received_at" : ANY },
615+ index = 0 ,
616+ reasoning = ReasoningContent (
617+ reasoning_text = "" ,
618+ extra = {
615619 "id" : "rs_0a8811e62a95217b00690c5ff70a308195a8207d7eb43f1d5b" ,
616620 "summary" : [],
617621 "type" : "reasoning" ,
618622 },
619- "output_index" : 0 ,
620- "sequence_number" : 3 ,
621- "type" : "response.output_item.done" ,
622- },
623- index = 0 ,
623+ ),
624624 ),
625625 StreamingChunk (
626626 content = "" ,
@@ -1075,18 +1075,16 @@ def test_convert_only_function_call(self):
10751075 ),
10761076 StreamingChunk (
10771077 content = "" ,
1078- meta = {
1079- "item" : {
1078+ meta = {"received_at" : ANY },
1079+ index = 0 ,
1080+ reasoning = ReasoningContent (
1081+ reasoning_text = "" ,
1082+ extra = {
10801083 "id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" ,
10811084 "summary" : [],
10821085 "type" : "reasoning" ,
10831086 },
1084- "output_index" : 0 ,
1085- "sequence_number" : 3 ,
1086- "type" : "response.output_item.done" ,
1087- "received_at" : ANY ,
1088- },
1089- index = 0 ,
1087+ ),
10901088 ),
10911089 StreamingChunk (
10921090 content = "" ,
@@ -1437,3 +1435,110 @@ def test_convert_invalid(self):
14371435 )
14381436 with pytest .raises (ValueError ):
14391437 _convert_chat_message_to_responses_api_format (message )
1438+
1439+ def test_convert_streaming_chunks_to_chat_message_preserves_encrypted_content (self ):
1440+ """Test that encrypted_content in reasoning extra is preserved during streaming conversion."""
1441+ chunks = [
1442+ StreamingChunk (
1443+ content = "" ,
1444+ meta = {"received_at" : ANY },
1445+ index = 0 ,
1446+ start = True ,
1447+ reasoning = ReasoningContent (
1448+ reasoning_text = "" ,
1449+ extra = {
1450+ "id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" ,
1451+ "type" : "reasoning" ,
1452+ "encrypted_content" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." , # Simulated encrypted reasoning
1453+ "status" : "in_progress" ,
1454+ },
1455+ ),
1456+ ),
1457+ StreamingChunk (
1458+ content = "" ,
1459+ meta = {
1460+ "received_at" : ANY ,
1461+ "response" : {
1462+ "id" : "resp_095b57053855eac100690491f4e22c8196ac124365e8c70424" ,
1463+ "created_at" : 1761907188.0 ,
1464+ "model" : "gpt-5-mini-2025-08-07" ,
1465+ "object" : "response" ,
1466+ "output" : [
1467+ {
1468+ "id" : "rs_095b57053855eac100690491f54e308196878239be3ba6133c" ,
1469+ "type" : "reasoning" ,
1470+ "encrypted_content" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
1471+ "status" : "completed" ,
1472+ }
1473+ ],
1474+ },
1475+ "sequence_number" : 16 ,
1476+ "type" : "response.completed" ,
1477+ },
1478+ finish_reason = "stop" ,
1479+ ),
1480+ ]
1481+
1482+ message = _convert_streaming_chunks_to_chat_message (chunks )
1483+
1484+ # Verify reasoning content exists and has the correct structure
1485+ assert message .reasoning is not None
1486+ assert message .reasoning .reasoning_text == ""
1487+
1488+ # Verify encrypted_content is preserved along with id and type
1489+ assert message .reasoning .extra .get ("id" ) == "rs_095b57053855eac100690491f54e308196878239be3ba6133c"
1490+ assert message .reasoning .extra .get ("type" ) == "reasoning"
1491+ assert message .reasoning .extra .get ("encrypted_content" ) == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
1492+ assert message .reasoning .extra .get ("status" ) == "in_progress"
1493+
1494+ def test_encrypted_content_preserved_through_full_streaming_pipeline (self ):
1495+ """
1496+ Feeds real OpenAI event objects through the full pipeline:
1497+
1498+ _convert_response_chunk_to_streaming_chunk → _convert_streaming_chunks_to_chat_message
1499+
1500+ to verify encrypted_content survives end-to-end.
1501+ """
1502+ REASONING_ID = "rs_abc123"
1503+ ENCRYPTED = "eyJhbGciOiJIUzI1NiJ9.encrypted_reasoning"
1504+
1505+ openai_events = [
1506+ # reasoning item starts — encrypted_content not yet available
1507+ ResponseOutputItemAddedEvent (
1508+ item = ResponseReasoningItem (id = REASONING_ID , summary = [], type = "reasoning" , status = "in_progress" ),
1509+ output_index = 0 ,
1510+ sequence_number = 0 ,
1511+ type = "response.output_item.added" ,
1512+ ),
1513+ # reasoning item finishes — encrypted_content is now populated
1514+ ResponseOutputItemDoneEvent (
1515+ item = ResponseReasoningItem (
1516+ id = REASONING_ID , summary = [], type = "reasoning" , encrypted_content = ENCRYPTED , status = "completed"
1517+ ),
1518+ output_index = 0 ,
1519+ sequence_number = 1 ,
1520+ type = "response.output_item.done" ,
1521+ ),
1522+ ]
1523+
1524+ streaming_chunks = []
1525+ for event in openai_events :
1526+ chunk = _convert_response_chunk_to_streaming_chunk (event , previous_chunks = streaming_chunks )
1527+ streaming_chunks .append (chunk )
1528+
1529+ # The done chunk must carry reasoning so encrypted_content reaches the assembly step
1530+ done_chunk = streaming_chunks [1 ]
1531+ assert done_chunk .reasoning is not None , (
1532+ "response.output_item.done for reasoning must produce a StreamingChunk with reasoning set; "
1533+ "without this, encrypted_content is silently dropped before assembly"
1534+ )
1535+ assert done_chunk .reasoning .extra .get ("encrypted_content" ) == ENCRYPTED
1536+
1537+ message = _convert_streaming_chunks_to_chat_message (streaming_chunks )
1538+
1539+ assert message .reasoning is not None
1540+ assert message .reasoning .extra .get ("id" ) == REASONING_ID
1541+ assert message .reasoning .extra .get ("encrypted_content" ) == ENCRYPTED , (
1542+ "encrypted_content was dropped — the response.output_item.done event for reasoning items "
1543+ "must be handled in _convert_response_chunk_to_streaming_chunk"
1544+ )
0 commit comments