File size: 3,936 Bytes
80d7f0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import type { AssistantMessage, AssistantMessageEvent, Model } from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import { type ProxyAssistantMessageEvent, streamProxy } from "../src/proxy.ts";
const model: Model<"openai-responses"> = {
id: "gpt-5.4",
name: "GPT-5.4",
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: 400000,
maxTokens: 128000,
};
const usage: AssistantMessage["usage"] = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
afterEach(() => {
vi.unstubAllGlobals();
});
describe("streamProxy", () => {
it("preserves tool-call metadata received only on toolcall_end", async () => {
const proxyEvents: ProxyAssistantMessageEvent[] = [
{ type: "start" },
{ type: "toolcall_start", contentIndex: 0, id: "call_test|fc_test", toolName: "lookup" },
{ type: "toolcall_delta", contentIndex: 0, delta: '{"value":"hello"}' },
{
type: "toolcall_end",
contentIndex: 0,
toolCall: {
type: "toolCall",
id: "call_test|fc_test",
name: "lookup",
arguments: { value: "hello" },
namespace: "dynamic_tools",
},
},
{ type: "done", reason: "toolUse", usage },
];
const body = proxyEvents.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("");
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(body, { status: 200 })),
);
const stream = streamProxy(
model,
{ systemPrompt: "", messages: [] },
{
authToken: "test-token",
proxyUrl: "https://proxy.example.com",
},
);
const events: AssistantMessageEvent[] = [];
for await (const event of stream) events.push(event);
const result = await stream.result();
const endEvent = events.find((event) => event.type === "toolcall_end");
expect(endEvent).toMatchObject({
type: "toolcall_end",
toolCall: { namespace: "dynamic_tools" },
});
expect(result.content[0]).toMatchObject({
type: "toolCall",
arguments: { value: "hello" },
namespace: "dynamic_tools",
});
});
// Regression tests for https://github.com/earendil-works/pi/issues/8996
it("processes terminal metadata when the event is not newline-terminated", async () => {
const start = `data: ${JSON.stringify({ type: "start" })}\n\n`;
const done = `data: ${JSON.stringify({ type: "done", reason: "stop", usage, providerThinkingLevel: "high" })}`;
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(start + done, { status: 200 })),
);
const stream = streamProxy(
model,
{ systemPrompt: "", messages: [] },
{
authToken: "test-token",
proxyUrl: "https://proxy.example.com",
},
);
const events: AssistantMessageEvent[] = [];
for await (const event of stream) events.push(event);
const result = await stream.result();
expect(events.map((event) => event.type)).toEqual(["start", "done"]);
expect(result.stopReason).toBe("stop");
expect(result.providerThinkingLevel).toBe("high");
});
it("emits an error instead of hanging when the stream ends without a terminal event", async () => {
const body = `data: ${JSON.stringify({ type: "start" })}\n\n`;
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(body, { status: 200 })),
);
const stream = streamProxy(
model,
{ systemPrompt: "", messages: [] },
{
authToken: "test-token",
proxyUrl: "https://proxy.example.com",
},
);
const events: AssistantMessageEvent[] = [];
for await (const event of stream) events.push(event);
const result = await stream.result();
expect(events.map((event) => event.type)).toEqual(["start", "error"]);
expect(result.stopReason).toBe("error");
expect(result.errorMessage).toContain("Connection closed by proxy server");
});
});
|