From 7b0852ef927d7fca2e94f6c55fa134a9c621efe5 Mon Sep 17 00:00:00 2001 From: Brandon Julio Thenaro Date: Sat, 1 Aug 2026 15:16:17 +0700 Subject: [PATCH] Expose OpenAI prompt cache request options --- .changeset/openai-response-cache-config.md | 5 ++ packages/ai/openai/src/OpenAiSchema.ts | 10 ++- .../openai/test/OpenAiLanguageModel.test.ts | 78 +++++++++++++++++-- packages/ai/openai/test/OpenAiSchema.test.ts | 19 +++++ 4 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 .changeset/openai-response-cache-config.md diff --git a/.changeset/openai-response-cache-config.md b/.changeset/openai-response-cache-config.md new file mode 100644 index 00000000000..838a86c16d2 --- /dev/null +++ b/.changeset/openai-response-cache-config.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Expose Responses API safety identifier and request-level prompt cache options through `OpenAiLanguageModel.Config`. diff --git a/packages/ai/openai/src/OpenAiSchema.ts b/packages/ai/openai/src/OpenAiSchema.ts index 4afd616d228..c6d9e6d7db8 100644 --- a/packages/ai/openai/src/OpenAiSchema.ts +++ b/packages/ai/openai/src/OpenAiSchema.ts @@ -639,7 +639,8 @@ export type TextResponseFormatConfiguration = typeof TextResponseFormatConfigura * Validates the Responses API request payload, including input content, model * selection, instructions, reasoning options, text output format, tools, * `tool_choice`, streaming, storage, response continuation, sampling options, - * and optional response fields requested through `include`. + * safety identification, prompt caching, and optional response fields requested + * through `include`. * * **Gotchas** * @@ -658,6 +659,13 @@ export const CreateResponse = Schema.Struct({ temperature: Schema.optional(Schema.Finite), top_p: Schema.optional(Schema.Finite), user: Schema.optional(Schema.String), + safety_identifier: Schema.optional(Schema.String.check(Schema.isMaxLength(64))), + prompt_cache_key: Schema.optional(Schema.String), + prompt_cache_retention: Schema.optional(Schema.Literals(["in_memory", "24h"])), + prompt_cache_options: Schema.optional(Schema.Struct({ + mode: Schema.optional(Schema.Literals(["implicit", "explicit"])), + ttl: Schema.optional(Schema.Literal("30m")) + })), service_tier: Schema.optional(Schema.String), previous_response_id: Schema.optional(Schema.String), model: Schema.optional(Schema.String), diff --git a/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/packages/ai/openai/test/OpenAiLanguageModel.test.ts index 1250b88b922..b37c6e04777 100644 --- a/packages/ai/openai/test/OpenAiLanguageModel.test.ts +++ b/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -30,6 +30,24 @@ describe("OpenAiLanguageModel", () => { }) describe("generateText", () => { + it.effect("forwards safety and legacy prompt-cache config", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ prompt: "test" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-5.5", { + safety_identifier: "user-hash-123", + prompt_cache_key: "reviewer:v1", + prompt_cache_retention: "24h" + })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.safety_identifier, "user-hash-123") + strictEqual(body.prompt_cache_key, "reviewer:v1") + strictEqual(body.prompt_cache_retention, "24h") + }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.5" as any } })))) + describe("message preparation", () => { describe("system messages", () => { it.effect("uses system role for standard models", () => @@ -913,6 +931,36 @@ describe("OpenAiLanguageModel", () => { }) describe("streamText", () => { + it.effect("forwards safety and GPT-5.6 prompt-cache config", () => + Effect.gen(function*() { + const streamEvents = [{ + type: "response.completed", + sequence_number: 1, + response: makeDefaultResponse({ model: "gpt-5.6" as any }) + }] as unknown as ReadonlyArray + + const requests = yield* Effect.gen(function*() { + yield* LanguageModel.streamText({ prompt: "test" }).pipe(Stream.runCollect) + return yield* MockHttpClient.requests + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-5.6", { + safety_identifier: "user-hash-456", + prompt_cache_key: "reviewer:v2", + prompt_cache_options: { + mode: "implicit", + ttl: "30m" + } + })), + Effect.provide(makeStreamHttpTestLayer(streamEvents)) + ) + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.safety_identifier, "user-hash-456") + strictEqual(body.prompt_cache_key, "reviewer:v2") + deepStrictEqual(body.prompt_cache_options, { mode: "implicit", ttl: "30m" }) + strictEqual(body.stream, true) + })) + it.effect("extracts usage information", () => Effect.gen(function*() { const streamEvents = [ @@ -1363,7 +1411,8 @@ describe("OpenAiLanguageModel", () => { class MockOpenAiResponse extends Context.Service | undefined readonly headers?: Record | undefined }>()("MockOpenAiResponse") {} @@ -1380,7 +1429,7 @@ const encodeResponse = Schema.encodeUnknownEffect(OpenAiSchema.Response) const makeHttpClient = Effect.gen(function*() { const capturedRequests = yield* Ref.make>([]) const response = yield* MockOpenAiResponse - const body = yield* Effect.orDie(encodeResponse(response.body)) + const body = response.body === undefined ? undefined : yield* Effect.orDie(encodeResponse(response.body)) const httpClient = HttpClient.makeWith( Effect.fnUntraced(function*(requestEffect) { @@ -1388,10 +1437,18 @@ const makeHttpClient = Effect.gen(function*() { yield* Ref.update(capturedRequests, Array.append(request)) return HttpClientResponse.fromWeb( request, - new Response(JSON.stringify(body), { - headers: response.headers ?? {}, - status: response.status - }) + new Response( + response.events === undefined + ? JSON.stringify(body) + : response.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), + { + headers: { + "content-type": response.events === undefined ? "application/json" : "text/event-stream", + ...response.headers + }, + status: response.status + } + ) ) }), Effect.succeed as HttpClient.HttpClient.Preprocess @@ -1424,6 +1481,15 @@ const makeStreamTestLayer = (events: ReadonlyArray) => + OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { + events, + status: 200 + })) + ) + const makeDefaultResponse = ( overrides: Partial = {} ): Generated.Response => ({ diff --git a/packages/ai/openai/test/OpenAiSchema.test.ts b/packages/ai/openai/test/OpenAiSchema.test.ts index 6f78d557f80..fc120afdde8 100644 --- a/packages/ai/openai/test/OpenAiSchema.test.ts +++ b/packages/ai/openai/test/OpenAiSchema.test.ts @@ -14,6 +14,25 @@ const makeResponse = (overrides: Record = {}) => ({ }) describe("OpenAiSchema", () => { + it("validates Responses safety and prompt-cache request options", () => { + const decoded = Schema.decodeUnknownSync(OpenAiSchema.CreateResponse)({ + safety_identifier: "user-hash-123", + prompt_cache_key: "reviewer:v1", + prompt_cache_retention: "24h", + prompt_cache_options: { + mode: "explicit", + ttl: "30m" + } + }) + + assert.strictEqual(decoded.safety_identifier, "user-hash-123") + assert.throws(() => + Schema.decodeUnknownSync(OpenAiSchema.CreateResponse)({ + safety_identifier: "x".repeat(65) + }) + ) + }) + it("decodes a representative response payload", () => { const decoded = Schema.decodeUnknownSync(OpenAiSchema.Response)({ ...makeResponse(),