Skip to content

Commit 812be52

Browse files
committed
Support OpenAI prompt cache breakpoints
1 parent be75d5e commit 812be52

5 files changed

Lines changed: 108 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@effect/ai-openai": patch
3+
"effect": patch
4+
---
5+
6+
Support OpenAI prompt cache request options and explicit breakpoints on system, developer, and user input text, and correct message constructor provider option types.

packages/ai/openai/src/OpenAiLanguageModel.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export type Model = typeof ResponseModelIds.Encoded | typeof SharedModelIds.Enco
5858
*/
5959
type ImageDetail = "auto" | "low" | "high"
6060

61+
type PromptCacheBreakpoint = { readonly mode: "explicit" }
62+
6163
// =============================================================================
6264
// Configuration
6365
// =============================================================================
@@ -127,6 +129,24 @@ export class Config extends Context.Service<
127129
// =============================================================================
128130

129131
declare module "effect/unstable/ai/Prompt" {
132+
/**
133+
* OpenAI-specific options for system messages.
134+
*
135+
* @category models
136+
* @since 4.0.0
137+
*/
138+
export interface SystemMessageOptions extends ProviderOptions {
139+
/**
140+
* Provider-specific system message options for the OpenAI Responses API.
141+
*/
142+
readonly openai?: {
143+
/**
144+
* Marks the system input text as the end of a reusable prompt prefix.
145+
*/
146+
readonly promptCacheBreakpoint?: PromptCacheBreakpoint | null
147+
} | null
148+
}
149+
130150
/**
131151
* OpenAI-specific options for file prompt parts.
132152
*
@@ -244,6 +264,10 @@ declare module "effect/unstable/ai/Prompt" {
244264
* A list of annotations that apply to the output text.
245265
*/
246266
readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
267+
/**
268+
* Marks the input text as the end of a reusable prompt prefix.
269+
*/
270+
readonly promptCacheBreakpoint?: PromptCacheBreakpoint | null
247271
} | null
248272
}
249273
}
@@ -817,9 +841,16 @@ const prepareMessages = Effect.fnUntraced(
817841
for (const message of prompt.content) {
818842
switch (message.role) {
819843
case "system": {
844+
const promptCacheBreakpoint = getPromptCacheBreakpoint(message)
820845
messages.push({
821846
role: getSystemMessageMode(config.model as string),
822-
content: [{ type: "input_text", text: message.content }]
847+
content: [{
848+
type: "input_text",
849+
text: message.content,
850+
...(Predicate.isNotNull(promptCacheBreakpoint)
851+
? { prompt_cache_breakpoint: promptCacheBreakpoint }
852+
: undefined)
853+
}]
823854
})
824855
break
825856
}
@@ -832,7 +863,14 @@ const prepareMessages = Effect.fnUntraced(
832863

833864
switch (part.type) {
834865
case "text": {
835-
content.push({ type: "input_text", text: part.text })
866+
const promptCacheBreakpoint = getPromptCacheBreakpoint(part)
867+
content.push({
868+
type: "input_text",
869+
text: part.text,
870+
...(Predicate.isNotNull(promptCacheBreakpoint)
871+
? { prompt_cache_breakpoint: promptCacheBreakpoint }
872+
: undefined)
873+
})
836874
break
837875
}
838876

@@ -2910,6 +2948,10 @@ const getEncryptedContent = (
29102948

29112949
const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto"
29122950

2951+
const getPromptCacheBreakpoint = (
2952+
input: Prompt.SystemMessage | Prompt.TextPart
2953+
): PromptCacheBreakpoint | null => input.options.openai?.promptCacheBreakpoint ?? null
2954+
29132955
const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : {}
29142956

29152957
const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>

packages/ai/openai/src/OpenAiSchema.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const MessageRole = Schema.Literals(["system", "developer", "user", "assistant"]
1919

2020
const ImageDetail = Schema.Literals(["low", "high", "auto"])
2121

22+
const PromptCacheBreakpoint = Schema.Struct({
23+
mode: Schema.Literal("explicit")
24+
})
25+
2226
/**
2327
* Schema for optional `include` values supported by the local handwritten
2428
* Responses client schema.
@@ -76,7 +80,8 @@ export type MessageStatus = typeof MessageStatus.Type
7680

7781
const InputTextContent = Schema.Struct({
7882
type: Schema.Literal("input_text"),
79-
text: Schema.String
83+
text: Schema.String,
84+
prompt_cache_breakpoint: Schema.optional(PromptCacheBreakpoint)
8085
})
8186

8287
const InputImageContent = Schema.Struct({
@@ -640,7 +645,7 @@ export type TextResponseFormatConfiguration = typeof TextResponseFormatConfigura
640645
* Validates the Responses API request payload, including input content, model
641646
* selection, instructions, reasoning options, text output format, tools,
642647
* `tool_choice`, streaming, storage, response continuation, sampling options,
643-
* and optional response fields requested through `include`.
648+
* prompt caching, and optional response fields requested through `include`.
644649
*
645650
* **Gotchas**
646651
*
@@ -659,6 +664,11 @@ export const CreateResponse = Schema.Struct({
659664
temperature: Schema.optional(Schema.Finite),
660665
top_p: Schema.optional(Schema.Finite),
661666
user: Schema.optional(Schema.String),
667+
prompt_cache_key: Schema.optional(Schema.String),
668+
prompt_cache_options: Schema.optional(Schema.Struct({
669+
mode: Schema.optional(Schema.Literals(["implicit", "explicit"])),
670+
ttl: Schema.optional(Schema.Literal("30m"))
671+
})),
662672
service_tier: Schema.optional(Schema.String),
663673
previous_response_id: Schema.optional(Schema.String),
664674
model: Schema.optional(Schema.String),

packages/ai/openai/test/OpenAiLanguageModel.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,51 @@ describe("OpenAiLanguageModel", () => {
3131

3232
describe("generateText", () => {
3333
describe("message preparation", () => {
34+
it.effect("forwards prompt cache configuration and text breakpoints", () =>
35+
Effect.gen(function*() {
36+
const breakpoint = { mode: "explicit" } as const
37+
yield* LanguageModel.generateText({
38+
prompt: Prompt.make([
39+
Prompt.systemMessage({
40+
content: "Stable instructions",
41+
options: { openai: { promptCacheBreakpoint: breakpoint } }
42+
}),
43+
Prompt.userMessage({
44+
content: [Prompt.textPart({
45+
text: "Stable context",
46+
options: { openai: { promptCacheBreakpoint: breakpoint } }
47+
})]
48+
})
49+
])
50+
}).pipe(
51+
Effect.provide(OpenAiLanguageModel.model("gpt-5.6", {
52+
prompt_cache_key: "assistant:v1",
53+
prompt_cache_options: { mode: "explicit", ttl: "30m" }
54+
}))
55+
)
56+
57+
const requests = yield* MockHttpClient.requests
58+
const body = yield* getRequestBody(requests[0])
59+
60+
strictEqual(body.prompt_cache_key, "assistant:v1")
61+
deepStrictEqual(body.prompt_cache_options, { mode: "explicit", ttl: "30m" })
62+
deepStrictEqual(body.input, [{
63+
role: "developer",
64+
content: [{
65+
type: "input_text",
66+
text: "Stable instructions",
67+
prompt_cache_breakpoint: breakpoint
68+
}]
69+
}, {
70+
role: "user",
71+
content: [{
72+
type: "input_text",
73+
text: "Stable context",
74+
prompt_cache_breakpoint: breakpoint
75+
}]
76+
}])
77+
}).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.6" as any } }))))
78+
3479
describe("system messages", () => {
3580
it.effect("uses system role for standard models", () =>
3681
Effect.gen(function*() {

packages/effect/src/unstable/ai/Prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,7 @@ export type MessageConstructorParams<M extends Message> = Omit<M, typeof Message
10981098
/**
10991099
* Optional provider-specific options for this message.
11001100
*/
1101-
readonly options?: Part["options"] | undefined
1101+
readonly options?: M["options"] | undefined
11021102
}
11031103

11041104
/**

0 commit comments

Comments
 (0)