Skip to content

Commit 687ede5

Browse files
authored
fix(agents): add prompt cache compatibility opt-out
Add compat.supportsPromptCacheKey for OpenAI Responses prompt_cache_key handling, update generated config baseline, changelog, and A2UI dependency-layout test compatibility.
1 parent f624b1d commit 687ede5

9 files changed

Lines changed: 86 additions & 13 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai
5050
- Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT.
5151
- BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept `updated-message` webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine.
5252
- Agents/skills: sort prompt-facing `available_skills` entries by skill name after merging sources so `skills.load.extraDirs` order no longer changes prompt-cache prefixes. (#64198) Thanks @Bartok9.
53+
- Agents/OpenAI Responses: add `models.providers.*.models.*.compat.supportsPromptCacheKey` so OpenAI-compatible proxies that forward `prompt_cache_key` can keep prompt caching enabled while incompatible endpoints can still force stripping. (#67427) Thanks @damselem.
5354

5455
## 2026.4.15-beta.1
5556

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
4fec95c9ce02dddb4d3021812cf68df8b4cc92c5ba4db35778bb1bfe6fa63021 config-baseline.json
2-
aafbb407e62908709e90f750ea0f8274016fcfcbd613394896ff984f967f236e config-baseline.core.json
1+
8bbc7501da1e567f5e12b2bedb1c59e8592bcc5f003ddc1b7d584cc1b1ff8913 config-baseline.json
2+
eeed6fe659078632d9f95b3350b27103b4aba282d050ff38d3b0953a456d242d config-baseline.core.json
33
ef83a06633fc001b5b2535566939186ecb49d05cd1a90b40e54cc58d3e6e44e3 config-baseline.channel.json
44
5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json

src/agents/provider-attribution.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,55 @@ describe("provider attribution", () => {
620620
});
621621
});
622622

