openai/openai-python

Public

mirrored fromhttps://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.61.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/lib/chat/test_completions_streaming.py

1169lines · modecode

1from __future__ import annotations
2
3import os
4from typing import Any, Generic, Callable, Iterator, cast, overload
5from typing_extensions import Literal, TypeVar
6
7import rich
8import httpx
9import pytest
10from respx import MockRouter
11from pydantic import BaseModel
12from inline_snapshot import external, snapshot, outsource
13
14import openai
15from openai import OpenAI, AsyncOpenAI
16from openai._utils import consume_sync_iterator, assert_signatures_in_sync
17from openai._compat import model_copy
18from openai.types.chat import ChatCompletionChunk
19from openai.lib.streaming.chat import (
20 ContentDoneEvent,
21 ChatCompletionStream,
22 ChatCompletionStreamEvent,
23 ChatCompletionStreamState,
24 ChatCompletionStreamManager,
25 ParsedChatCompletionSnapshot,
26)
27from openai.lib._parsing._completions import ResponseFormatT
28
29from ._utils import print_obj
30from ...conftest import base_url
31
32_T = TypeVar("_T")
33
34# all the snapshots in this file are auto-generated from the live API
35#
36# you can update them with
37#
38# `OPENAI_LIVE=1 pytest --inline-snapshot=fix`
39
40
41@pytest.mark.respx(base_url=base_url)
42def test_parse_nothing(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
43 listener = _make_stream_snapshot_request(
44 lambda c: c.beta.chat.completions.stream(
45 model="gpt-4o-2024-08-06",
46 messages=[
47 {
48 "role": "user",
49 "content": "What's the weather like in SF?",
50 },
51 ],
52 ),
53 content_snapshot=snapshot(external("e2aad469b71d*.bin")),
54 mock_client=client,
55 respx_mock=respx_mock,
56 )
57
58 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
59 """\
60[
61 ParsedChoice[NoneType](
62 finish_reason='stop',
63 index=0,
64 logprobs=None,
65 message=ParsedChatCompletionMessage[NoneType](
66 audio=None,
67 content="I'm unable to provide real-time weather updates. To get the current weather in San Francisco, I
68recommend checking a reliable weather website or a weather app.",
69 function_call=None,
70 parsed=None,
71 refusal=None,
72 role='assistant',
73 tool_calls=[]
74 )
75 )
76]
77"""
78 )
79 assert print_obj(listener.get_event_by_type("content.done"), monkeypatch) == snapshot(
80 """\
81ContentDoneEvent[NoneType](
82 content="I'm unable to provide real-time weather updates. To get the current weather in San Francisco, I recommend
83checking a reliable weather website or a weather app.",
84 parsed=None,
85 type='content.done'
86)
87"""
88 )
89
90
91@pytest.mark.respx(base_url=base_url)
92def test_parse_pydantic_model(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
93 class Location(BaseModel):
94 city: str
95 temperature: float
96 units: Literal["c", "f"]
97
98 done_snapshots: list[ParsedChatCompletionSnapshot] = []
99
100 def on_event(stream: ChatCompletionStream[Location], event: ChatCompletionStreamEvent[Location]) -> None:
101 if event.type == "content.done":
102 done_snapshots.append(model_copy(stream.current_completion_snapshot, deep=True))
103
104 listener = _make_stream_snapshot_request(
105 lambda c: c.beta.chat.completions.stream(
106 model="gpt-4o-2024-08-06",
107 messages=[
108 {
109 "role": "user",
110 "content": "What's the weather like in SF?",
111 },
112 ],
113 response_format=Location,
114 ),
115 content_snapshot=snapshot(external("7e5ea4d12e7c*.bin")),
116 mock_client=client,
117 respx_mock=respx_mock,
118 on_event=on_event,
119 )
120
121 assert len(done_snapshots) == 1
122 assert isinstance(done_snapshots[0].choices[0].message.parsed, Location)
123
124 for event in reversed(listener.events):
125 if event.type == "content.delta":
126 data = cast(Any, event.parsed)
127 assert isinstance(data["city"], str), data
128 assert isinstance(data["temperature"], (int, float)), data
129 assert isinstance(data["units"], str), data
130 break
131 else:
132 rich.print(listener.events)
133 raise AssertionError("Did not find a `content.delta` event")
134
135 assert print_obj(listener.stream.get_final_completion(), monkeypatch) == snapshot(
136 """\
137ParsedChatCompletion[Location](
138 choices=[
139 ParsedChoice[Location](
140 finish_reason='stop',
141 index=0,
142 logprobs=None,
143 message=ParsedChatCompletionMessage[Location](
144 audio=None,
145 content='{"city":"San Francisco","temperature":61,"units":"f"}',
146 function_call=None,
147 parsed=Location(city='San Francisco', temperature=61.0, units='f'),
148 refusal=None,
149 role='assistant',
150 tool_calls=[]
151 )
152 )
153 ],
154 created=1727346169,
155 id='chatcmpl-ABfw1e5abtU8OwGr15vOreYVb2MiF',
156 model='gpt-4o-2024-08-06',
157 object='chat.completion',
158 service_tier=None,
159 system_fingerprint='fp_5050236cbd',
160 usage=CompletionUsage(
161 completion_tokens=14,
162 completion_tokens_details=CompletionTokensDetails(
163 accepted_prediction_tokens=None,
164 audio_tokens=None,
165 reasoning_tokens=0,
166 rejected_prediction_tokens=None
167 ),
168 prompt_tokens=79,
169 prompt_tokens_details=None,
170 total_tokens=93
171 )
172)
173"""
174 )
175 assert print_obj(listener.get_event_by_type("content.done"), monkeypatch) == snapshot(
176 """\
177ContentDoneEvent[Location](
178 content='{"city":"San Francisco","temperature":61,"units":"f"}',
179 parsed=Location(city='San Francisco', temperature=61.0, units='f'),
180 type='content.done'
181)
182"""
183 )
184
185
186@pytest.mark.respx(base_url=base_url)
187def test_parse_pydantic_model_multiple_choices(
188 client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
189) -> None:
190 class Location(BaseModel):
191 city: str
192 temperature: float
193 units: Literal["c", "f"]
194
195 listener = _make_stream_snapshot_request(
196 lambda c: c.beta.chat.completions.stream(
197 model="gpt-4o-2024-08-06",
198 messages=[
199 {
200 "role": "user",
201 "content": "What's the weather like in SF?",
202 },
203 ],
204 n=3,
205 response_format=Location,
206 ),
207 content_snapshot=snapshot(external("a491adda08c3*.bin")),
208 mock_client=client,
209 respx_mock=respx_mock,
210 )
211
212 assert [e.type for e in listener.events] == snapshot(
213 [
214 "chunk",
215 "content.delta",
216 "chunk",
217 "content.delta",
218 "chunk",
219 "content.delta",
220 "chunk",
221 "content.delta",
222 "chunk",
223 "content.delta",
224 "chunk",
225 "content.delta",
226 "chunk",
227 "content.delta",
228 "chunk",
229 "content.delta",
230 "chunk",
231 "content.delta",
232 "chunk",
233 "content.delta",
234 "chunk",
235 "content.delta",
236 "chunk",
237 "content.delta",
238 "chunk",
239 "content.delta",
240 "chunk",
241 "content.delta",
242 "chunk",
243 "content.delta",
244 "chunk",
245 "content.delta",
246 "chunk",
247 "content.delta",
248 "chunk",
249 "content.delta",
250 "chunk",
251 "content.delta",
252 "chunk",
253 "content.delta",
254 "chunk",
255 "content.delta",
256 "chunk",
257 "content.delta",
258 "chunk",
259 "content.delta",
260 "chunk",
261 "content.delta",
262 "chunk",
263 "content.delta",
264 "chunk",
265 "content.delta",
266 "chunk",
267 "content.delta",
268 "chunk",
269 "content.delta",
270 "chunk",
271 "content.delta",
272 "chunk",
273 "content.delta",
274 "chunk",
275 "content.delta",
276 "chunk",
277 "content.delta",
278 "chunk",
279 "content.delta",
280 "chunk",
281 "content.delta",
282 "chunk",
283 "content.delta",
284 "chunk",
285 "content.delta",
286 "chunk",
287 "content.delta",
288 "chunk",
289 "content.delta",
290 "chunk",
291 "content.delta",
292 "chunk",
293 "content.delta",
294 "chunk",
295 "content.delta",
296 "chunk",
297 "content.delta",
298 "chunk",
299 "content.delta",
300 "chunk",
301 "content.delta",
302 "chunk",
303 "content.delta",
304 "chunk",
305 "content.done",
306 "chunk",
307 "content.done",
308 "chunk",
309 "content.done",
310 "chunk",
311 ]
312 )
313 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
314 """\
315[
316 ParsedChoice[Location](
317 finish_reason='stop',
318 index=0,
319 logprobs=None,
320 message=ParsedChatCompletionMessage[Location](
321 audio=None,
322 content='{"city":"San Francisco","temperature":65,"units":"f"}',
323 function_call=None,
324 parsed=Location(city='San Francisco', temperature=65.0, units='f'),
325 refusal=None,
326 role='assistant',
327 tool_calls=[]
328 )
329 ),
330 ParsedChoice[Location](
331 finish_reason='stop',
332 index=1,
333 logprobs=None,
334 message=ParsedChatCompletionMessage[Location](
335 audio=None,
336 content='{"city":"San Francisco","temperature":61,"units":"f"}',
337 function_call=None,
338 parsed=Location(city='San Francisco', temperature=61.0, units='f'),
339 refusal=None,
340 role='assistant',
341 tool_calls=[]
342 )
343 ),
344 ParsedChoice[Location](
345 finish_reason='stop',
346 index=2,
347 logprobs=None,
348 message=ParsedChatCompletionMessage[Location](
349 audio=None,
350 content='{"city":"San Francisco","temperature":59,"units":"f"}',
351 function_call=None,
352 parsed=Location(city='San Francisco', temperature=59.0, units='f'),
353 refusal=None,
354 role='assistant',
355 tool_calls=[]
356 )
357 )
358]
359"""
360 )
361
362
363@pytest.mark.respx(base_url=base_url)
364def test_parse_max_tokens_reached(client: OpenAI, respx_mock: MockRouter) -> None:
365 class Location(BaseModel):
366 city: str
367 temperature: float
368 units: Literal["c", "f"]
369
370 with pytest.raises(openai.LengthFinishReasonError):
371 _make_stream_snapshot_request(
372 lambda c: c.beta.chat.completions.stream(
373 model="gpt-4o-2024-08-06",
374 messages=[
375 {
376 "role": "user",
377 "content": "What's the weather like in SF?",
378 },
379 ],
380 max_tokens=1,
381 response_format=Location,
382 ),
383 content_snapshot=snapshot(external("4cc50a6135d2*.bin")),
384 mock_client=client,
385 respx_mock=respx_mock,
386 )
387
388
389@pytest.mark.respx(base_url=base_url)
390def test_parse_pydantic_model_refusal(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
391 class Location(BaseModel):
392 city: str
393 temperature: float
394 units: Literal["c", "f"]
395
396 listener = _make_stream_snapshot_request(
397 lambda c: c.beta.chat.completions.stream(
398 model="gpt-4o-2024-08-06",
399 messages=[
400 {
401 "role": "user",
402 "content": "How do I make anthrax?",
403 },
404 ],
405 response_format=Location,
406 ),
407 content_snapshot=snapshot(external("173417d55340*.bin")),
408 mock_client=client,
409 respx_mock=respx_mock,
410 )
411
412 assert print_obj(listener.get_event_by_type("refusal.done"), monkeypatch) == snapshot("""\
413RefusalDoneEvent(refusal="I'm sorry, I can't assist with that request.", type='refusal.done')
414""")
415
416 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
417 """\
418[
419 ParsedChoice[Location](
420 finish_reason='stop',
421 index=0,
422 logprobs=None,
423 message=ParsedChatCompletionMessage[Location](
424 audio=None,
425 content=None,
426 function_call=None,
427 parsed=None,
428 refusal="I'm sorry, I can't assist with that request.",
429 role='assistant',
430 tool_calls=[]
431 )
432 )
433]
434"""
435 )
436
437
438@pytest.mark.respx(base_url=base_url)
439def test_content_logprobs_events(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
440 listener = _make_stream_snapshot_request(
441 lambda c: c.beta.chat.completions.stream(
442 model="gpt-4o-2024-08-06",
443 messages=[
444 {
445 "role": "user",
446 "content": "Say foo",
447 },
448 ],
449 logprobs=True,
450 ),
451 content_snapshot=snapshot(external("83b060bae42e*.bin")),
452 mock_client=client,
453 respx_mock=respx_mock,
454 )
455
456 assert print_obj([e for e in listener.events if e.type.startswith("logprobs")], monkeypatch) == snapshot("""\
457[
458 LogprobsContentDeltaEvent(
459 content=[
460 ChatCompletionTokenLogprob(bytes=[70, 111, 111], logprob=-0.0025094282, token='Foo', top_logprobs=[])
461 ],
462 snapshot=[
463 ChatCompletionTokenLogprob(bytes=[70, 111, 111], logprob=-0.0025094282, token='Foo', top_logprobs=[])
464 ],
465 type='logprobs.content.delta'
466 ),
467 LogprobsContentDeltaEvent(
468 content=[ChatCompletionTokenLogprob(bytes=[33], logprob=-0.26638845, token='!', top_logprobs=[])],
469 snapshot=[
470 ChatCompletionTokenLogprob(bytes=[70, 111, 111], logprob=-0.0025094282, token='Foo', top_logprobs=[]),
471 ChatCompletionTokenLogprob(bytes=[33], logprob=-0.26638845, token='!', top_logprobs=[])
472 ],
473 type='logprobs.content.delta'
474 ),
475 LogprobsContentDoneEvent(
476 content=[
477 ChatCompletionTokenLogprob(bytes=[70, 111, 111], logprob=-0.0025094282, token='Foo', top_logprobs=[]),
478 ChatCompletionTokenLogprob(bytes=[33], logprob=-0.26638845, token='!', top_logprobs=[])
479 ],
480 type='logprobs.content.done'
481 )
482]
483""")
484
485 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot("""\
486[
487 ParsedChoice[NoneType](
488 finish_reason='stop',
489 index=0,
490 logprobs=ChoiceLogprobs(
491 content=[
492 ChatCompletionTokenLogprob(bytes=[70, 111, 111], logprob=-0.0025094282, token='Foo', top_logprobs=[]),
493 ChatCompletionTokenLogprob(bytes=[33], logprob=-0.26638845, token='!', top_logprobs=[])
494 ],
495 refusal=None
496 ),
497 message=ParsedChatCompletionMessage[NoneType](
498 audio=None,
499 content='Foo!',
500 function_call=None,
501 parsed=None,
502 refusal=None,
503 role='assistant',
504 tool_calls=[]
505 )
506 )
507]
508""")
509
510
511@pytest.mark.respx(base_url=base_url)
512def test_refusal_logprobs_events(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
513 class Location(BaseModel):
514 city: str
515 temperature: float
516 units: Literal["c", "f"]
517
518 listener = _make_stream_snapshot_request(
519 lambda c: c.beta.chat.completions.stream(
520 model="gpt-4o-2024-08-06",
521 messages=[
522 {
523 "role": "user",
524 "content": "How do I make anthrax?",
525 },
526 ],
527 logprobs=True,
528 response_format=Location,
529 ),
530 content_snapshot=snapshot(external("569c877e6942*.bin")),
531 mock_client=client,
532 respx_mock=respx_mock,
533 )
534
535 assert print_obj([e.type for e in listener.events if e.type.startswith("logprobs")], monkeypatch) == snapshot("""\
536[
537 'logprobs.refusal.delta',
538 'logprobs.refusal.delta',
539 'logprobs.refusal.delta',
540 'logprobs.refusal.delta',
541 'logprobs.refusal.delta',
542 'logprobs.refusal.delta',
543 'logprobs.refusal.delta',
544 'logprobs.refusal.delta',
545 'logprobs.refusal.delta',
546 'logprobs.refusal.delta',
547 'logprobs.refusal.delta',
548 'logprobs.refusal.done'
549]
550""")
551
552 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot("""\
553[
554 ParsedChoice[Location](
555 finish_reason='stop',
556 index=0,
557 logprobs=ChoiceLogprobs(
558 content=None,
559 refusal=[
560 ChatCompletionTokenLogprob(bytes=[73, 39, 109], logprob=-0.0012038043, token="I'm", top_logprobs=[]),
561 ChatCompletionTokenLogprob(
562 bytes=[32, 118, 101, 114, 121],
563 logprob=-0.8438816,
564 token=' very',
565 top_logprobs=[]
566 ),
567 ChatCompletionTokenLogprob(
568 bytes=[32, 115, 111, 114, 114, 121],
569 logprob=-3.4121115e-06,
570 token=' sorry',
571 top_logprobs=[]
572 ),
573 ChatCompletionTokenLogprob(bytes=[44], logprob=-3.3809047e-05, token=',', top_logprobs=[]),
574 ChatCompletionTokenLogprob(
575 bytes=[32, 98, 117, 116],
576 logprob=-0.038048144,
577 token=' but',
578 top_logprobs=[]
579 ),
580 ChatCompletionTokenLogprob(bytes=[32, 73], logprob=-0.0016109125, token=' I', top_logprobs=[]),
581 ChatCompletionTokenLogprob(
582 bytes=[32, 99, 97, 110, 39, 116],
583 logprob=-0.0073532974,
584 token=" can't",
585 top_logprobs=[]
586 ),
587 ChatCompletionTokenLogprob(
588 bytes=[32, 97, 115, 115, 105, 115, 116],
589 logprob=-0.0020837625,
590 token=' assist',
591 top_logprobs=[]
592 ),
593 ChatCompletionTokenLogprob(
594 bytes=[32, 119, 105, 116, 104],
595 logprob=-0.00318354,
596 token=' with',
597 top_logprobs=[]
598 ),
599 ChatCompletionTokenLogprob(
600 bytes=[32, 116, 104, 97, 116],
601 logprob=-0.0017186158,
602 token=' that',
603 top_logprobs=[]
604 ),
605 ChatCompletionTokenLogprob(bytes=[46], logprob=-0.57687104, token='.', top_logprobs=[])
606 ]
607 ),
608 message=ParsedChatCompletionMessage[Location](
609 audio=None,
610 content=None,
611 function_call=None,
612 parsed=None,
613 refusal="I'm very sorry, but I can't assist with that.",
614 role='assistant',
615 tool_calls=[]
616 )
617 )
618]
619""")
620
621
622@pytest.mark.respx(base_url=base_url)
623def test_parse_pydantic_tool(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
624 class GetWeatherArgs(BaseModel):
625 city: str
626 country: str
627 units: Literal["c", "f"] = "c"
628
629 listener = _make_stream_snapshot_request(
630 lambda c: c.beta.chat.completions.stream(
631 model="gpt-4o-2024-08-06",
632 messages=[
633 {
634 "role": "user",
635 "content": "What's the weather like in Edinburgh?",
636 },
637 ],
638 tools=[
639 openai.pydantic_function_tool(GetWeatherArgs),
640 ],
641 ),
642 content_snapshot=snapshot(external("c6aa7e397b71*.bin")),
643 mock_client=client,
644 respx_mock=respx_mock,
645 )
646
647 assert print_obj(listener.stream.current_completion_snapshot.choices, monkeypatch) == snapshot(
648 """\
649[
650 ParsedChoice[object](
651 finish_reason='tool_calls',
652 index=0,
653 logprobs=None,
654 message=ParsedChatCompletionMessage[object](
655 audio=None,
656 content=None,
657 function_call=None,
658 parsed=None,
659 refusal=None,
660 role='assistant',
661 tool_calls=[
662 ParsedFunctionToolCall(
663 function=ParsedFunction(
664 arguments='{"city":"Edinburgh","country":"UK","units":"c"}',
665 name='GetWeatherArgs',
666 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='UK', units='c')
667 ),
668 id='call_c91SqDXlYFuETYv8mUHzz6pp',
669 index=0,
670 type='function'
671 )
672 ]
673 )
674 )
675]
676"""
677 )
678
679 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
680 """\
681[
682 ParsedChoice[NoneType](
683 finish_reason='tool_calls',
684 index=0,
685 logprobs=None,
686 message=ParsedChatCompletionMessage[NoneType](
687 audio=None,
688 content=None,
689 function_call=None,
690 parsed=None,
691 refusal=None,
692 role='assistant',
693 tool_calls=[
694 ParsedFunctionToolCall(
695 function=ParsedFunction(
696 arguments='{"city":"Edinburgh","country":"UK","units":"c"}',
697 name='GetWeatherArgs',
698 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='UK', units='c')
699 ),
700 id='call_c91SqDXlYFuETYv8mUHzz6pp',
701 index=0,
702 type='function'
703 )
704 ]
705 )
706 )
707]
708"""
709 )
710
711
712@pytest.mark.respx(base_url=base_url)
713def test_parse_multiple_pydantic_tools(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
714 class GetWeatherArgs(BaseModel):
715 """Get the temperature for the given country/city combo"""
716
717 city: str
718 country: str
719 units: Literal["c", "f"] = "c"
720
721 class GetStockPrice(BaseModel):
722 ticker: str
723 exchange: str
724
725 listener = _make_stream_snapshot_request(
726 lambda c: c.beta.chat.completions.stream(
727 model="gpt-4o-2024-08-06",
728 messages=[
729 {
730 "role": "user",
731 "content": "What's the weather like in Edinburgh?",
732 },
733 {
734 "role": "user",
735 "content": "What's the price of AAPL?",
736 },
737 ],
738 tools=[
739 openai.pydantic_function_tool(GetWeatherArgs),
740 openai.pydantic_function_tool(
741 GetStockPrice, name="get_stock_price", description="Fetch the latest price for a given ticker"
742 ),
743 ],
744 ),
745 content_snapshot=snapshot(external("f82268f2fefd*.bin")),
746 mock_client=client,
747 respx_mock=respx_mock,
748 )
749
750 assert print_obj(listener.stream.current_completion_snapshot.choices, monkeypatch) == snapshot(
751 """\
752[
753 ParsedChoice[object](
754 finish_reason='tool_calls',
755 index=0,
756 logprobs=None,
757 message=ParsedChatCompletionMessage[object](
758 audio=None,
759 content=None,
760 function_call=None,
761 parsed=None,
762 refusal=None,
763 role='assistant',
764 tool_calls=[
765 ParsedFunctionToolCall(
766 function=ParsedFunction(
767 arguments='{"city": "Edinburgh", "country": "GB", "units": "c"}',
768 name='GetWeatherArgs',
769 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='GB', units='c')
770 ),
771 id='call_JMW1whyEaYG438VE1OIflxA2',
772 index=0,
773 type='function'
774 ),
775 ParsedFunctionToolCall(
776 function=ParsedFunction(
777 arguments='{"ticker": "AAPL", "exchange": "NASDAQ"}',
778 name='get_stock_price',
779 parsed_arguments=GetStockPrice(exchange='NASDAQ', ticker='AAPL')
780 ),
781 id='call_DNYTawLBoN8fj3KN6qU9N1Ou',
782 index=1,
783 type='function'
784 )
785 ]
786 )
787 )
788]
789"""
790 )
791 completion = listener.stream.get_final_completion()
792 assert print_obj(completion.choices[0].message.tool_calls, monkeypatch) == snapshot(
793 """\
794[
795 ParsedFunctionToolCall(
796 function=ParsedFunction(
797 arguments='{"city": "Edinburgh", "country": "GB", "units": "c"}',
798 name='GetWeatherArgs',
799 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='GB', units='c')
800 ),
801 id='call_JMW1whyEaYG438VE1OIflxA2',
802 index=0,
803 type='function'
804 ),
805 ParsedFunctionToolCall(
806 function=ParsedFunction(
807 arguments='{"ticker": "AAPL", "exchange": "NASDAQ"}',
808 name='get_stock_price',
809 parsed_arguments=GetStockPrice(exchange='NASDAQ', ticker='AAPL')
810 ),
811 id='call_DNYTawLBoN8fj3KN6qU9N1Ou',
812 index=1,
813 type='function'
814 )
815]
816"""
817 )
818
819
820@pytest.mark.respx(base_url=base_url)
821def test_parse_strict_tools(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
822 listener = _make_stream_snapshot_request(
823 lambda c: c.beta.chat.completions.stream(
824 model="gpt-4o-2024-08-06",
825 messages=[
826 {
827 "role": "user",
828 "content": "What's the weather like in SF?",
829 },
830 ],
831 tools=[
832 {
833 "type": "function",
834 "function": {
835 "name": "get_weather",
836 "parameters": {
837 "type": "object",
838 "properties": {
839 "city": {"type": "string"},
840 "state": {"type": "string"},
841 },
842 "required": [
843 "city",
844 "state",
845 ],
846 "additionalProperties": False,
847 },
848 "strict": True,
849 },
850 }
851 ],
852 ),
853 content_snapshot=snapshot(external("a247c49c5fcd*.bin")),
854 mock_client=client,
855 respx_mock=respx_mock,
856 )
857
858 assert print_obj(listener.stream.current_completion_snapshot.choices, monkeypatch) == snapshot(
859 """\
860[
861 ParsedChoice[object](
862 finish_reason='tool_calls',
863 index=0,
864 logprobs=None,
865 message=ParsedChatCompletionMessage[object](
866 audio=None,
867 content=None,
868 function_call=None,
869 parsed=None,
870 refusal=None,
871 role='assistant',
872 tool_calls=[
873 ParsedFunctionToolCall(
874 function=ParsedFunction(
875 arguments='{"city":"San Francisco","state":"CA"}',
876 name='get_weather',
877 parsed_arguments={'city': 'San Francisco', 'state': 'CA'}
878 ),
879 id='call_CTf1nWJLqSeRgDqaCG27xZ74',
880 index=0,
881 type='function'
882 )
883 ]
884 )
885 )
886]
887"""
888 )
889
890
891@pytest.mark.respx(base_url=base_url)
892def test_non_pydantic_response_format(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
893 listener = _make_stream_snapshot_request(
894 lambda c: c.beta.chat.completions.stream(
895 model="gpt-4o-2024-08-06",
896 messages=[
897 {
898 "role": "user",
899 "content": "What's the weather like in SF? Give me any JSON back",
900 },
901 ],
902 response_format={"type": "json_object"},
903 ),
904 content_snapshot=snapshot(external("d61558011839*.bin")),
905 mock_client=client,
906 respx_mock=respx_mock,
907 )
908
909 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
910 """\
911[
912 ParsedChoice[NoneType](
913 finish_reason='stop',
914 index=0,
915 logprobs=None,
916 message=ParsedChatCompletionMessage[NoneType](
917 audio=None,
918 content='\\n {\\n "location": "San Francisco, CA",\\n "weather": {\\n "temperature": "18°C",\\n
919"condition": "Partly Cloudy",\\n "humidity": "72%",\\n "windSpeed": "15 km/h",\\n "windDirection": "NW"\\n
920},\\n "forecast": [\\n {\\n "day": "Monday",\\n "high": "20°C",\\n "low": "14°C",\\n
921"condition": "Sunny"\\n },\\n {\\n "day": "Tuesday",\\n "high": "19°C",\\n "low": "15°C",\\n
922"condition": "Mostly Cloudy"\\n },\\n {\\n "day": "Wednesday",\\n "high": "18°C",\\n "low":
923"14°C",\\n "condition": "Cloudy"\\n }\\n ]\\n }\\n',
924 function_call=None,
925 parsed=None,
926 refusal=None,
927 role='assistant',
928 tool_calls=[]
929 )
930 )
931]
932"""
933 )
934
935
936@pytest.mark.respx(base_url=base_url)
937def test_allows_non_strict_tools_but_no_parsing(
938 client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
939) -> None:
940 listener = _make_stream_snapshot_request(
941 lambda c: c.beta.chat.completions.stream(
942 model="gpt-4o-2024-08-06",
943 messages=[{"role": "user", "content": "what's the weather in NYC?"}],
944 tools=[
945 {
946 "type": "function",
947 "function": {
948 "name": "get_weather",
949 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
950 },
951 }
952 ],
953 ),
954 content_snapshot=snapshot(external("2018feb66ae1*.bin")),
955 mock_client=client,
956 respx_mock=respx_mock,
957 )
958
959 assert print_obj(listener.get_event_by_type("tool_calls.function.arguments.done"), monkeypatch) == snapshot("""\
960FunctionToolCallArgumentsDoneEvent(
961 arguments='{"city":"New York City"}',
962 index=0,
963 name='get_weather',
964 parsed_arguments=None,
965 type='tool_calls.function.arguments.done'
966)
967""")
968
969 assert print_obj(listener.stream.get_final_completion().choices, monkeypatch) == snapshot(
970 """\
971[
972 ParsedChoice[NoneType](
973 finish_reason='tool_calls',
974 index=0,
975 logprobs=None,
976 message=ParsedChatCompletionMessage[NoneType](
977 audio=None,
978 content=None,
979 function_call=None,
980 parsed=None,
981 refusal=None,
982 role='assistant',
983 tool_calls=[
984 ParsedFunctionToolCall(
985 function=ParsedFunction(
986 arguments='{"city":"New York City"}',
987 name='get_weather',
988 parsed_arguments=None
989 ),
990 id='call_4XzlGBLtUe9dy3GVNV4jhq7h',
991 index=0,
992 type='function'
993 )
994 ]
995 )
996 )
997]
998"""
999 )
1000
1001
1002@pytest.mark.respx(base_url=base_url)
1003def test_chat_completion_state_helper(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
1004 state = ChatCompletionStreamState()
1005
1006 def streamer(client: OpenAI) -> Iterator[ChatCompletionChunk]:
1007 stream = client.chat.completions.create(
1008 model="gpt-4o-2024-08-06",
1009 messages=[
1010 {
1011 "role": "user",
1012 "content": "What's the weather like in SF?",
1013 },
1014 ],
1015 stream=True,
1016 )
1017 for chunk in stream:
1018 state.handle_chunk(chunk)
1019 yield chunk
1020
1021 _make_raw_stream_snapshot_request(
1022 streamer,
1023 content_snapshot=snapshot(external("e2aad469b71d*.bin")),
1024 mock_client=client,
1025 respx_mock=respx_mock,
1026 )
1027
1028 assert print_obj(state.get_final_completion().choices, monkeypatch) == snapshot(
1029 """\
1030[
1031 ParsedChoice[NoneType](
1032 finish_reason='stop',
1033 index=0,
1034 logprobs=None,
1035 message=ParsedChatCompletionMessage[NoneType](
1036 audio=None,
1037 content="I'm unable to provide real-time weather updates. To get the current weather in San Francisco, I
1038recommend checking a reliable weather website or a weather app.",
1039 function_call=None,
1040 parsed=None,
1041 refusal=None,
1042 role='assistant',
1043 tool_calls=[]
1044 )
1045 )
1046]
1047"""
1048 )
1049
1050
1051@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
1052def test_stream_method_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None:
1053 checking_client: OpenAI | AsyncOpenAI = client if sync else async_client
1054
1055 assert_signatures_in_sync(
1056 checking_client.chat.completions.create,
1057 checking_client.beta.chat.completions.stream,
1058 exclude_params={"response_format", "stream"},
1059 )
1060
1061
1062class StreamListener(Generic[ResponseFormatT]):
1063 def __init__(self, stream: ChatCompletionStream[ResponseFormatT]) -> None:
1064 self.stream = stream
1065 self.events: list[ChatCompletionStreamEvent[ResponseFormatT]] = []
1066
1067 def __iter__(self) -> Iterator[ChatCompletionStreamEvent[ResponseFormatT]]:
1068 for event in self.stream:
1069 self.events.append(event)
1070 yield event
1071
1072 @overload
1073 def get_event_by_type(self, event_type: Literal["content.done"]) -> ContentDoneEvent[ResponseFormatT] | None: ...
1074
1075 @overload
1076 def get_event_by_type(self, event_type: str) -> ChatCompletionStreamEvent[ResponseFormatT] | None: ...
1077
1078 def get_event_by_type(self, event_type: str) -> ChatCompletionStreamEvent[ResponseFormatT] | None:
1079 return next((e for e in self.events if e.type == event_type), None)
1080
1081
1082def _make_stream_snapshot_request(
1083 func: Callable[[OpenAI], ChatCompletionStreamManager[ResponseFormatT]],
1084 *,
1085 content_snapshot: Any,
1086 respx_mock: MockRouter,
1087 mock_client: OpenAI,
1088 on_event: Callable[[ChatCompletionStream[ResponseFormatT], ChatCompletionStreamEvent[ResponseFormatT]], Any]
1089 | None = None,
1090) -> StreamListener[ResponseFormatT]:
1091 live = os.environ.get("OPENAI_LIVE") == "1"
1092 if live:
1093
1094 def _on_response(response: httpx.Response) -> None:
1095 # update the content snapshot
1096 assert outsource(response.read()) == content_snapshot
1097
1098 respx_mock.stop()
1099
1100 client = OpenAI(
1101 http_client=httpx.Client(
1102 event_hooks={
1103 "response": [_on_response],
1104 }
1105 )
1106 )
1107 else:
1108 respx_mock.post("/chat/completions").mock(
1109 return_value=httpx.Response(
1110 200,
1111 content=content_snapshot._old_value._load_value(),
1112 headers={"content-type": "text/event-stream"},
1113 )
1114 )
1115
1116 client = mock_client
1117
1118 with func(client) as stream:
1119 listener = StreamListener(stream)
1120
1121 for event in listener:
1122 if on_event:
1123 on_event(stream, event)
1124
1125 if live:
1126 client.close()
1127
1128 return listener
1129
1130
1131def _make_raw_stream_snapshot_request(
1132 func: Callable[[OpenAI], Iterator[ChatCompletionChunk]],
1133 *,
1134 content_snapshot: Any,
1135 respx_mock: MockRouter,
1136 mock_client: OpenAI,
1137) -> None:
1138 live = os.environ.get("OPENAI_LIVE") == "1"
1139 if live:
1140
1141 def _on_response(response: httpx.Response) -> None:
1142 # update the content snapshot
1143 assert outsource(response.read()) == content_snapshot
1144
1145 respx_mock.stop()
1146
1147 client = OpenAI(
1148 http_client=httpx.Client(
1149 event_hooks={
1150 "response": [_on_response],
1151 }
1152 )
1153 )
1154 else:
1155 respx_mock.post("/chat/completions").mock(
1156 return_value=httpx.Response(
1157 200,
1158 content=content_snapshot._old_value._load_value(),
1159 headers={"content-type": "text/event-stream"},
1160 )
1161 )
1162
1163 client = mock_client
1164
1165 stream = func(client)
1166 consume_sync_iterator(stream)
1167
1168 if live:
1169 client.close()