Skip to content

Commit 15d83ba

Browse files
authored
fix(adapters): forward prompt_cache_key through openai-chat adapter (#217) (#224)
* fix(adapters): forward prompt_cache_key through openai-chat adapter (#217) The Responses parser writes prompt_cache_key to options.promptCacheKey, but the openai-chat adapter's buildRequest() never copied it into the outbound /chat/completions body. OpenAI's Chat Completions API accepts prompt_cache_key as an optional string for prompt-cache affinity grouping. Copy parsed.options.promptCacheKey to body.prompt_cache_key when set, omit when unset. The openai-responses passthrough adapter already preserves it via _rawBody forwarding. Add two tests: one asserting the field is forwarded when present, one asserting it is absent when not provided. * fix(adapters): gate prompt_cache_key behind provider opt-in flag The previous unconditional copy forwarded prompt_cache_key to every openai-chat backend. Strict providers (Groq, Cerebras, etc.) reject unknown body fields, so this could break those routed requests. Add an opt-in promptCacheKey boolean flag to OcxProviderConfig and ProviderRegistryEntry, wired through router scalar backfill. The adapter now only forwards body.prompt_cache_key when the provider explicitly opts in. Default off, so no built-in provider is affected. The built-in OpenAI provider uses openai-responses (passthrough), which already preserves prompt_cache_key via _rawBody forwarding. This flag is for user-configured openai-chat providers that document prompt_cache_key support. Tests updated to cover opt-in, opt-out, and unset cases.
1 parent 90d1ef4 commit 15d83ba

5 files changed

Lines changed: 42 additions & 0 deletions

File tree

src/adapters/openai-chat.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
502502
if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
503503
body.frequency_penalty = parsed.options.frequencyPenalty;
504504
}
505+
// prompt_cache_key is an OpenAI-specific chat extension; strict backends (Groq,
506+
// Cerebras, etc.) reject unknown fields. Only forward when the provider opts in.
507+
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
508+
body.prompt_cache_key = parsed.options.promptCacheKey;
509+
}
505510

506511
if (tools) {
507512
// Default-ON for chat-completions providers (user decision 260709): the buffered

src/providers/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface ProviderRegistryEntry {
6565
noPenaltyModels?: string[];
6666
/** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */
6767
parallelToolCalls?: boolean;
68+
/** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */
69+
promptCacheKey?: boolean;
6870
autoToolChoiceOnlyModels?: string[];
6971
preserveReasoningContentModels?: string[];
7072
thinkingToggleModels?: string[];

src/router.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
182182
// Scalar backfill: a persisted config created before the flag shipped inherits the registry
183183
// opt-in, while an explicit user `false` keeps overriding registry `true`.
184184
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
185+
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
185186
...(modelContextWindows ? { modelContextWindows } : {}),
186187
...(modelInputModalities ? { modelInputModalities } : {}),
187188
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,12 @@ export interface OcxProviderConfig {
692692
* only on explicit `true`. See devlog/_plan/260709_parallel_tool_calls.
693693
*/
694694
parallelToolCalls?: boolean;
695+
/**
696+
* Opt-in: forward `prompt_cache_key` to the upstream `/chat/completions` body.
697+
* OpenAI-specific extension; strict backends (Groq, Cerebras, etc.) reject unknown
698+
* fields. Default off; only enable for providers that document this parameter.
699+
*/
700+
promptCacheKey?: boolean;
695701
/** Model ids whose tool_choice only accepts `auto` or `none`; forced/named choices are downgraded. */
696702
autoToolChoiceOnlyModels?: string[];
697703
/** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */

tests/openai-chat-hardening.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,32 @@ describe("openai-chat credential hardening", () => {
129129
Authorization: "Bearer sk-litellm",
130130
});
131131
});
132+
133+
test("forwards prompt_cache_key to the outbound chat body when the provider opts in", () => {
134+
const adapter = createOpenAIChatAdapter(provider({ promptCacheKey: true }));
135+
const req = parsed();
136+
req.options.promptCacheKey = "shared-prefix-v1";
137+
138+
const body = JSON.parse(adapter.buildRequest(req).body);
139+
140+
expect(body.prompt_cache_key).toBe("shared-prefix-v1");
141+
});
142+
143+
test("does not forward prompt_cache_key when the provider has not opted in", () => {
144+
const adapter = createOpenAIChatAdapter(provider());
145+
const req = parsed();
146+
req.options.promptCacheKey = "shared-prefix-v1";
147+
148+
const body = JSON.parse(adapter.buildRequest(req).body);
149+
150+
expect(body).not.toHaveProperty("prompt_cache_key");
151+
});
152+
153+
test("omits prompt_cache_key from the outbound chat body when unset", () => {
154+
const adapter = createOpenAIChatAdapter(provider({ promptCacheKey: true }));
155+
156+
const body = JSON.parse(adapter.buildRequest(parsed()).body);
157+
158+
expect(body).not.toHaveProperty("prompt_cache_key");
159+
});
132160
});

0 commit comments

Comments
 (0)