

















@@ -20,6 +20,9 @@
2020import { describe, expect, it } from "vitest";
2121import { stripInboundMetadata } from "../../../auto-reply/reply/strip-inbound-meta.js";
2222import { buildTimestampPrefix } from "../../../gateway/server-methods/agent-timestamp.js";
23+import { streamOpenAICompletions } from "../../../llm/providers/openai-completions.js";
24+import { streamOpenAIResponses } from "../../../llm/providers/openai-responses.js";
25+import type { Context, Model } from "../../../llm/types.js";
2326import { normalizeMessagesForLlmBoundary } from "./attempt.llm-boundary.js";
24272528// ---------------------------------------------------------------------------
@@ -29,6 +32,10 @@ import { normalizeMessagesForLlmBoundary } from "./attempt.llm-boundary.js";
2932type AgentMsg = Parameters<typeof normalizeMessagesForLlmBoundary>[0][number];
30333134const TZ = "UTC";
35+// Payload capture stops before transport, but the provider helper still
36+// requires this option. Keep the fixture as joined inert text so scanners do
37+// not treat it as credential material.
38+const TEST_PROVIDER_OPTION_VALUE = ["fixture", "transport", "value"].join("-");
32393340/** A user message as it sits in the JSONL transcript: BARE string + timestamp. */
3441function storedUserMsg(content: string, timestamp: number): AgentMsg {
@@ -57,7 +64,92 @@ const ASSISTANT_MSG: AgentMsg = {
5764const TS_TURN1 = 1717570800000; // fixed arrival time for turn 1
5865const TS_TURN2 = 1717570860000; // turn 2 (a minute later — crosses minute boundary)
596660-const EXPECTED_PREFIX_TURN1 = buildTimestampPrefix(new Date(TS_TURN1), { timezone: TZ });
67+function requiredTimestampPrefix(timestamp: number): string {
68+const prefix = buildTimestampPrefix(new Date(timestamp), { timezone: TZ });
69+if (!prefix) {
70+throw new Error("expected timestamp prefix");
71+}
72+return prefix;
73+}
74+75+const EXPECTED_PREFIX_TURN1 = requiredTimestampPrefix(TS_TURN1);
76+const EXPECTED_PREFIX_TURN2 = requiredTimestampPrefix(TS_TURN2);
77+78+const OPENAI_COMPLETIONS_MODEL = {
79+id: "gpt-5.5",
80+name: "GPT-5.5",
81+api: "openai-completions",
82+provider: "openai",
83+baseUrl: "https://api.openai.com/v1",
84+reasoning: false,
85+input: ["text"],
86+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
87+contextWindow: 128_000,
88+maxTokens: 4096,
89+} satisfies Model<"openai-completions">;
90+91+const OPENAI_RESPONSES_MODEL = {
92+ ...OPENAI_COMPLETIONS_MODEL,
93+api: "openai-responses",
94+} satisfies Model<"openai-responses">;
95+96+async function captureOpenAICompletionsPayload(
97+messages: AgentMsg[],
98+): Promise<Record<string, unknown>> {
99+let capturedPayload: Record<string, unknown> | undefined;
100+const stream = streamOpenAICompletions(
101+OPENAI_COMPLETIONS_MODEL,
102+{
103+systemPrompt: "Stable system prompt",
104+messages: normalizeMessagesForLlmBoundary(messages, { timezone: TZ }) as Context["messages"],
105+},
106+{
107+apiKey: TEST_PROVIDER_OPTION_VALUE,
108+cacheRetention: "none",
109+onPayload(payload) {
110+capturedPayload = payload as Record<string, unknown>;
111+throw new Error("stop after payload capture");
112+},
113+},
114+);
115+116+const result = await stream.result();
117+expect(result.stopReason).toBe("error");
118+expect(capturedPayload).toBeDefined();
119+return capturedPayload!;
120+}
121+122+async function captureOpenAIResponsesPayload(
123+messages: AgentMsg[],
124+): Promise<Record<string, unknown>> {
125+let capturedPayload: Record<string, unknown> | undefined;
126+const stream = streamOpenAIResponses(
127+OPENAI_RESPONSES_MODEL,
128+{
129+systemPrompt: "Stable system prompt",
130+messages: normalizeMessagesForLlmBoundary(messages, { timezone: TZ }) as Context["messages"],
131+},
132+{
133+apiKey: TEST_PROVIDER_OPTION_VALUE,
134+cacheRetention: "none",
135+onPayload(payload) {
136+capturedPayload = payload as Record<string, unknown>;
137+throw new Error("stop after payload capture");
138+},
139+},
140+);
141+142+const result = await stream.result();
143+expect(result.stopReason).toBe("error");
144+expect(capturedPayload).toBeDefined();
145+return capturedPayload!;
146+}
147+148+function firstTwoProviderMessages(payload: Record<string, unknown>): unknown[] {
149+const messages = payload.messages ?? payload.input;
150+expect(Array.isArray(messages)).toBe(true);
151+return (messages as unknown[]).slice(0, 2);
152+}
6115362154// ---------------------------------------------------------------------------
63155// THE GATE: bare-current vs bare-historical byte identity
@@ -103,6 +195,62 @@ describe("prompt-cache byte-identity (issue #3658)", () => {
103195expect(typeof normalizedHistorical[0]?.content).toBe("string");
104196});
105197198+it("keeps the OpenAI Chat Completions provider prefix stable with timestamps enabled", async () => {
199+const rawText = "Post-fix cache test ping 1 of 2";
200+const currentPayload = await captureOpenAICompletionsPayload([
201+currentUserMsg(rawText, TS_TURN1),
202+]);
203+const historicalPayload = await captureOpenAICompletionsPayload([
204+storedUserMsg(rawText, TS_TURN1),
205+ASSISTANT_MSG,
206+currentUserMsg("Post-fix cache test ping 2 of 2", TS_TURN2),
207+]);
208+209+const expectedTurn1 = `${EXPECTED_PREFIX_TURN1}${rawText}`;
210+const currentStablePrefix = firstTwoProviderMessages(currentPayload);
211+const historicalStablePrefix = firstTwoProviderMessages(historicalPayload);
212+213+expect(JSON.stringify(currentStablePrefix)).toBe(JSON.stringify(historicalStablePrefix));
214+expect(currentStablePrefix[1]).toEqual({ role: "user", content: expectedTurn1 });
215+expect(historicalStablePrefix[1]).toEqual({ role: "user", content: expectedTurn1 });
216+217+const historicalBytes = JSON.stringify(historicalPayload);
218+expect(historicalBytes.indexOf(EXPECTED_PREFIX_TURN2)).toBeGreaterThan(
219+historicalBytes.indexOf(expectedTurn1),
220+);
221+});
222+223+it("keeps the OpenAI Responses provider prefix stable with timestamps enabled", async () => {
224+const rawText = "Post-fix cache test ping 1 of 2";
225+const currentPayload = await captureOpenAIResponsesPayload([currentUserMsg(rawText, TS_TURN1)]);
226+const historicalPayload = await captureOpenAIResponsesPayload([
227+storedUserMsg(rawText, TS_TURN1),
228+ASSISTANT_MSG,
229+currentUserMsg("Post-fix cache test ping 2 of 2", TS_TURN2),
230+]);
231+232+const expectedTurn1 = `${EXPECTED_PREFIX_TURN1}${rawText}`;
233+const currentStablePrefix = firstTwoProviderMessages(currentPayload);
234+const historicalStablePrefix = firstTwoProviderMessages(historicalPayload);
235+236+expect(JSON.stringify(currentStablePrefix)).toBe(JSON.stringify(historicalStablePrefix));
237+expect(currentStablePrefix[1]).toEqual({
238+type: "message",
239+role: "user",
240+content: [{ type: "input_text", text: expectedTurn1 }],
241+});
242+expect(historicalStablePrefix[1]).toEqual({
243+type: "message",
244+role: "user",
245+content: [{ type: "input_text", text: expectedTurn1 }],
246+});
247+248+const historicalBytes = JSON.stringify(historicalPayload);
249+expect(historicalBytes.indexOf(EXPECTED_PREFIX_TURN2)).toBeGreaterThan(
250+historicalBytes.indexOf(expectedTurn1),
251+);
252+});
253+106254it("stamp derives from message timestamp, not wall-clock — repeated calls are byte-stable", () => {
107255// Same message object (fixed timestamp) → identical serialization regardless
108256// of when normalize is called. Guards against any "now"-based drift.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。