Skip to content

Commit 2a77287

Browse files
authored
feat: support OpenAI cache breakpoints and configuration (#128)
1 parent e874eab commit 2a77287

8 files changed

Lines changed: 144 additions & 139 deletions

File tree

README.md

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
- [Make Chat Completions Requests](#make-chat-completions-requests)
2626
- [Without Streaming](#without-streaming)
2727
- [With Streaming](#with-streaming)
28-
- [DIAL-specific and Extended Parameters](#dial-specific-and-extended-parameters)
2928
- [Working with Files](#working-with-files)
3029
- [Working with URLs](#working-with-urls)
3130
- [Uploading Files](#uploading-files)
@@ -465,115 +464,6 @@ ChatCompletionChunk(
465464
)
466465
```
467466

468-
#### DIAL-specific and Extended Parameters
469-
470-
Along with the standard OpenAI parameters, `chat.completions.create` accepts
471-
the DIAL extensions and the newer OpenAI parameters:
472-
473-
```python
474-
completion = client.chat.completions.create(
475-
deployment_name="gpt-4o",
476-
stream=False,
477-
messages=[
478-
# Messages support multi-modal content parts,
479-
# the "developer" role and per-message cache breakpoints
480-
{
481-
"role": "developer",
482-
"content": "Be brief",
483-
"custom_fields": {"cache_breakpoint": {"expire_at": "1h"}},
484-
},
485-
{
486-
"role": "user",
487-
"content": [
488-
{"type": "text", "text": "What is on the picture?"},
489-
{
490-
"type": "image_url",
491-
"image_url": {"url": "https://example.com/image.png"},
492-
},
493-
],
494-
# DIAL attachments, stages and forms
495-
"custom_content": {
496-
"attachments": [
497-
{"type": "image/png", "url": "files/bucket/image.png"}
498-
]
499-
},
500-
},
501-
],
502-
tools=[
503-
{
504-
"type": "function",
505-
"function": {"name": "get_weather", "parameters": {}, "strict": True},
506-
"custom_fields": {"cache_breakpoint": {}},
507-
},
508-
# DIAL static tools, resolved by DIAL Core itself
509-
{
510-
"type": "static_function",
511-
"static_function": {"name": "search", "configuration": {}},
512-
},
513-
],
514-
tool_choice="required",
515-
parallel_tool_calls=False,
516-
reasoning_effort="high",
517-
max_completion_tokens=1000,
518-
response_format={
519-
"type": "json_schema",
520-
"json_schema": {"name": "answer", "schema": {"type": "object"}},
521-
},
522-
stream_options={"include_usage": True},
523-
# DIAL-specific parameters
524-
max_prompt_tokens=8000,
525-
custom_fields={
526-
"configuration": {},
527-
"cache_breakpoint": {"expire_at": "5m"},
528-
},
529-
)
530-
```
531-
532-
The response models cover the DIAL extensions as well:
533-
534-
```pycon
535-
>>> completion.choices[0].message.custom_content
536-
CustomContent(
537-
stages=[
538-
Stage(
539-
index=None,
540-
name='Thinking',
541-
status='completed',
542-
content='...',
543-
attachments=None
544-
)
545-
],
546-
attachments=None,
547-
state=None,
548-
form_value=None,
549-
form_schema=None
550-
)
551-
>>> completion.usage
552-
CompletionUsage(
553-
prompt_tokens=11,
554-
completion_tokens=1,
555-
total_tokens=12,
556-
prompt_tokens_details=PromptTokensDetails(
557-
cached_tokens=8,
558-
cache_write_tokens=3
559-
),
560-
completion_tokens_details=CompletionTokensDetails(reasoning_tokens=1)
561-
)
562-
>>> completion.statistics
563-
Statistics(
564-
usage_per_model=[
565-
UsagePerModel(
566-
index=0,
567-
model='gpt-4o',
568-
prompt_tokens=11,
569-
completion_tokens=1,
570-
total_tokens=12
571-
)
572-
],
573-
discarded_messages=[0, 1]
574-
)
575-
```
576-
577467
### Working with Files
578468

579469
#### Working with URLs

aidial_client/resources/chat/completions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
FunctionCallSpecParam,
2929
FunctionParam,
3030
Message,
31+
PromptCacheOptionsParam,
3132
ReasoningEffort,
3233
ResponseFormat,
3334
StaticToolParam,
@@ -73,6 +74,8 @@ def create(
7374
seed: int | None = None,
7475
user: str | None = None,
7576
reasoning_effort: ReasoningEffort | None = None,
77+
prompt_cache_key: str | None = None,
78+
prompt_cache_options: PromptCacheOptionsParam | None = None,
7679
response_format: ResponseFormat | None = None,
7780
stream_options: StreamOptions | None = None,
7881
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -115,6 +118,8 @@ def create(
115118
seed: int | None = None,
116119
user: str | None = None,
117120
reasoning_effort: ReasoningEffort | None = None,
121+
prompt_cache_key: str | None = None,
122+
prompt_cache_options: PromptCacheOptionsParam | None = None,
118123
response_format: ResponseFormat | None = None,
119124
stream_options: StreamOptions | None = None,
120125
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -156,6 +161,8 @@ def create(
156161
seed: int | None = None,
157162
user: str | None = None,
158163
reasoning_effort: ReasoningEffort | None = None,
164+
prompt_cache_key: str | None = None,
165+
prompt_cache_options: PromptCacheOptionsParam | None = None,
159166
response_format: ResponseFormat | None = None,
160167
stream_options: StreamOptions | None = None,
161168
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -202,6 +209,8 @@ def create(
202209
"max_completion_tokens": max_completion_tokens,
203210
"parallel_tool_calls": parallel_tool_calls,
204211
"reasoning_effort": reasoning_effort,
212+
"prompt_cache_key": prompt_cache_key,
213+
"prompt_cache_options": prompt_cache_options,
205214
"response_format": response_format,
206215
"stream_options": stream_options,
207216
}
@@ -273,6 +282,8 @@ async def create(
273282
seed: int | None = None,
274283
user: str | None = None,
275284
reasoning_effort: ReasoningEffort | None = None,
285+
prompt_cache_key: str | None = None,
286+
prompt_cache_options: PromptCacheOptionsParam | None = None,
276287
response_format: ResponseFormat | None = None,
277288
stream_options: StreamOptions | None = None,
278289
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -315,6 +326,8 @@ async def create(
315326
seed: int | None = None,
316327
user: str | None = None,
317328
reasoning_effort: ReasoningEffort | None = None,
329+
prompt_cache_key: str | None = None,
330+
prompt_cache_options: PromptCacheOptionsParam | None = None,
318331
response_format: ResponseFormat | None = None,
319332
stream_options: StreamOptions | None = None,
320333
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -356,6 +369,8 @@ async def create(
356369
seed: int | None = None,
357370
user: str | None = None,
358371
reasoning_effort: ReasoningEffort | None = None,
372+
prompt_cache_key: str | None = None,
373+
prompt_cache_options: PromptCacheOptionsParam | None = None,
359374
response_format: ResponseFormat | None = None,
360375
stream_options: StreamOptions | None = None,
361376
custom_fields: ChatCompletionRequestCustomFields | None = None,
@@ -402,6 +417,8 @@ async def create(
402417
"max_completion_tokens": max_completion_tokens,
403418
"parallel_tool_calls": parallel_tool_calls,
404419
"reasoning_effort": reasoning_effort,
420+
"prompt_cache_key": prompt_cache_key,
421+
"prompt_cache_options": prompt_cache_options,
405422
"response_format": response_format,
406423
"stream_options": stream_options,
407424
}

aidial_client/types/chat/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from .cache import CacheBreakpointParam
1+
from .cache import (
2+
CacheBreakpointParam,
3+
PromptCacheBreakpointParam,
4+
PromptCacheOptionsParam,
5+
)
26
from .function import FunctionCallSpecParam, FunctionParam
37
from .request import (
48
ChatCompletionRequest,
@@ -89,6 +93,8 @@
8993
"MessageContentRefusalPartParam",
9094
"MessageContentTextPartParam",
9195
"MessageCustomFieldsParam",
96+
"PromptCacheBreakpointParam",
97+
"PromptCacheOptionsParam",
9298
"PromptTokensDetails",
9399
"ReasoningEffort",
94100
"ResponseFormat",

aidial_client/types/chat/cache.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1+
from typing import Literal
2+
13
from typing_extensions import TypedDict
24

35

46
class CacheBreakpointParam(TypedDict, total=False):
57
expire_at: str | None
8+
9+
10+
class PromptCacheBreakpointParam(TypedDict):
11+
mode: Literal["explicit"]
12+
13+
14+
class PromptCacheOptionsParam(TypedDict, total=False):
15+
mode: Literal["implicit", "explicit"] | None
16+
ttl: Literal["30m"] | str | None

aidial_client/types/chat/request.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from typing_extensions import TypedDict
44

5-
from aidial_client.types.chat.cache import CacheBreakpointParam
5+
from aidial_client.types.chat.cache import (
6+
CacheBreakpointParam,
7+
PromptCacheOptionsParam,
8+
)
69
from aidial_client.types.chat.function import (
710
FunctionCallSpecParam,
811
FunctionParam,
@@ -46,6 +49,8 @@ class ChatCompletionRequest(TypedDict, total=False):
4649
logprobs: bool | None
4750
top_logprobs: int | None
4851
reasoning_effort: ReasoningEffort | None
52+
prompt_cache_key: str | None
53+
prompt_cache_options: PromptCacheOptionsParam | None
4954
response_format: ResponseFormat | None
5055
tools: list[ToolParam | StaticToolParam] | None
5156
tool_choice: Literal["none", "auto", "required"] | ToolCallSpecParam | None

aidial_client/types/chat/request_param.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from typing_extensions import Required, TypedDict
44

5-
from aidial_client.types.chat.cache import CacheBreakpointParam
5+
from aidial_client.types.chat.cache import (
6+
CacheBreakpointParam,
7+
PromptCacheBreakpointParam,
8+
)
69
from aidial_client.types.chat.function import FunctionCallParam
710
from aidial_client.types.chat.tool import ToolCallParam
811

@@ -60,19 +63,21 @@ class MessageCustomFieldsParam(TypedDict, total=False):
6063
cache_breakpoint: CacheBreakpointParam | None
6164

6265

63-
class MessageContentTextPartParam(TypedDict):
64-
type: Literal["text"]
65-
text: str
66+
class MessageContentTextPartParam(TypedDict, total=False):
67+
type: Required[Literal["text"]]
68+
text: Required[str]
69+
prompt_cache_breakpoint: PromptCacheBreakpointParam | None
6670

6771

6872
class ImageURLParam(TypedDict, total=False):
6973
url: Required[str]
7074
detail: Literal["auto", "low", "high"] | None
7175

7276

73-
class MessageContentImagePartParam(TypedDict):
74-
type: Literal["image_url"]
75-
image_url: ImageURLParam
77+
class MessageContentImagePartParam(TypedDict, total=False):
78+
type: Required[Literal["image_url"]]
79+
image_url: Required[ImageURLParam]
80+
prompt_cache_breakpoint: PromptCacheBreakpointParam | None
7681

7782

7883
class InputFileParam(TypedDict, total=False):
@@ -81,9 +86,10 @@ class InputFileParam(TypedDict, total=False):
8186
filename: str | None
8287

8388

84-
class MessageContentFilePartParam(TypedDict):
85-
type: Literal["file"]
86-
file: InputFileParam
89+
class MessageContentFilePartParam(TypedDict, total=False):
90+
type: Required[Literal["file"]]
91+
file: Required[InputFileParam]
92+
prompt_cache_breakpoint: PromptCacheBreakpointParam | None
8793

8894

8995
class InputAudioParam(TypedDict):
@@ -92,9 +98,10 @@ class InputAudioParam(TypedDict):
9298
format: str
9399

94100

95-
class MessageContentAudioPartParam(TypedDict):
96-
type: Literal["input_audio"]
97-
input_audio: InputAudioParam
101+
class MessageContentAudioPartParam(TypedDict, total=False):
102+
type: Required[Literal["input_audio"]]
103+
input_audio: Required[InputAudioParam]
104+
prompt_cache_breakpoint: PromptCacheBreakpointParam | None
98105

99106

100107
class MessageContentRefusalPartParam(TypedDict):
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Prompt caching request fields"""
2+
3+
from typing import Any
4+
5+
import pytest
6+
7+
from tests.resources.completions.conftest import GetRequestBody
8+
9+
pytestmark = pytest.mark.asyncio
10+
11+
12+
class TestPromptCache:
13+
"""Prompt cache options must reach the request body as-is"""
14+
15+
async def test_request_prompt_cache_key(
16+
self, get_request_body: GetRequestBody
17+
):
18+
body = await get_request_body(prompt_cache_key="user-42")
19+
20+
assert body["prompt_cache_key"] == "user-42"
21+
22+
async def test_request_prompt_cache_options(
23+
self, get_request_body: GetRequestBody
24+
):
25+
body = await get_request_body(
26+
prompt_cache_options={"mode": "explicit", "ttl": "30m"}
27+
)
28+
29+
assert body["prompt_cache_options"]["mode"] == "explicit"
30+
assert body["prompt_cache_options"]["ttl"] == "30m"
31+
32+
@pytest.mark.parametrize(
33+
"part",
34+
[
35+
{"type": "text", "text": "What is on the picture?"},
36+
{"type": "image_url", "image_url": {"url": "http://a.com/b.png"}},
37+
{"type": "file", "file": {"file_id": "files/bucket/a.pdf"}},
38+
{
39+
"type": "input_audio",
40+
"input_audio": {"data": "Zm9v", "format": "wav"},
41+
},
42+
],
43+
ids=["text", "image_url", "file", "input_audio"],
44+
)
45+
async def test_request_content_part_breakpoint(
46+
self, get_request_body: GetRequestBody, part: dict[str, Any]
47+
):
48+
body = await get_request_body(
49+
messages=[
50+
{
51+
"role": "user",
52+
"content": [
53+
{
54+
**part,
55+
"prompt_cache_breakpoint": {"mode": "explicit"},
56+
}
57+
],
58+
}
59+
]
60+
)
61+
62+
sent_part = body["messages"][0]["content"][0]
63+
assert sent_part["prompt_cache_breakpoint"] == {"mode": "explicit"}

0 commit comments

Comments
 (0)