openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.41.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/lib/chat/test_completions.py

747lines · modecode

1from __future__ import annotations
2
3import os
4import json
5from enum import Enum
6from typing import Any, Callable, Optional
7from typing_extensions import Literal, TypeVar
8
9import httpx
10import pytest
11from respx import MockRouter
12from pydantic import Field, BaseModel
13from inline_snapshot import snapshot
14
15import openai
16from openai import OpenAI, AsyncOpenAI
17from openai._utils import assert_signatures_in_sync
18from openai._compat import PYDANTIC_V2
19
20from ._utils import print_obj
21from ...conftest import base_url
22from ..schema_types.query import Query
23
24_T = TypeVar("_T")
25
26# all the snapshots in this file are auto-generated from the live API
27#
28# you can update them with
29#
30# `OPENAI_LIVE=1 pytest --inline-snapshot=fix`
31
32
33@pytest.mark.respx(base_url=base_url)
34def test_parse_nothing(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
35 completion = _make_snapshot_request(
36 lambda c: c.beta.chat.completions.parse(
37 model="gpt-4o-2024-08-06",
38 messages=[
39 {
40 "role": "user",
41 "content": "What's the weather like in SF?",
42 },
43 ],
44 ),
45 content_snapshot=snapshot(
46 '{"id": "chatcmpl-9tXjSozlYq8oGdlRH3vgLsiUNRg8c", "object": "chat.completion", "created": 1723024734, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "I\'m unable to provide real-time weather updates. To find out the current weather in San Francisco, please check a reliable weather website or app.", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 14, "completion_tokens": 28, "total_tokens": 42}, "system_fingerprint": "fp_845eaabc1f"}'
47 ),
48 mock_client=client,
49 respx_mock=respx_mock,
50 )
51
52 assert print_obj(completion, monkeypatch) == snapshot(
53 """\
54ParsedChatCompletion[NoneType](
55 choices=[
56 ParsedChoice[NoneType](
57 finish_reason='stop',
58 index=0,
59 logprobs=None,
60 message=ParsedChatCompletionMessage[NoneType](
61 content="I'm unable to provide real-time weather updates. To find out the current weather in San
62Francisco, please check a reliable weather website or app.",
63 function_call=None,
64 parsed=None,
65 refusal=None,
66 role='assistant',
67 tool_calls=[]
68 )
69 )
70 ],
71 created=1723024734,
72 id='chatcmpl-9tXjSozlYq8oGdlRH3vgLsiUNRg8c',
73 model='gpt-4o-2024-08-06',
74 object='chat.completion',
75 service_tier=None,
76 system_fingerprint='fp_845eaabc1f',
77 usage=CompletionUsage(completion_tokens=28, prompt_tokens=14, total_tokens=42)
78)
79"""
80 )
81
82
83@pytest.mark.respx(base_url=base_url)
84def test_parse_pydantic_model(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
85 class Location(BaseModel):
86 city: str
87 temperature: float
88 units: Literal["c", "f"]
89
90 completion = _make_snapshot_request(
91 lambda c: c.beta.chat.completions.parse(
92 model="gpt-4o-2024-08-06",
93 messages=[
94 {
95 "role": "user",
96 "content": "What's the weather like in SF?",
97 },
98 ],
99 response_format=Location,
100 ),
101 content_snapshot=snapshot(
102 '{"id": "chatcmpl-9tXjTNupyDe7nL1Z8eOO6BdSyrHAD", "object": "chat.completion", "created": 1723024735, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"city\\":\\"San Francisco\\",\\"temperature\\":56,\\"units\\":\\"f\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 17, "completion_tokens": 14, "total_tokens": 31}, "system_fingerprint": "fp_2a322c9ffc"}'
103 ),
104 mock_client=client,
105 respx_mock=respx_mock,
106 )
107
108 assert print_obj(completion, monkeypatch) == snapshot(
109 """\
110ParsedChatCompletion[Location](
111 choices=[
112 ParsedChoice[Location](
113 finish_reason='stop',
114 index=0,
115 logprobs=None,
116 message=ParsedChatCompletionMessage[Location](
117 content='{"city":"San Francisco","temperature":56,"units":"f"}',
118 function_call=None,
119 parsed=Location(city='San Francisco', temperature=56.0, units='f'),
120 refusal=None,
121 role='assistant',
122 tool_calls=[]
123 )
124 )
125 ],
126 created=1723024735,
127 id='chatcmpl-9tXjTNupyDe7nL1Z8eOO6BdSyrHAD',
128 model='gpt-4o-2024-08-06',
129 object='chat.completion',
130 service_tier=None,
131 system_fingerprint='fp_2a322c9ffc',
132 usage=CompletionUsage(completion_tokens=14, prompt_tokens=17, total_tokens=31)
133)
134"""
135 )
136
137
138@pytest.mark.respx(base_url=base_url)
139def test_parse_pydantic_model_optional_default(
140 client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
141) -> None:
142 class Location(BaseModel):
143 city: str
144 temperature: float
145 units: Optional[Literal["c", "f"]] = None
146
147 completion = _make_snapshot_request(
148 lambda c: c.beta.chat.completions.parse(
149 model="gpt-4o-2024-08-06",
150 messages=[
151 {
152 "role": "user",
153 "content": "What's the weather like in SF?",
154 },
155 ],
156 response_format=Location,
157 ),
158 content_snapshot=snapshot(
159 '{"id": "chatcmpl-9y39Q2jGzWmeEZlm5CoNVOuQzcxP4", "object": "chat.completion", "created": 1724098820, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"city\\":\\"San Francisco\\",\\"temperature\\":62,\\"units\\":\\"f\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 17, "completion_tokens": 14, "total_tokens": 31}, "system_fingerprint": "fp_2a322c9ffc"}'
160 ),
161 mock_client=client,
162 respx_mock=respx_mock,
163 )
164
165 assert print_obj(completion, monkeypatch) == snapshot(
166 """\
167ParsedChatCompletion[Location](
168 choices=[
169 ParsedChoice[Location](
170 finish_reason='stop',
171 index=0,
172 logprobs=None,
173 message=ParsedChatCompletionMessage[Location](
174 content='{"city":"San Francisco","temperature":62,"units":"f"}',
175 function_call=None,
176 parsed=Location(city='San Francisco', temperature=62.0, units='f'),
177 refusal=None,
178 role='assistant',
179 tool_calls=[]
180 )
181 )
182 ],
183 created=1724098820,
184 id='chatcmpl-9y39Q2jGzWmeEZlm5CoNVOuQzcxP4',
185 model='gpt-4o-2024-08-06',
186 object='chat.completion',
187 service_tier=None,
188 system_fingerprint='fp_2a322c9ffc',
189 usage=CompletionUsage(completion_tokens=14, prompt_tokens=17, total_tokens=31)
190)
191"""
192 )
193
194
195@pytest.mark.respx(base_url=base_url)
196def test_parse_pydantic_model_enum(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
197 class Color(Enum):
198 """The detected color"""
199
200 RED = "red"
201 BLUE = "blue"
202 GREEN = "green"
203
204 class ColorDetection(BaseModel):
205 color: Color
206 hex_color_code: str = Field(description="The hex color code of the detected color")
207
208 if not PYDANTIC_V2:
209 ColorDetection.update_forward_refs(**locals()) # type: ignore
210
211 completion = _make_snapshot_request(
212 lambda c: c.beta.chat.completions.parse(
213 model="gpt-4o-2024-08-06",
214 messages=[
215 {"role": "user", "content": "What color is a Coke can?"},
216 ],
217 response_format=ColorDetection,
218 ),
219 content_snapshot=snapshot(
220 '{"id": "chatcmpl-9vK4UZVr385F2UgZlP1ShwPn2nFxG", "object": "chat.completion", "created": 1723448878, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"color\\":\\"red\\",\\"hex_color_code\\":\\"#FF0000\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 18, "completion_tokens": 14, "total_tokens": 32}, "system_fingerprint": "fp_845eaabc1f"}'
221 ),
222 mock_client=client,
223 respx_mock=respx_mock,
224 )
225
226 assert print_obj(completion.choices[0], monkeypatch) == snapshot(
227 """\
228ParsedChoice[ColorDetection](
229 finish_reason='stop',
230 index=0,
231 logprobs=None,
232 message=ParsedChatCompletionMessage[ColorDetection](
233 content='{"color":"red","hex_color_code":"#FF0000"}',
234 function_call=None,
235 parsed=ColorDetection(color=<Color.RED: 'red'>, hex_color_code='#FF0000'),
236 refusal=None,
237 role='assistant',
238 tool_calls=[]
239 )
240)
241"""
242 )
243
244
245@pytest.mark.respx(base_url=base_url)
246def test_parse_pydantic_model_multiple_choices(
247 client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
248) -> None:
249 class Location(BaseModel):
250 city: str
251 temperature: float
252 units: Literal["c", "f"]
253
254 completion = _make_snapshot_request(
255 lambda c: c.beta.chat.completions.parse(
256 model="gpt-4o-2024-08-06",
257 messages=[
258 {
259 "role": "user",
260 "content": "What's the weather like in SF?",
261 },
262 ],
263 n=3,
264 response_format=Location,
265 ),
266 content_snapshot=snapshot(
267 '{"id": "chatcmpl-9tXjUrNFyyjSB2FJ842TMDNRM6Gen", "object": "chat.completion", "created": 1723024736, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"city\\":\\"San Francisco\\",\\"temperature\\":58,\\"units\\":\\"f\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}, {"index": 1, "message": {"role": "assistant", "content": "{\\"city\\":\\"San Francisco\\",\\"temperature\\":58,\\"units\\":\\"f\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}, {"index": 2, "message": {"role": "assistant", "content": "{\\"city\\":\\"San Francisco\\",\\"temperature\\":63,\\"units\\":\\"f\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 17, "completion_tokens": 42, "total_tokens": 59}, "system_fingerprint": "fp_845eaabc1f"}'
268 ),
269 mock_client=client,
270 respx_mock=respx_mock,
271 )
272
273 assert print_obj(completion.choices, monkeypatch) == snapshot(
274 """\
275[
276 ParsedChoice[Location](
277 finish_reason='stop',
278 index=0,
279 logprobs=None,
280 message=ParsedChatCompletionMessage[Location](
281 content='{"city":"San Francisco","temperature":58,"units":"f"}',
282 function_call=None,
283 parsed=Location(city='San Francisco', temperature=58.0, units='f'),
284 refusal=None,
285 role='assistant',
286 tool_calls=[]
287 )
288 ),
289 ParsedChoice[Location](
290 finish_reason='stop',
291 index=1,
292 logprobs=None,
293 message=ParsedChatCompletionMessage[Location](
294 content='{"city":"San Francisco","temperature":58,"units":"f"}',
295 function_call=None,
296 parsed=Location(city='San Francisco', temperature=58.0, units='f'),
297 refusal=None,
298 role='assistant',
299 tool_calls=[]
300 )
301 ),
302 ParsedChoice[Location](
303 finish_reason='stop',
304 index=2,
305 logprobs=None,
306 message=ParsedChatCompletionMessage[Location](
307 content='{"city":"San Francisco","temperature":63,"units":"f"}',
308 function_call=None,
309 parsed=Location(city='San Francisco', temperature=63.0, units='f'),
310 refusal=None,
311 role='assistant',
312 tool_calls=[]
313 )
314 )
315]
316"""
317 )
318
319
320@pytest.mark.respx(base_url=base_url)
321def test_pydantic_tool_model_all_types(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
322 completion = _make_snapshot_request(
323 lambda c: c.beta.chat.completions.parse(
324 model="gpt-4o-2024-08-06",
325 messages=[
326 {
327 "role": "user",
328 "content": "look up all my orders in may of last year that were fulfilled but not delivered on time",
329 },
330 ],
331 tools=[openai.pydantic_function_tool(Query)],
332 response_format=Query,
333 ),
334 content_snapshot=snapshot(
335 '{"id": "chatcmpl-9tXjVJVCLTn7CWFhpjETixvvApCk3", "object": "chat.completion", "created": 1723024737, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "tool_calls": [{"id": "call_Un4g0IXeQGOyqKBS3zhqNCox", "type": "function", "function": {"name": "Query", "arguments": "{\\"table_name\\":\\"orders\\",\\"columns\\":[\\"id\\",\\"status\\",\\"expected_delivery_date\\",\\"delivered_at\\",\\"shipped_at\\",\\"ordered_at\\"],\\"conditions\\":[{\\"column\\":\\"ordered_at\\",\\"operator\\":\\">=\\",\\"value\\":\\"2022-05-01\\"},{\\"column\\":\\"ordered_at\\",\\"operator\\":\\"<=\\",\\"value\\":\\"2022-05-31\\"},{\\"column\\":\\"status\\",\\"operator\\":\\"=\\",\\"value\\":\\"fulfilled\\"},{\\"column\\":\\"delivered_at\\",\\"operator\\":\\">\\",\\"value\\":{\\"column_name\\":\\"expected_delivery_date\\"}}],\\"order_by\\":\\"asc\\"}"}}], "refusal": null}, "logprobs": null, "finish_reason": "tool_calls"}], "usage": {"prompt_tokens": 195, "completion_tokens": 114, "total_tokens": 309}, "system_fingerprint": "fp_845eaabc1f"}'
336 ),
337 mock_client=client,
338 respx_mock=respx_mock,
339 )
340
341 assert print_obj(completion.choices[0], monkeypatch) == snapshot(
342 """\
343ParsedChoice[Query](
344 finish_reason='tool_calls',
345 index=0,
346 logprobs=None,
347 message=ParsedChatCompletionMessage[Query](
348 content=None,
349 function_call=None,
350 parsed=None,
351 refusal=None,
352 role='assistant',
353 tool_calls=[
354 ParsedFunctionToolCall(
355 function=ParsedFunction(
356 arguments='{"table_name":"orders","columns":["id","status","expected_delivery_date","delivered_at","
357shipped_at","ordered_at"],"conditions":[{"column":"ordered_at","operator":">=","value":"2022-05-01"},{"column":"ordered_
358at","operator":"<=","value":"2022-05-31"},{"column":"status","operator":"=","value":"fulfilled"},{"column":"delivered_at
359","operator":">","value":{"column_name":"expected_delivery_date"}}],"order_by":"asc"}',
360 name='Query',
361 parsed_arguments=Query(
362 columns=[
363 <Column.id: 'id'>,
364 <Column.status: 'status'>,
365 <Column.expected_delivery_date: 'expected_delivery_date'>,
366 <Column.delivered_at: 'delivered_at'>,
367 <Column.shipped_at: 'shipped_at'>,
368 <Column.ordered_at: 'ordered_at'>
369 ],
370 conditions=[
371 Condition(column='ordered_at', operator=<Operator.ge: '>='>, value='2022-05-01'),
372 Condition(column='ordered_at', operator=<Operator.le: '<='>, value='2022-05-31'),
373 Condition(column='status', operator=<Operator.eq: '='>, value='fulfilled'),
374 Condition(
375 column='delivered_at',
376 operator=<Operator.gt: '>'>,
377 value=DynamicValue(column_name='expected_delivery_date')
378 )
379 ],
380 name=None,
381 order_by=<OrderBy.asc: 'asc'>,
382 table_name=<Table.orders: 'orders'>
383 )
384 ),
385 id='call_Un4g0IXeQGOyqKBS3zhqNCox',
386 type='function'
387 )
388 ]
389 )
390)
391"""
392 )
393
394
395@pytest.mark.respx(base_url=base_url)
396def test_parse_max_tokens_reached(client: OpenAI, respx_mock: MockRouter) -> None:
397 class Location(BaseModel):
398 city: str
399 temperature: float
400 units: Literal["c", "f"]
401
402 with pytest.raises(openai.LengthFinishReasonError):
403 _make_snapshot_request(
404 lambda c: c.beta.chat.completions.parse(
405 model="gpt-4o-2024-08-06",
406 messages=[
407 {
408 "role": "user",
409 "content": "What's the weather like in SF?",
410 },
411 ],
412 max_tokens=1,
413 response_format=Location,
414 ),
415 content_snapshot=snapshot(
416 '{"id": "chatcmpl-9tXjYACgVKixKdMv2nVQqDVELkdSF", "object": "chat.completion", "created": 1723024740, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"", "refusal": null}, "logprobs": null, "finish_reason": "length"}], "usage": {"prompt_tokens": 17, "completion_tokens": 1, "total_tokens": 18}, "system_fingerprint": "fp_2a322c9ffc"}'
417 ),
418 mock_client=client,
419 respx_mock=respx_mock,
420 )
421
422
423@pytest.mark.respx(base_url=base_url)
424def test_parse_pydantic_model_refusal(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
425 class Location(BaseModel):
426 city: str
427 temperature: float
428 units: Literal["c", "f"]
429
430 completion = _make_snapshot_request(
431 lambda c: c.beta.chat.completions.parse(
432 model="gpt-4o-2024-08-06",
433 messages=[
434 {
435 "role": "user",
436 "content": "How do I make anthrax?",
437 },
438 ],
439 response_format=Location,
440 ),
441 content_snapshot=snapshot(
442 '{"id": "chatcmpl-9tXm7FnIj3hSot5xM4c954MIePle0", "object": "chat.completion", "created": 1723024899, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "refusal": "I\'m very sorry, but I can\'t assist with that request."}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 17, "completion_tokens": 13, "total_tokens": 30}, "system_fingerprint": "fp_845eaabc1f"}'
443 ),
444 mock_client=client,
445 respx_mock=respx_mock,
446 )
447
448 assert print_obj(completion.choices, monkeypatch) == snapshot(
449 """\
450[
451 ParsedChoice[Location](
452 finish_reason='stop',
453 index=0,
454 logprobs=None,
455 message=ParsedChatCompletionMessage[Location](
456 content=None,
457 function_call=None,
458 parsed=None,
459 refusal="I'm very sorry, but I can't assist with that request.",
460 role='assistant',
461 tool_calls=[]
462 )
463 )
464]
465"""
466 )
467
468
469@pytest.mark.respx(base_url=base_url)
470def test_parse_pydantic_tool(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
471 class GetWeatherArgs(BaseModel):
472 city: str
473 country: str
474 units: Literal["c", "f"] = "c"
475
476 completion = _make_snapshot_request(
477 lambda c: c.beta.chat.completions.parse(
478 model="gpt-4o-2024-08-06",
479 messages=[
480 {
481 "role": "user",
482 "content": "What's the weather like in Edinburgh?",
483 },
484 ],
485 tools=[
486 openai.pydantic_function_tool(GetWeatherArgs),
487 ],
488 ),
489 content_snapshot=snapshot(
490 '{"id": "chatcmpl-9tXjbQ9V0l5XPlynOJHKvrWsJQymO", "object": "chat.completion", "created": 1723024743, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "tool_calls": [{"id": "call_EEaIYq8aTdiDWro8jILNl3XK", "type": "function", "function": {"name": "GetWeatherArgs", "arguments": "{\\"city\\":\\"Edinburgh\\",\\"country\\":\\"GB\\",\\"units\\":\\"c\\"}"}}], "refusal": null}, "logprobs": null, "finish_reason": "tool_calls"}], "usage": {"prompt_tokens": 76, "completion_tokens": 24, "total_tokens": 100}, "system_fingerprint": "fp_2a322c9ffc"}'
491 ),
492 mock_client=client,
493 respx_mock=respx_mock,
494 )
495
496 assert print_obj(completion.choices, monkeypatch) == snapshot(
497 """\
498[
499 ParsedChoice[NoneType](
500 finish_reason='tool_calls',
501 index=0,
502 logprobs=None,
503 message=ParsedChatCompletionMessage[NoneType](
504 content=None,
505 function_call=None,
506 parsed=None,
507 refusal=None,
508 role='assistant',
509 tool_calls=[
510 ParsedFunctionToolCall(
511 function=ParsedFunction(
512 arguments='{"city":"Edinburgh","country":"GB","units":"c"}',
513 name='GetWeatherArgs',
514 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='GB', units='c')
515 ),
516 id='call_EEaIYq8aTdiDWro8jILNl3XK',
517 type='function'
518 )
519 ]
520 )
521 )
522]
523"""
524 )
525
526
527@pytest.mark.respx(base_url=base_url)
528def test_parse_multiple_pydantic_tools(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
529 class GetWeatherArgs(BaseModel):
530 """Get the temperature for the given country/city combo"""
531
532 city: str
533 country: str
534 units: Literal["c", "f"] = "c"
535
536 class GetStockPrice(BaseModel):
537 ticker: str
538 exchange: str
539
540 completion = _make_snapshot_request(
541 lambda c: c.beta.chat.completions.parse(
542 model="gpt-4o-2024-08-06",
543 messages=[
544 {
545 "role": "user",
546 "content": "What's the weather like in Edinburgh?",
547 },
548 {
549 "role": "user",
550 "content": "What's the price of AAPL?",
551 },
552 ],
553 tools=[
554 openai.pydantic_function_tool(GetWeatherArgs),
555 openai.pydantic_function_tool(
556 GetStockPrice, name="get_stock_price", description="Fetch the latest price for a given ticker"
557 ),
558 ],
559 ),
560 content_snapshot=snapshot(
561 '{"id": "chatcmpl-9tXjcnIvzZDXRfLfbVTPNL5963GWw", "object": "chat.completion", "created": 1723024744, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "tool_calls": [{"id": "call_ECSuZ8gcNPPwgt24me91jHsJ", "type": "function", "function": {"name": "GetWeatherArgs", "arguments": "{\\"city\\": \\"Edinburgh\\", \\"country\\": \\"UK\\", \\"units\\": \\"c\\"}"}}, {"id": "call_Z3fM2sNBBGILhMtimk5Y3RQk", "type": "function", "function": {"name": "get_stock_price", "arguments": "{\\"ticker\\": \\"AAPL\\", \\"exchange\\": \\"NASDAQ\\"}"}}], "refusal": null}, "logprobs": null, "finish_reason": "tool_calls"}], "usage": {"prompt_tokens": 149, "completion_tokens": 60, "total_tokens": 209}, "system_fingerprint": "fp_845eaabc1f"}'
562 ),
563 mock_client=client,
564 respx_mock=respx_mock,
565 )
566
567 assert print_obj(completion.choices, monkeypatch) == snapshot(
568 """\
569[
570 ParsedChoice[NoneType](
571 finish_reason='tool_calls',
572 index=0,
573 logprobs=None,
574 message=ParsedChatCompletionMessage[NoneType](
575 content=None,
576 function_call=None,
577 parsed=None,
578 refusal=None,
579 role='assistant',
580 tool_calls=[
581 ParsedFunctionToolCall(
582 function=ParsedFunction(
583 arguments='{"city": "Edinburgh", "country": "UK", "units": "c"}',
584 name='GetWeatherArgs',
585 parsed_arguments=GetWeatherArgs(city='Edinburgh', country='UK', units='c')
586 ),
587 id='call_ECSuZ8gcNPPwgt24me91jHsJ',
588 type='function'
589 ),
590 ParsedFunctionToolCall(
591 function=ParsedFunction(
592 arguments='{"ticker": "AAPL", "exchange": "NASDAQ"}',
593 name='get_stock_price',
594 parsed_arguments=GetStockPrice(exchange='NASDAQ', ticker='AAPL')
595 ),
596 id='call_Z3fM2sNBBGILhMtimk5Y3RQk',
597 type='function'
598 )
599 ]
600 )
601 )
602]
603"""
604 )
605
606
607@pytest.mark.respx(base_url=base_url)
608def test_parse_strict_tools(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
609 completion = _make_snapshot_request(
610 lambda c: c.beta.chat.completions.parse(
611 model="gpt-4o-2024-08-06",
612 messages=[
613 {
614 "role": "user",
615 "content": "What's the weather like in SF?",
616 },
617 ],
618 tools=[
619 {
620 "type": "function",
621 "function": {
622 "name": "get_weather",
623 "parameters": {
624 "type": "object",
625 "properties": {
626 "city": {"type": "string"},
627 "state": {"type": "string"},
628 },
629 "required": [
630 "city",
631 "state",
632 ],
633 "additionalProperties": False,
634 },
635 "strict": True,
636 },
637 }
638 ],
639 ),
640 content_snapshot=snapshot(
641 '{"id": "chatcmpl-9tXjfjETDIqeYvDjsuGACbwdY0xsr", "object": "chat.completion", "created": 1723024747, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "tool_calls": [{"id": "call_7ZZPctBXQWexQlIHSrIHMVUq", "type": "function", "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"San Francisco\\",\\"state\\":\\"CA\\"}"}}], "refusal": null}, "logprobs": null, "finish_reason": "tool_calls"}], "usage": {"prompt_tokens": 48, "completion_tokens": 19, "total_tokens": 67}, "system_fingerprint": "fp_2a322c9ffc"}'
642 ),
643 mock_client=client,
644 respx_mock=respx_mock,
645 )
646
647 assert print_obj(completion.choices, monkeypatch) == snapshot(
648 """\
649[
650 ParsedChoice[NoneType](
651 finish_reason='tool_calls',
652 index=0,
653 logprobs=None,
654 message=ParsedChatCompletionMessage[NoneType](
655 content=None,
656 function_call=None,
657 parsed=None,
658 refusal=None,
659 role='assistant',
660 tool_calls=[
661 ParsedFunctionToolCall(
662 function=ParsedFunction(
663 arguments='{"city":"San Francisco","state":"CA"}',
664 name='get_weather',
665 parsed_arguments={'city': 'San Francisco', 'state': 'CA'}
666 ),
667 id='call_7ZZPctBXQWexQlIHSrIHMVUq',
668 type='function'
669 )
670 ]
671 )
672 )
673]
674"""
675 )
676
677
678def test_parse_non_strict_tools(client: OpenAI) -> None:
679 with pytest.raises(
680 ValueError, match="`get_weather` is not strict. Only `strict` function tools can be auto-parsed"
681 ):
682 client.beta.chat.completions.parse(
683 model="gpt-4o-2024-08-06",
684 messages=[],
685 tools=[
686 {
687 "type": "function",
688 "function": {
689 "name": "get_weather",
690 "parameters": {},
691 },
692 }
693 ],
694 )
695
696
697@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
698def test_parse_method_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None:
699 checking_client: OpenAI | AsyncOpenAI = client if sync else async_client
700
701 assert_signatures_in_sync(
702 checking_client.chat.completions.create,
703 checking_client.beta.chat.completions.parse,
704 exclude_params={"response_format", "stream"},
705 )
706
707
708def _make_snapshot_request(
709 func: Callable[[OpenAI], _T],
710 *,
711 content_snapshot: Any,
712 respx_mock: MockRouter,
713 mock_client: OpenAI,
714) -> _T:
715 live = os.environ.get("OPENAI_LIVE") == "1"
716 if live:
717
718 def _on_response(response: httpx.Response) -> None:
719 # update the content snapshot
720 assert json.dumps(json.loads(response.read())) == content_snapshot
721
722 respx_mock.stop()
723
724 client = OpenAI(
725 http_client=httpx.Client(
726 event_hooks={
727 "response": [_on_response],
728 }
729 )
730 )
731 else:
732 respx_mock.post("/chat/completions").mock(
733 return_value=httpx.Response(
734 200,
735 content=content_snapshot._old_value,
736 headers={"content-type": "application/json"},
737 )
738 )
739
740 client = mock_client
741
742 result = func(client)
743
744 if live:
745 client.close()
746
747 return result
748