import type { AssistantMessage, Model, ToolResultMessage } from "@mariozechner/pi-ai"; import { streamOpenAIResponses } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { describe, expect, it } from "vitest"; function buildModel(): Model<"openai-responses"> { return { id: "gpt-5.2", name: "gpt-5.2", api: "openai-responses", provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128_000, maxTokens: 4096, }; } function installFailingFetchCapture() { const originalFetch = globalThis.fetch; let lastBody: unknown; const fetchImpl: typeof fetch = async (_input, init) => { const rawBody = init?.body; const bodyText = (() => { if (!rawBody) return ""; if (typeof rawBody === "string") return rawBody; if (rawBody instanceof Uint8Array) return Buffer.from(rawBody).toString("utf8"); if (rawBody instanceof ArrayBuffer) return Buffer.from(new Uint8Array(rawBody)).toString("utf8"); return String(rawBody); })(); lastBody = bodyText ? (JSON.parse(bodyText) as unknown) : undefined; throw new Error("intentional fetch abort (test)"); }; globalThis.fetch = fetchImpl; return { getLastBody: () => lastBody as Record | undefined, restore: () => { globalThis.fetch = originalFetch; }, }; } describe("openai-responses reasoning replay", () => { it("replays reasoning for tool-call-only turns (OpenAI requires it)", async () => { const cap = installFailingFetchCapture(); try { const model = buildModel(); const assistantToolOnly: AssistantMessage = { role: "assistant", api: "openai-responses", provider: "openai", model: "gpt-5.2", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "toolUse", timestamp: Date.now(), content: [ { type: "thinking", thinking: "internal", thinkingSignature: JSON.stringify({ type: "reasoning", id: "rs_test", summary: [], }), }, { type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {}, }, ], }; const toolResult: ToolResultMessage = { role: "toolResult", toolCallId: "call_123|fc_123", toolName: "noop", content: [{ type: "text", text: "ok" }], isError: false, timestamp: Date.now(), }; const stream = streamOpenAIResponses( model, { systemPrompt: "system", messages: [ { role: "user", content: "Call noop.", timestamp: Date.now(), }, assistantToolOnly, toolResult, { role: "user", content: "Now reply with ok.", timestamp: Date.now(), }, ], tools: [ { name: "noop", description: "no-op", parameters: Type.Object({}, { additionalProperties: false }), }, ], }, { apiKey: "test" }, ); await stream.result(); const body = cap.getLastBody(); const input = Array.isArray(body?.input) ? body?.input : []; const types = input .map((item) => item && typeof item === "object" ? (item as Record).type : undefined, ) .filter((t): t is string => typeof t === "string"); expect(types).toContain("reasoning"); expect(types).toContain("function_call"); expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call")); } finally { cap.restore(); } }); it("still replays reasoning when paired with an assistant message", async () => { const cap = installFailingFetchCapture(); try { const model = buildModel(); const assistantWithText: AssistantMessage = { role: "assistant", api: "openai-responses", provider: "openai", model: "gpt-5.2", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "stop", timestamp: Date.now(), content: [ { type: "thinking", thinking: "internal", thinkingSignature: JSON.stringify({ type: "reasoning", id: "rs_test", summary: [], }), }, { type: "text", text: "hello", textSignature: "msg_test" }, ], }; const stream = streamOpenAIResponses( model, { systemPrompt: "system", messages: [ { role: "user", content: "Hi", timestamp: Date.now() }, assistantWithText, { role: "user", content: "Ok", timestamp: Date.now() }, ], }, { apiKey: "test" }, ); await stream.result(); const body = cap.getLastBody(); const input = Array.isArray(body?.input) ? body?.input : []; const types = input .map((item) => item && typeof item === "object" ? (item as Record).type : undefined, ) .filter((t): t is string => typeof t === "string"); expect(types).toContain("reasoning"); expect(types).toContain("message"); } finally { cap.restore(); } }); });