62 lines
3.8 KiB
TypeScript
62 lines
3.8 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { createOpenAiProvider } from "../src/providers/openai";
|
|
import { createProvider } from "../src/providers";
|
|
import { collect, fakeFetch, sseBody } from "./helpers";
|
|
|
|
const ok = (events: [string | null, unknown][]) => () => new Response(sseBody(events));
|
|
const chunk = (delta: unknown, finish: string | null = null) => [null, { choices: [{ index: 0, delta, finish_reason: finish }] }] as [null, unknown];
|
|
|
|
test("streams text deltas from an Ollama-style endpoint without a key", async () => {
|
|
const ff = fakeFetch([ok([chunk({ role: "assistant", content: "Hel" }), chunk({ content: "lo" }), chunk({}, "stop"), [null, "[DONE]"]])]);
|
|
const p = createOpenAiProvider({ baseUrl: "http://ollama:11434/v1/", model: "llama3.1", fetch: ff.fetch });
|
|
const r = await collect(p.chat([{ role: "system", content: "sys" }, { role: "user", content: "hi" }], {}));
|
|
expect(r.text).toBe("Hello");
|
|
const c = ff.calls[0]!;
|
|
expect(c.url).toBe("http://ollama:11434/v1/chat/completions");
|
|
expect(new Headers(c.init.headers).get("authorization")).toBeNull();
|
|
expect(c.body).toMatchObject({ model: "llama3.1", stream: true, messages: [{ role: "system", content: "sys" }, { role: "user", content: "hi" }] });
|
|
});
|
|
|
|
test("assembles fragmented tool calls and maps a tool round back", async () => {
|
|
const ff = fakeFetch([
|
|
ok([
|
|
chunk({ tool_calls: [{ index: 0, id: "call_1", type: "function", function: { name: "read_lesson", arguments: "" } }] }),
|
|
chunk({ tool_calls: [{ index: 0, function: { arguments: "{\"lessonId\":" } }] }),
|
|
chunk({ tool_calls: [{ index: 0, function: { arguments: "\"m1-intro\"}" } }] }),
|
|
chunk({}, "tool_calls"),
|
|
[null, "[DONE]"],
|
|
]),
|
|
ok([chunk({ content: "Done" }), [null, "[DONE]"]]),
|
|
]);
|
|
const p = createOpenAiProvider({ baseUrl: "https://api.example.com/v1", apiKey: "sk-x", model: "gpt-x", fetch: ff.fetch });
|
|
const tools = [{ name: "read_lesson", description: "Read", inputSchema: { type: "object" } }];
|
|
const r = await collect(p.chat([{ role: "user", content: "q" }], { tools }));
|
|
expect(r.toolCalls).toEqual([{ id: "call_1", name: "read_lesson", input: { lessonId: "m1-intro" } }]);
|
|
expect(ff.calls[0]!.body.tools).toEqual([{ type: "function", function: { name: "read_lesson", description: "Read", parameters: { type: "object" } } }]);
|
|
expect(new Headers(ff.calls[0]!.init.headers).get("authorization")).toBe("Bearer sk-x");
|
|
|
|
await collect(p.chat([
|
|
{ role: "user", content: "q" },
|
|
{ role: "assistant", content: "Reading." },
|
|
{ role: "assistant", content: "{\"lessonId\":\"m1-intro\"}", toolCallId: "call_1", name: "read_lesson" },
|
|
{ role: "tool", content: "# Introduction", toolCallId: "call_1", name: "read_lesson" },
|
|
], { tools }));
|
|
expect(ff.calls[1]!.body.messages).toEqual([
|
|
{ role: "user", content: "q" },
|
|
{ role: "assistant", content: "Reading.", tool_calls: [{ id: "call_1", type: "function", function: { name: "read_lesson", arguments: "{\"lessonId\":\"m1-intro\"}" } }] },
|
|
{ role: "tool", tool_call_id: "call_1", content: "# Introduction" },
|
|
]);
|
|
});
|
|
|
|
test("errors carry the endpoint message", async () => {
|
|
const ff = fakeFetch([() => Response.json({ error: { message: "model not found" } }, { status: 404 })]);
|
|
await expect(collect(createOpenAiProvider({ baseUrl: "http://x/v1", model: "m", fetch: ff.fetch }).chat([{ role: "user", content: "x" }], {}))).rejects.toThrow(/model not found/);
|
|
});
|
|
|
|
test("createProvider picks the implementation and defaults", () => {
|
|
expect(createProvider({ kind: "anthropic" }, "k").id).toBe("anthropic");
|
|
expect(createProvider({ kind: "openai", baseUrl: "http://x/v1", model: "m" }, null).id).toBe("openai");
|
|
expect(() => createProvider({ kind: "anthropic" }, null)).toThrow(/key/);
|
|
expect(() => createProvider({ kind: "openai" }, null)).toThrow(/base URL|model/);
|
|
});
|