Files
pi_harness/packages/ai/test/openai-responses-partial-json-cleanup.test.ts
T
Mario Zechner ba93da9a93 feat(ai): move API implementations to src/api with lazy wrappers (phase 2)
Stream implementations move from src/providers/ to src/api/, renamed by
API id (anthropic.ts -> anthropic-messages.ts, google.ts ->
google-generative-ai.ts, mistral.ts -> mistral-conversations.ts,
amazon-bedrock.ts -> bedrock-converse-stream.ts). Every module now
exports exactly stream/streamSimple; shared helpers move alongside.

New ProviderStreams dispatch contract in types.ts, lazyApi() wrapper in
api/lazy.ts, and one .lazy.ts wrapper per API. Bedrock's wrapper keeps
the node-only variable-specifier import and setBedrockProviderModule()
(now taking ProviderStreams).

providers/register-builtins.ts deleted; interim until the compat
entrypoint lands, builtin api-registry registration lives in stream.ts
and lazy wrappers are exported from the root barrel. Old per-API lazy
exports (streamAnthropic, ...) are gone; package.json subpaths retarget
to dist/api/.
2026-06-10 20:08:59 +02:00

102 lines
3.2 KiB
TypeScript

import type { ResponseStreamEvent } from "openai/resources/responses/responses.js";
import { describe, expect, it, vi } from "vitest";
import { processResponsesStream } from "../src/api/openai-responses-shared.ts";
import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.ts";
import { AssistantMessageEventStream } from "../src/utils/event-stream.ts";
function createOutput(model: Model<"openai-responses">): AssistantMessage {
return {
role: "assistant",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
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(),
};
}
async function* createFunctionCallEvents(argumentsJson: string): AsyncIterable<ResponseStreamEvent> {
yield {
type: "response.output_item.added",
item: {
type: "function_call",
id: "fc_test",
call_id: "call_test",
name: "edit",
arguments: "",
},
} as ResponseStreamEvent;
yield {
type: "response.function_call_arguments.delta",
delta: '{"path":"README.md"',
} as ResponseStreamEvent;
yield {
type: "response.function_call_arguments.delta",
delta: ',"content":"updated"}',
} as ResponseStreamEvent;
yield {
type: "response.function_call_arguments.done",
arguments: argumentsJson,
} as ResponseStreamEvent;
yield {
type: "response.output_item.done",
item: {
type: "function_call",
id: "fc_test",
call_id: "call_test",
name: "edit",
arguments: argumentsJson,
},
} as ResponseStreamEvent;
}
describe("openai responses partialJson cleanup", () => {
it("removes partialJson from persisted tool-call blocks at output_item.done", async () => {
const model: Model<"openai-responses"> = {
id: "gpt-5-mini",
name: "GPT-5 Mini",
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 output = createOutput(model);
const stream = new AssistantMessageEventStream();
const pushSpy = vi.spyOn(stream, "push");
const argumentsJson = '{"path":"README.md","content":"updated"}';
await processResponsesStream(createFunctionCallEvents(argumentsJson), output, stream, model);
expect(output.content).toHaveLength(1);
const persistedToolCall = output.content[0];
expect(persistedToolCall?.type).toBe("toolCall");
if (!persistedToolCall || persistedToolCall.type !== "toolCall") {
throw new Error("Expected toolCall block");
}
expect(persistedToolCall.arguments).toEqual({ path: "README.md", content: "updated" });
expect("partialJson" in persistedToolCall).toBe(false);
const emittedEvents = pushSpy.mock.calls.map(([event]) => event as AssistantMessageEvent);
const toolCallEnd = emittedEvents.find((event) => event.type === "toolcall_end");
expect(toolCallEnd).toBeDefined();
if (!toolCallEnd || toolCallEnd.type !== "toolcall_end") {
throw new Error("Expected toolcall_end event");
}
expect(toolCallEnd.toolCall).toBe(persistedToolCall);
expect("partialJson" in toolCallEnd.toolCall).toBe(false);
});
});