fix(ai): recomputed cache breakpoints after history edits

This commit is contained in:
can1357
2026-07-24 03:10:02 +02:00
parent 031d687325
commit debccaaacb
3 changed files with 81 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed stateful OpenAI Responses explicit cache breakpoints being restored onto edited historical messages, ensuring full replays recompute the latest stable cache boundary.
## [17.1.0] - 2026-07-24
### Added
+24 -4
View File
@@ -930,6 +930,25 @@ function isStableStringResponsesInstruction(item: unknown): item is ResponsesStr
);
}
function matchesResponsesCacheBaseline(
baseline: ResponsesPromptCacheableMessage,
current: ResponsesPromptCacheableMessage,
): boolean {
if (baseline.role !== current.role || baseline.content.length !== current.content.length) return false;
for (let index = 0; index < baseline.content.length; index++) {
const baselineBlock = baseline.content[index];
const currentBlock = current.content[index];
if (!baselineBlock || !currentBlock) return false;
const breakpoint = baselineBlock.prompt_cache_breakpoint;
if (breakpoint) {
if (!Bun.deepEquals(baselineBlock, { ...currentBlock, prompt_cache_breakpoint: breakpoint })) return false;
} else if (!Bun.deepEquals(baselineBlock, currentBlock)) {
return false;
}
}
return true;
}
function restoreResponsesCacheBreakpointsFromBaseline(
input: ResponseInput | undefined,
baseline: ResponseInput | undefined,
@@ -944,7 +963,7 @@ function restoreResponsesCacheBreakpointsFromBaseline(
if (isStableStringResponsesInstruction(message)) {
const [baselineBlock] = baselineMessage.content;
if (
baselineMessage.content.length === 1 &&
baselineMessage.role === message.role &&
baselineBlock?.type === "input_text" &&
baselineBlock.text === message.content &&
baselineBlock.prompt_cache_breakpoint
@@ -963,11 +982,12 @@ function restoreResponsesCacheBreakpointsFromBaseline(
continue;
}
if (!isResponsesPromptCacheableMessage(message)) continue;
for (let j = 0; j < baselineMessage.content.length && j < message.content.length; j++) {
if (!isResponsesPromptCacheableMessage(message) || !matchesResponsesCacheBaseline(baselineMessage, message))
continue;
for (let j = 0; j < baselineMessage.content.length; j++) {
const baselineBlock = baselineMessage.content[j];
const block = message.content[j];
if (!baselineBlock?.prompt_cache_breakpoint || !isResponsesPromptCacheableContentBlock(block)) continue;
if (!baselineBlock?.prompt_cache_breakpoint || !block) continue;
Object.assign(block, { prompt_cache_breakpoint: baselineBlock.prompt_cache_breakpoint });
restored = true;
}
@@ -329,6 +329,59 @@ describe("openai-responses stateful chaining", () => {
]);
});
it("recomputes the cache breakpoint when the marked message content changes", async () => {
const sentRequests: Array<Record<string, unknown>> = [];
const fetchMock = createCapturingFetch(sentRequests);
const providerSessionState = new Map<string, ProviderSessionState>();
const options = {
apiKey: "test-key",
sessionId: "stateful-edited-marked-message-session",
providerSessionState,
statefulResponses: true,
promptCache: { mode: "explicit" as const },
fetch: fetchMock,
};
const oldestUser = { role: "user" as const, content: "Oldest stable question", timestamp: 1000 };
const firstUser = { role: "user" as const, content: "First question", timestamp: 1001 };
const firstResponse = await streamOpenAIResponses(
explicitPromptCacheModel,
{ messages: [oldestUser, firstUser] },
options,
).result();
await streamOpenAIResponses(
explicitPromptCacheModel,
{
messages: [
{ ...oldestUser, content: "Edited oldest question" },
firstUser,
firstResponse,
{ role: "user", content: "Second question", timestamp: 1002 },
],
},
options,
).result();
expect(sentRequests).toHaveLength(2);
expect(sentRequests[1]?.previous_response_id).toBeUndefined();
const replayInput = sentRequests[1]?.input;
if (!Array.isArray(replayInput)) throw new Error("Expected a full Responses replay");
expect(replayInput[0]).toEqual({
role: "user",
content: [{ type: "input_text", text: "Edited oldest question" }],
});
expect(replayInput[1]).toEqual({
role: "user",
content: [
{
type: "input_text",
text: "First question",
prompt_cache_breakpoint: { mode: "explicit" },
},
],
});
});
it("chains turns without appending an extra no-reasoning developer item", async () => {
const sentRequests: Array<Record<string, unknown>> = [];
const fetchMock = createCapturingFetch(sentRequests);