diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 43a86b4403c..052f869fe54 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -666,7 +666,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques const lowerOptions = (request: LLMRequest) => { const options = OpenResponsesOptions.resolve(request) - const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey) + const cacheKey = ProviderShared.promptCacheKey(request) const parallelToolCalls = resolveParallelToolCalls(request) return { ...(options.instructions ? { instructions: options.instructions } : {}), diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index 4770a301838..923c19e641e 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -659,7 +659,7 @@ const detectZaiToolStream = ( const lowerOptions = (request: LLMRequest, supportsStore: boolean) => { const options = OpenAIOptions.resolve(request) - const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey) + const cacheKey = ProviderShared.promptCacheKey(request) return { ...(supportsStore && options.store !== undefined ? { store: options.store } : {}), // For providers that support `store`, ensure stateless `store:false` is sent diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 49d8acef386..1041624af39 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -28,10 +28,10 @@ export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64 // OpenAI limits `prompt_cache_key` to 64 chars; DeepSeek and Zai inherit the same // limit via their OpenAI-compatible APIs. Clamp with unicode-aware slicing. -export const clampPromptCacheKey = (key: string | undefined): string | undefined => { - if (key === undefined) return undefined - const chars = Array.from(key) - if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key +export const promptCacheKey = (request: LLMRequest): string | undefined => { + if (request.cache === "none" || request.promptCacheKey === undefined) return undefined + const chars = Array.from(request.promptCacheKey) + if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return request.promptCacheKey return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("") } diff --git a/packages/ai/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts index b00cd54e4d3..7f1005fdb36 100644 --- a/packages/ai/src/providers/openrouter.ts +++ b/packages/ai/src/providers/openrouter.ts @@ -9,7 +9,7 @@ import type { ProviderPackage } from "../provider-package.js" import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js" import * as OpenAIChat from "../protocols/openai-chat.js" import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js" -import { isRecord, ProviderShared } from "../protocols/shared.js" +import { isRecord } from "../protocols/shared.js" export const profile = OpenAICompatibleProfiles.profiles.openrouter export const id = ProviderID.make(profile.provider) @@ -115,12 +115,10 @@ export const protocol = Protocol.make({ reasoning_details: reasoningDetails, } }) - const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey) return { ...body, messages, ...bodyOptions(request.providerOptions), - ...(cacheKey ? { prompt_cache_key: cacheKey } : {}), } as OpenRouterBody }), ), diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index b681559126d..b546dc12a47 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -192,6 +192,21 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("omits the prompt cache key when caching is disabled", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model, + prompt: "Hello", + promptCacheKey: "session_123", + cache: "none", + }), + ) + + expect(prepared.body).not.toHaveProperty("prompt_cache_key") + }), + ) + it.effect("maps the xAI Chat prompt cache key to conversation affinity", () => LLMClient.generate( LLM.request({ diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 471eec85595..23e610650ba 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -1699,6 +1699,21 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("omits the prompt cache key when caching is disabled", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model, + prompt: "Hello", + promptCacheKey: "request_cache", + cache: "none", + }), + ) + + expect(prepared.body).not.toHaveProperty("prompt_cache_key") + }), + ) + it.effect("parses text and usage stream fixtures", () => Effect.gen(function* () { const body = sseEvents( diff --git a/packages/ai/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts index c8fdce503ce..c8a181bacb9 100644 --- a/packages/ai/test/provider/openrouter.test.ts +++ b/packages/ai/test/provider/openrouter.test.ts @@ -190,6 +190,21 @@ describe("OpenRouter", () => { }), ) + it.effect("omits the prompt cache key when caching is disabled", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini"), + prompt: "Hello", + promptCacheKey: "session_123", + cache: "none", + }), + ) + + expect(prepared.body).not.toHaveProperty("prompt_cache_key") + }), + ) + it.effect("filters invalid known OpenRouter options while preserving extensions", () => Effect.gen(function* () { const invalid: Record = {