openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.100.3

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/lib/chat/test_completions_streaming.py

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