openai/openai-python

Public

mirrored from https://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.89.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/lib/chat/test_completions_streaming.py

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