623+
it("respects compat.supportsPromptCacheKey override on prompt cache stripping", () => {
624+
// compat.supportsPromptCacheKey = true disables the strip even on a
625+
// proxy-like endpoint that would otherwise trigger it.
626+
expect(
627+
resolveProviderRequestCapabilities({
628+
provider: "custom-proxy",
629+
api: "openai-responses",
630+
baseUrl: "https://proxy.example.com/v1",
631+
capability: "llm",
632+
transport: "stream",
633+
compat: { supportsPromptCacheKey: true },
634+
}),
635+
).toMatchObject({
636+
endpointClass: "custom",
637+
shouldStripResponsesPromptCache: false,
638+
});
639+
640+
// compat.supportsPromptCacheKey = false forces the strip even on a
641+
// native OpenAI endpoint that would otherwise forward the field.
642+
expect(
643+
resolveProviderRequestCapabilities({
644+
provider: "openai",
645+
api: "openai-responses",
646+
baseUrl: "https://api.openai.com/v1",
647+
capability: "llm",
648+
transport: "stream",
649+
compat: { supportsPromptCacheKey: false },
650+
}),
651+
).toMatchObject({
652+
endpointClass: "openai-public",
653+
shouldStripResponsesPromptCache: true,
654+
});
655+
656+
// compat.supportsPromptCacheKey unset preserves the existing default
657+
// (strip on proxy-like responses endpoints, preserving the fix from
658+
// #48155 for providers that reject the field).
659+
expect(
660+
resolveProviderRequestCapabilities({
661+
provider: "custom-proxy",
662+
api: "openai-responses",
663+
baseUrl: "https://proxy.example.com/v1",
664+
capability: "llm",
665+
transport: "stream",
666+
}),
667+
).toMatchObject({
668+
shouldStripResponsesPromptCache: true,
669+
});
670+
});
671+
623672
it("resolves shared compat families and native streaming-usage gates", () => {
624673
expect(
625674
resolveProviderRequestCapabilities({

src/agents/provider-attribution.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export type ProviderRequestCapabilitiesInput = ProviderRequestPolicyInput & {
9292
modelId?: string | null;
9393
compat?: {
9494
supportsStore?: boolean;
95+
supportsPromptCacheKey?: boolean;
9596
} | null;
9697
};
9798

@@ -607,8 +608,20 @@ export function resolveProviderRequestCapabilities(
607608
OPENAI_RESPONSES_APIS.has(api) &&
608609
OPENAI_RESPONSES_PROVIDERS.has(provider) &&
609610
policy.usesKnownNativeOpenAIEndpoint,
611+
// Default strip behavior (proxy-like endpoints with responses APIs) is
612+
// preserved as a safety net for providers that reject prompt_cache_key —
613+
// see #48155 (Volcano Engine DeepSeek). Operators running their payload
614+
// through an OpenAI-compatible proxy known to forward the field
615+
// (CLIProxy, LiteLLM, etc.) can opt out via compat.supportsPromptCacheKey
616+
// to recover prompt caching; providers known to reject the field can
617+
// force the strip with compat.supportsPromptCacheKey = false even on
618+
// native endpoints.
610619
shouldStripResponsesPromptCache:
611-
api !== undefined && OPENAI_RESPONSES_APIS.has(api) && policy.usesExplicitProxyLikeEndpoint,
620+
input.compat?.supportsPromptCacheKey === true
621+
? false
622+
: input.compat?.supportsPromptCacheKey === false
623+
? api !== undefined && OPENAI_RESPONSES_APIS.has(api)
624+
: api !== undefined && OPENAI_RESPONSES_APIS.has(api) && policy.usesExplicitProxyLikeEndpoint,
612625
// Native endpoint class is the real signal here. Users can point a generic
613626
// provider key at Moonshot or DashScope and still need streaming usage.
614627
supportsNativeStreamingUsageCompat:

src/agents/provider-request-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ type ResolveProviderRequestPolicyConfigParams = {
162162
authHeader?: boolean;
163163
compat?: {
164164
supportsStore?: boolean;
165+
supportsPromptCacheKey?: boolean;
165166
} | null;
166167
modelId?: string | null;
167168
allowPrivateNetwork?: boolean;

src/config/schema.base.generated.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2798,6 +2798,9 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
27982798
supportsStore: {
27992799
type: "boolean",
28002800
},
2801+
supportsPromptCacheKey: {
2802+
type: "boolean",
2803+
},
28012804
supportsDeveloperRole: {
28022805
type: "boolean",
28032806
},

src/config/types.models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type SupportedThinkingFormat =
3737
export type ModelCompatConfig = SupportedOpenAICompatFields & {
3838
thinkingFormat?: SupportedThinkingFormat;
3939
supportsTools?: boolean;
40+
supportsPromptCacheKey?: boolean;
4041
requiresStringContent?: boolean;
4142
toolSchemaProfile?: string;
4243
unsupportedToolSchemaKeywords?: string[];

src/config/zod-schema.core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export const ModelApiSchema = z.enum(MODEL_APIS);
186186
export const ModelCompatSchema = z
187187
.object({
188188
supportsStore: z.boolean().optional(),
189+
supportsPromptCacheKey: z.boolean().optional(),
189190
supportsDeveloperRole: z.boolean().optional(),
190191
supportsReasoningEffort: z.boolean().optional(),
191192
supportsUsageInStreaming: z.boolean().optional(),

test/scripts/bundle-a2ui.test.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,21 @@ describe("scripts/bundle-a2ui.mjs", () => {
6464
it("tracks only the resolved bundle dependency manifests from node_modules", () => {
6565
const repoRoot = process.cwd();
6666
const dependencyPaths = getResolvedBundleDependencyPackageJsonPaths(repoRoot);
67-
68-
expect(dependencyPaths).toContain(path.join(repoRoot, "node_modules", "lit", "package.json"));
69-
expect(dependencyPaths).toContain(
70-
path.join(repoRoot, "node_modules", "@lit/context", "package.json"),
71-
);
72-
expect(dependencyPaths).toContain(
73-
path.join(repoRoot, "node_modules", "@lit-labs/signals", "package.json"),
74-
);
75-
expect(dependencyPaths).toContain(
76-
path.join(repoRoot, "node_modules", "signal-utils", "package.json"),
67+
const relativeDependencyPaths = dependencyPaths.map((dependencyPath) =>
68+
path.relative(repoRoot, dependencyPath),
7769
);
70+
71+
expect(
72+
relativeDependencyPaths.map((relativePath) => relativePath.replace(/^ui\//u, "")),
73+
).toEqual([
74+
path.join("node_modules", "lit", "package.json"),
75+
path.join("node_modules", "@lit/context", "package.json"),
76+
path.join("node_modules", "@lit-labs/signals", "package.json"),
77+
path.join("node_modules", "signal-utils", "package.json"),
78+
]);
79+
expect(
80+
relativeDependencyPaths.every((relativePath) => /^(ui\/)?node_modules\//u.test(relativePath)),
81+
).toBe(true);
7882
expect(getBundleHashInputPaths(repoRoot)).not.toContain(path.join(repoRoot, "package.json"));
7983
expect(getBundleHashInputPaths(repoRoot)).not.toContain(path.join(repoRoot, "pnpm-lock.yaml"));
8084
});

0 commit comments

Comments
 (0)