evOo-local/packages/adapters/src/openai-compatible-thinking.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

151 lines
5.1 KiB
TypeScript

import { type Model, Type } from "@earendil-works/pi-ai";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildModelConnectPlaintext } from "./model-connect.js";
import { registerOpenAiCompatibleRuntime } from "./pi-openai-compatible-provider.js";
const modelId = "arbitrary-model";
const baseUrl = "http://127.0.0.1:8090/v1";
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it("keeps the existing keyless connection format", () => {
const plaintext = buildModelConnectPlaintext({ provider: "openai-compatible", baseUrl, modelId });
expect(plaintext).toContain(baseUrl);
expect(plaintext).not.toContain("apiKey");
});
type Payload = {
model: string;
stream: boolean;
messages: Array<{ role: string }>;
tools: Array<{ function: { name: string } }>;
max_tokens?: number;
max_completion_tokens?: number;
chat_template_kwargs?: Record<string, unknown>;
reasoning_effort?: string;
};
function fixtureResponse(): Response {
const deltas = [
{ reasoning_content: "Check the fixture." },
{ content: "Ready." },
{
tool_calls: [
{
index: 0,
id: "call_test",
type: "function",
function: { name: "lookup", arguments: '{"value":' },
},
],
},
{ tool_calls: [{ index: 0, function: { arguments: "42}" } }] },
];
const chunks = [...deltas, {}].map((delta, index) => ({
id: "fixture",
object: "chat.completion.chunk",
created: 1,
model: modelId,
choices: [{ index: 0, delta, finish_reason: index === deltas.length ? "tool_calls" : null }],
}));
return new Response(
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
{
headers: { "content-type": "text/event-stream" },
},
);
}
describe("OpenAI-compatible standard thinking transport", () => {
const provider = "openai-compatible";
async function capture(
reasoning?: "minimal" | "low" | "medium" | "high",
supportsThinking = true,
maxTokens?: number,
) {
let payload: Payload | undefined;
const mockFetch = vi.fn<typeof fetch>(async (input, init) => {
expect(String(input)).toBe(`${baseUrl}/chat/completions`);
payload = JSON.parse(String(init?.body)) as Payload;
return fixtureResponse();
});
vi.stubGlobal("fetch", mockFetch);
const models = registerOpenAiCompatibleRuntime(builtinModels(), {
modelId,
baseUrl,
reasoning: supportsThinking,
maxTokens,
});
const model = models.getModel(provider, modelId) as Model<"openai-completions">;
const stream = models.streamSimple(
model,
{
systemPrompt: "Use the fixture.",
messages: [{ role: "user", content: "Look it up.", timestamp: 1 }],
tools: [
{
name: "lookup",
description: "Look up a fixture",
parameters: Type.Object({ value: Type.Number() }),
},
],
},
{ reasoning, fetch: mockFetch },
);
const events = [];
for await (const event of stream) events.push(event.type);
const result = await stream.result();
expect(mockFetch).toHaveBeenCalledOnce();
expect(result.stopReason).toBe("toolUse");
return { payload: payload!, result, events };
}
it.each(["minimal", "low", "medium", "high"] as const)(
"sends standard effort %s unchanged",
async (level) => {
const { payload } = await capture(level);
expect(payload.reasoning_effort).toBe(level);
expect(payload.chat_template_kwargs).toBeUndefined();
},
);
it("sends the configured maximum output-token limit", async () => {
const { payload } = await capture("medium", true, 8192);
expect(payload.max_tokens ?? payload.max_completion_tokens).toBe(8192);
});
it("sends none when thinking is off", async () => {
const { payload } = await capture();
expect(payload.reasoning_effort).toBe("none");
expect(payload.chat_template_kwargs).toBeUndefined();
});
it("does not send reasoning fields on a connection without support, even for the same model ID", async () => {
await capture("high");
const { payload } = await capture("high", false);
expect(payload.reasoning_effort).toBeUndefined();
expect(payload.chat_template_kwargs).toBeUndefined();
});
it("streams system-role requests, reasoning, text and fragmented tool arguments", async () => {
const { payload, result, events } = await capture("medium");
expect(payload.messages[0]?.role).toBe("system");
expect(payload.stream).toBe(true);
expect(payload.tools[0]?.function.name).toBe("lookup");
expect(events).toEqual(
expect.arrayContaining(["thinking_delta", "text_delta", "toolcall_delta", "done"]),
);
expect(result.content).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "thinking", thinking: "Check the fixture." }),
expect.objectContaining({ type: "text", text: "Ready." }),
expect.objectContaining({ type: "toolCall", name: "lookup", arguments: { value: 42 } }),
]),
);
});
});