@@ -147,13 +147,18 @@ def get_mock_chat_completion_kwargs(async_open_ai: AsyncOpenAI) -> list[dict[str
147
147
148
148
149
149
def completion_message (
150
- message : ChatCompletionMessage , * , usage : CompletionUsage | None = None , logprobs : ChoiceLogprobs | None = None
150
+ message : ChatCompletionMessage ,
151
+ * ,
152
+ usage : CompletionUsage | None = None ,
153
+ logprobs : ChoiceLogprobs | None = None ,
154
+ response_id : str = '123' ,
155
+ finish_reason : str = 'stop'
151
156
) -> chat .ChatCompletion :
152
- choices = [Choice (finish_reason = 'stop' , index = 0 , message = message )]
157
+ choices = [Choice (finish_reason = finish_reason , index = 0 , message = message )]
153
158
if logprobs :
154
- choices = [Choice (finish_reason = 'stop' , index = 0 , message = message , logprobs = logprobs )]
159
+ choices = [Choice (finish_reason = finish_reason , index = 0 , message = message , logprobs = logprobs )]
155
160
return chat .ChatCompletion (
156
- id = '123' ,
161
+ id = response_id ,
157
162
choices = choices ,
158
163
created = 1704067200 , # 2024-01-01
159
164
model = 'gpt-4o-123' ,
@@ -189,6 +194,8 @@ async def test_request_simple_success(allow_model_requests: None):
189
194
model_name = 'gpt-4o-123' ,
190
195
timestamp = datetime (2024 , 1 , 1 , 0 , 0 , tzinfo = timezone .utc ),
191
196
vendor_id = '123' ,
197
+ id = '123' ,
198
+ finish_reason = 'stop' ,
192
199
),
193
200
ModelRequest (parts = [UserPromptPart (content = 'hello' , timestamp = IsNow (tz = timezone .utc ))]),
194
201
ModelResponse (
@@ -197,6 +204,8 @@ async def test_request_simple_success(allow_model_requests: None):
197
204
model_name = 'gpt-4o-123' ,
198
205
timestamp = datetime (2024 , 1 , 1 , 0 , 0 , tzinfo = timezone .utc ),
199
206
vendor_id = '123' ,
207
+ id = '123' ,
208
+ finish_reason = 'stop' ,
200
209
),
201
210
]
202
211
)
@@ -234,6 +243,36 @@ async def test_request_simple_usage(allow_model_requests: None):
234
243
assert result .usage () == snapshot (Usage (requests = 1 , request_tokens = 2 , response_tokens = 1 , total_tokens = 3 ))
235
244
236
245
246
+ async def test_id_and_finish_reason_fields (allow_model_requests : None ):
247
+ """Test that id and finish_reason fields are properly populated in ModelResponse."""
248
+ # Test with different finish reasons
249
+ test_cases = [
250
+ ('stop' , 'response-id-1' ),
251
+ ('length' , 'response-id-2' ),
252
+ ('tool_calls' , 'response-id-3' ),
253
+ ]
254
+
255
+ for finish_reason , response_id in test_cases :
256
+ c = completion_message (
257
+ ChatCompletionMessage (content = 'test response' , role = 'assistant' ),
258
+ response_id = response_id ,
259
+ finish_reason = finish_reason ,
260
+ )
261
+ mock_client = MockOpenAI .create_mock (c )
262
+ m = OpenAIModel ('gpt-4o' , provider = OpenAIProvider (openai_client = mock_client ))
263
+ agent = Agent (m )
264
+
265
+ result = await agent .run ('test' )
266
+ assert result .output == 'test response'
267
+
268
+ # Check that the ModelResponse contains the correct id and finish_reason
269
+ messages = result .all_messages ()
270
+ model_response = messages [1 ] # Second message should be the model response
271
+ assert isinstance (model_response , ModelResponse )
272
+ assert model_response .id == response_id
273
+ assert model_response .finish_reason == finish_reason
274
+
275
+
237
276
async def test_request_structured_response (allow_model_requests : None ):
238
277
c = completion_message (
239
278
ChatCompletionMessage (
@@ -420,9 +459,9 @@ async def get_location(loc_name: str) -> str:
420
459
FinishReason = Literal ['stop' , 'length' , 'tool_calls' , 'content_filter' , 'function_call' ]
421
460
422
461
423
- def chunk (delta : list [ChoiceDelta ], finish_reason : FinishReason | None = None ) -> chat .ChatCompletionChunk :
462
+ def chunk (delta : list [ChoiceDelta ], finish_reason : FinishReason | None = None , chunk_id : str = 'x' ) -> chat .ChatCompletionChunk :
424
463
return chat .ChatCompletionChunk (
425
- id = 'x' ,
464
+ id = chunk_id ,
426
465
choices = [
427
466
ChunkChoice (index = index , delta = delta , finish_reason = finish_reason ) for index , delta in enumerate (delta )
428
467
],
@@ -433,8 +472,8 @@ def chunk(delta: list[ChoiceDelta], finish_reason: FinishReason | None = None) -
433
472
)
434
473
435
474
436
- def text_chunk (text : str , finish_reason : FinishReason | None = None ) -> chat .ChatCompletionChunk :
437
- return chunk ([ChoiceDelta (content = text , role = 'assistant' )], finish_reason = finish_reason )
475
+ def text_chunk (text : str , finish_reason : FinishReason | None = None , chunk_id : str = 'x' ) -> chat .ChatCompletionChunk :
476
+ return chunk ([ChoiceDelta (content = text , role = 'assistant' )], finish_reason = finish_reason , chunk_id = chunk_id )
438
477
439
478
440
479
async def test_stream_text (allow_model_requests : None ):
@@ -550,6 +589,55 @@ async def test_stream_structured_finish_reason(allow_model_requests: None):
550
589
assert result .is_complete
551
590
552
591
592
+ async def test_stream_id_and_finish_reason_fields (allow_model_requests : None ):
593
+ """Test that streaming responses properly track id and finish_reason fields."""
594
+ # Test streaming text response
595
+ stream = [
596
+ text_chunk ('hello ' , chunk_id = 'stream-response-123' ),
597
+ text_chunk ('world' , chunk_id = 'stream-response-123' ),
598
+ text_chunk ('!' , finish_reason = 'stop' , chunk_id = 'stream-response-123' ),
599
+ ]
600
+ mock_client = MockOpenAI .create_mock_stream (stream )
601
+ m = OpenAIModel ('gpt-4o' , provider = OpenAIProvider (openai_client = mock_client ))
602
+ agent = Agent (m )
603
+
604
+ async with agent .run_stream ('test' ) as result :
605
+ assert not result .is_complete
606
+ text_chunks = [c async for c in result .stream_text (debounce_by = None )]
607
+ assert text_chunks == ['hello ' , 'hello world' , 'hello world!' ]
608
+ assert result .is_complete
609
+
610
+ # Get the final messages and check the ModelResponse
611
+ messages = result .all_messages ()
612
+ model_response = messages [1 ] # Second message should be the model response
613
+ assert isinstance (model_response , ModelResponse )
614
+ assert model_response .id == 'stream-response-123'
615
+ assert model_response .finish_reason == 'stop'
616
+
617
+ # Test streaming with structured output and different finish reason
618
+ stream = [
619
+ struc_chunk ('final_result' , None ),
620
+ chunk ([ChoiceDelta (tool_calls = [ChoiceDeltaToolCall (index = 0 , function = ChoiceDeltaToolCallFunction (name = None , arguments = '{"first": "Test"' ))])], chunk_id = 'struct-response-456' ),
621
+ chunk ([ChoiceDelta (tool_calls = [ChoiceDeltaToolCall (index = 0 , function = ChoiceDeltaToolCallFunction (name = None , arguments = '}' ))])], finish_reason = 'length' , chunk_id = 'struct-response-456' ),
622
+ ]
623
+ mock_client = MockOpenAI .create_mock_stream (stream )
624
+ m = OpenAIModel ('gpt-4o' , provider = OpenAIProvider (openai_client = mock_client ))
625
+ agent = Agent (m , output_type = MyTypedDict )
626
+
627
+ async with agent .run_stream ('test' ) as result :
628
+ assert not result .is_complete
629
+ structured_chunks = [dict (c ) async for c in result .stream (debounce_by = None )]
630
+ assert structured_chunks == [{'first' : 'Test' }, {'first' : 'Test' }]
631
+ assert result .is_complete
632
+
633
+ # Get the final messages and check the ModelResponse
634
+ messages = result .all_messages ()
635
+ model_response = messages [1 ] # Second message should be the model response
636
+ assert isinstance (model_response , ModelResponse )
637
+ assert model_response .id == 'struct-response-456'
638
+ assert model_response .finish_reason == 'length'
639
+
640
+
553
641
async def test_stream_native_output (allow_model_requests : None ):
554
642
stream = [
555
643
chunk ([]),
0 commit comments