fix(ai): recomputed cache breakpoints after history edits
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user