openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dev/hayden/bedrock-aws-auth

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/lib/chat/test_completions_streaming.py

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