





















@@ -11,7 +11,10 @@
1111import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
1212import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
1313import type { ResponseObject } from "./openai-ws-connection.js";
14-import { buildOpenAIWebSocketResponseCreatePayload } from "./openai-ws-request.js";
14+import {
15+buildOpenAIWebSocketResponseCreatePayload,
16+planOpenAIWebSocketRequestPayload,
17+} from "./openai-ws-request.js";
1518import {
1619__testing as openAIWsStreamTesting,
1720buildAssistantMessageFromResponse,
@@ -22,6 +25,7 @@ import {
2225planTurnInput,
2326releaseWsSession,
2427} from "./openai-ws-stream.js";
28+import type { InputItem, ResponseCreateEvent } from "./openai-ws-types.js";
2529import { log } from "./pi-embedded-runner/logger.js";
2630import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js";
2731@@ -152,6 +156,10 @@ const { MockManager } = vi.hoisted(() => {
152156153157// Test helper: simulate a server event
154158simulateEvent(event: unknown): void {
159+const maybeEvent = event as { type?: string; response?: { id?: string } };
160+if (maybeEvent.type === "response.completed" && maybeEvent.response?.id) {
161+this._previousResponseId = maybeEvent.response.id;
162+}
155163for (const fn of this._listeners) {
156164fn(event);
157165}
@@ -1678,6 +1686,101 @@ describe("planTurnInput", () => {
1678168616791687// ─────────────────────────────────────────────────────────────────────────────
168016881689+describe("planOpenAIWebSocketRequestPayload", () => {
1690+it("sends only the strict suffix when the full input extends the prior response chain", () => {
1691+const previousInputItems: InputItem[] = [{ type: "message", role: "user", content: "Hello" }];
1692+const previousRequest: ResponseCreateEvent = {
1693+type: "response.create",
1694+model: "gpt-5.4",
1695+store: false,
1696+instructions: "You are helpful.",
1697+input: previousInputItems,
1698+};
1699+const previousResponseInputItems: InputItem[] = [
1700+{ type: "message", role: "assistant", content: "Hi" },
1701+];
1702+const fullPayload: ResponseCreateEvent = {
1703+type: "response.create",
1704+model: "gpt-5.4",
1705+store: false,
1706+instructions: "You are helpful.",
1707+input: [
1708+ ...previousInputItems,
1709+ ...previousResponseInputItems,
1710+{ type: "message", role: "user", content: "Next" },
1711+],
1712+};
1713+1714+const plan = planOpenAIWebSocketRequestPayload({
1715+ fullPayload,
1716+previousRequestPayload: previousRequest,
1717+previousResponseId: "resp_prev",
1718+previousResponseInputItems: [...previousResponseInputItems],
1719+});
1720+1721+expect(plan.mode).toBe("incremental");
1722+expect(plan.payload.previous_response_id).toBe("resp_prev");
1723+expect(plan.payload.input).toEqual([{ type: "message", role: "user", content: "Next" }]);
1724+});
1725+1726+it("falls back to full context when non-input fields differ", () => {
1727+const previousInputItems: InputItem[] = [{ type: "message", role: "user", content: "Hello" }];
1728+const previousRequest: ResponseCreateEvent = {
1729+type: "response.create",
1730+model: "gpt-5.4",
1731+store: false,
1732+instructions: "Old instructions",
1733+input: previousInputItems,
1734+};
1735+const fullPayload: ResponseCreateEvent = {
1736+ ...previousRequest,
1737+instructions: "New instructions",
1738+input: [
1739+ ...previousInputItems,
1740+{ type: "message", role: "assistant", content: "Hi" },
1741+{ type: "message", role: "user", content: "Next" },
1742+],
1743+};
1744+1745+const plan = planOpenAIWebSocketRequestPayload({
1746+ fullPayload,
1747+previousRequestPayload: previousRequest,
1748+previousResponseId: "resp_prev",
1749+previousResponseInputItems: [{ type: "message", role: "assistant", content: "Hi" }],
1750+});
1751+1752+expect(plan.mode).toBe("full_context");
1753+expect(plan.payload.previous_response_id).toBeUndefined();
1754+expect(plan.payload.input).toEqual(fullPayload.input);
1755+});
1756+1757+it("falls back to full context when the input is not a strict response-chain extension", () => {
1758+const previousRequest: ResponseCreateEvent = {
1759+type: "response.create",
1760+model: "gpt-5.4",
1761+store: false,
1762+input: [{ type: "message", role: "user", content: "Hello" }],
1763+};
1764+const fullPayload: ResponseCreateEvent = {
1765+ ...previousRequest,
1766+input: [{ type: "message", role: "user", content: "Different" }],
1767+};
1768+1769+const plan = planOpenAIWebSocketRequestPayload({
1770+ fullPayload,
1771+previousRequestPayload: previousRequest,
1772+previousResponseId: "resp_prev",
1773+previousResponseInputItems: [{ type: "message", role: "assistant", content: "Hi" }],
1774+});
1775+1776+expect(plan.mode).toBe("full_context");
1777+expect(plan.payload.previous_response_id).toBeUndefined();
1778+expect(plan.payload.input).toEqual(fullPayload.input);
1779+});
1780+});
1781+1782+// ─────────────────────────────────────────────────────────────────────────────
1783+16811784describe("createOpenAIWebSocketStreamFn", () => {
16821785const modelStub = {
16831786api: "openai-responses",
@@ -2738,7 +2841,6 @@ describe("createOpenAIWebSocketStreamFn", () => {
2738284127392842// Server responds with a tool call
27402843const turn1Response = makeResponseObject("resp_turn1", undefined, "exec");
2741-manager.setPreviousResponseId("resp_turn1");
27422844manager.simulateEvent({ type: "response.completed", response: turn1Response });
27432845await done1;
27442846@@ -2747,8 +2849,8 @@ describe("createOpenAIWebSocketStreamFn", () => {
27472849systemPrompt: "You are helpful.",
27482850messages: [
27492851userMsg("Run ls"),
2750-assistantMsg([], [{ id: "call_1", name: "exec", args: { cmd: "ls" } }]),
2751-toolResultMsg("call_1", "file.txt"),
2852+buildAssistantMessageFromResponse(turn1Response, modelStub),
2853+toolResultMsg("call_abc|item_2", "file.txt"),
27522854] as Parameters<typeof convertMessagesToInputItems>[0],
27532855tools: [],
27542856};
@@ -2785,7 +2887,68 @@ describe("createOpenAIWebSocketStreamFn", () => {
27852887expect(inputTypes).toHaveLength(1);
27862888});
278728892788-it("omits previous_response_id when replaying full context on follow-up turns", async () => {
2890+it("sends only a follow-up user message when the full context is a strict extension", async () => {
2891+const sessionId = "sess-user-delta";
2892+const streamFn = createOpenAIWebSocketStreamFn("sk-test", sessionId);
2893+2894+const ctx1 = {
2895+systemPrompt: "You are helpful.",
2896+messages: [userMsg("Hello")] as Parameters<typeof convertMessagesToInputItems>[0],
2897+tools: [],
2898+};
2899+2900+const stream1 = streamFn(
2901+modelStub as Parameters<typeof streamFn>[0],
2902+ctx1 as Parameters<typeof streamFn>[1],
2903+);
2904+const done1 = (async () => {
2905+for await (const _ of await resolveStream(stream1)) {
2906+/* consume */
2907+}
2908+})();
2909+2910+await new Promise((r) => setImmediate(r));
2911+const manager = MockManager.lastInstance!;
2912+const turn1Response = makeResponseObject("resp_turn1_text", "Hi there.");
2913+manager.simulateEvent({ type: "response.completed", response: turn1Response });
2914+await done1;
2915+2916+const ctx2 = {
2917+systemPrompt: "You are helpful.",
2918+messages: [
2919+userMsg("Hello"),
2920+buildAssistantMessageFromResponse(turn1Response, modelStub),
2921+userMsg("What can you do?"),
2922+] as Parameters<typeof convertMessagesToInputItems>[0],
2923+tools: [],
2924+};
2925+2926+const stream2 = streamFn(
2927+modelStub as Parameters<typeof streamFn>[0],
2928+ctx2 as Parameters<typeof streamFn>[1],
2929+);
2930+const done2 = (async () => {
2931+for await (const _ of await resolveStream(stream2)) {
2932+/* consume */
2933+}
2934+})();
2935+2936+await new Promise((r) => setImmediate(r));
2937+manager.simulateEvent({
2938+type: "response.completed",
2939+response: makeResponseObject("resp_turn2_text", "I can help."),
2940+});
2941+await done2;
2942+2943+const sent2 = manager.sentEvents[1] as {
2944+previous_response_id?: string;
2945+input: Array<{ type: string; role?: string; content?: unknown }>;
2946+};
2947+expect(sent2.previous_response_id).toBe("resp_turn1_text");
2948+expect(sent2.input).toEqual([{ type: "message", role: "user", content: "What can you do?" }]);
2949+});
2950+2951+it("uses an empty incremental payload when replay context exactly matches the response chain", async () => {
27892952const sessionId = "sess-full-context-replay";
27902953const streamFn = createOpenAIWebSocketStreamFn("sk-test", sessionId);
27912954@@ -2830,7 +2993,6 @@ describe("createOpenAIWebSocketStreamFn", () => {
2830299328312994await new Promise((r) => setImmediate(r));
28322995const manager = MockManager.lastInstance!;
2833-manager.setPreviousResponseId("resp_turn1_reasoning");
28342996manager.simulateEvent({ type: "response.completed", response: turn1Response });
28352997await done1;
28362998@@ -2864,14 +3026,8 @@ describe("createOpenAIWebSocketStreamFn", () => {
28643026previous_response_id?: string;
28653027input: Array<{ type: string; id?: string; call_id?: string }>;
28663028};
2867-expect(sent2.previous_response_id).toBeUndefined();
2868-expect(sent2.input.map((item) => item.type)).toEqual(["message", "reasoning", "function_call"]);
2869-expect(sent2.input[1]).toMatchObject({ type: "reasoning", id: "rs_turn1" });
2870-expect(sent2.input[2]).toMatchObject({
2871-type: "function_call",
2872-call_id: "call_turn1",
2873-id: "fc_turn1",
2874-});
3029+expect(sent2.previous_response_id).toBe("resp_turn1_reasoning");
3030+expect(sent2.input).toEqual([]);
28753031});
2876303228773033it("sends instructions (system prompt) in each request", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。