summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/listModels.test.ts
blob: 63c95fcb365aeeb2a1fc12ef5213e4e6ef7307e6 (plain)
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
import type { ApiKeyCredentials, ModelInfo, ProviderContract } from "@dispatch/kernel";
import type { FetchLike } from "@dispatch/trace-replay";
import { describe, expect, it, vi } from "vitest";
import { parseModelList } from "./listModels.js";
import { createOpenAICompatProvider } from "./provider.js";

function makeProvider(fetchFn: FetchLike, apiKey = "sk-test-1234567890abcdef"): ProviderContract {
	const creds: ApiKeyCredentials = {
		type: "api-key",
		apiKey,
		baseURL: "https://api.example.com/v1",
	};
	return createOpenAICompatProvider({
		credentials: creds,
		model: "test-model",
		id: "openai-compat",
		fetchFn,
	});
}

function jsonResponse(body: unknown, status = 200): Response {
	return new Response(JSON.stringify(body), {
		status,
		headers: { "Content-Type": "application/json" },
	});
}

describe("listModels — pure mapping (parseModelList)", () => {
	it("maps OpenAI model entries to ModelInfo", () => {
		const result = parseModelList([{ id: "a" }, { id: "b" }]);
		expect(result).toEqual([{ id: "a" }, { id: "b" }]);
	});

	it("returns empty array for empty input", () => {
		const result = parseModelList([]);
		expect(result).toEqual([]);
	});
});

describe("listModels — provider contract", () => {
	it("GETs models endpoint with bearer key and returns mapped ModelInfo[]", async () => {
		const fetchFn = vi.fn(
			() => jsonResponse({ data: [{ id: "a" }, { id: "b" }] }) as unknown as ReturnType<FetchLike>,
		);
		const provider = makeProvider(fetchFn);
		const listModels = provider.listModels;
		if (!listModels) throw new Error("listModels not defined");

		const models = await listModels();

		expect(fetchFn).toHaveBeenCalledOnce();
		const callArgs = fetchFn.mock.calls[0];
		if (!callArgs) throw new Error("no call args");
		const [url, init] = callArgs as unknown as [string, RequestInit];
		expect(url).toBe("https://api.example.com/v1/models");
		expect(init.method).toBe("GET");
		expect(init.headers).toEqual({ Authorization: "Bearer sk-test-1234567890abcdef" });

		expect(models).toEqual([{ id: "a" }, { id: "b" }] as readonly ModelInfo[]);
	});

	it("throws on non-OK HTTP status with a clear message", async () => {
		const fetchFn = vi.fn(
			() =>
				new Response("Unauthorized", {
					status: 401,
					headers: { "Content-Type": "text/plain" },
				}) as unknown as ReturnType<FetchLike>,
		);
		const provider = makeProvider(fetchFn);
		const listModels = provider.listModels;
		if (!listModels) throw new Error("listModels not defined");

		await expect(listModels()).rejects.toThrow(
			"listModels[openai-compat]: HTTP 401 — Unauthorized",
		);
	});

	it("throws on network error with a clear message", async () => {
		const fetchFn = vi.fn(() => {
			throw new Error("connection refused");
		}) as unknown as FetchLike;
		const provider = makeProvider(fetchFn);
		const listModels = provider.listModels;
		if (!listModels) throw new Error("listModels not defined");

		await expect(listModels()).rejects.toThrow(
			"listModels[openai-compat]: network error — connection refused",
		);
	});

	it("throws when response shape is missing data array", async () => {
		const fetchFn = vi.fn(() => jsonResponse({ models: [] }) as unknown as ReturnType<FetchLike>);
		const provider = makeProvider(fetchFn);
		const listModels = provider.listModels;
		if (!listModels) throw new Error("listModels not defined");

		await expect(listModels()).rejects.toThrow(
			'listModels[openai-compat]: unexpected response shape — missing "data" array',
		);
	});
});