Files
2026-07-13 12:28:55 +08:00

128 lines
4.7 KiB
TypeScript

import { expect, test } from "bun:test"
import { Models, ModelsDevError } from "../src/index.js"
interface Call {
url: URL
init: RequestInit
}
function stub(data: unknown, init?: ResponseInit) {
const calls: Call[] = []
const fetch = (async (input: unknown, requestInit?: RequestInit) => {
calls.push({ url: input as URL, init: requestInit ?? {} })
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
...init,
})
}) as typeof globalThis.fetch
return { calls, fetch }
}
function headers(call: Call) {
return new Headers(call.init.headers)
}
test("providers() GETs /api.json with the default base URL", async () => {
const providers = { anthropic: { id: "anthropic" } }
const { calls, fetch } = stub(providers)
const client = Models.make({ fetch })
const result = await client.providers()
expect(result).toEqual(providers as never)
expect(calls[0]?.url.href).toBe("https://models.dev/api.json")
expect(calls[0]?.init.method).toBe("GET")
})
test("models() and catalog() hit their endpoints", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch })
await client.models()
await client.catalog()
expect(calls.map((call) => call.url.href)).toEqual(["https://models.dev/models.json", "https://models.dev/catalog.json"])
})
test("baseUrl with subpath is preserved, with or without trailing slash", async () => {
const { calls, fetch } = stub({})
await Models.make({ fetch, baseUrl: "https://example.com/mirror" }).providers()
await Models.make({ fetch, baseUrl: "https://example.com/mirror/" }).providers()
expect(calls.map((call) => call.url.href)).toEqual([
"https://example.com/mirror/api.json",
"https://example.com/mirror/api.json",
])
})
test("does not add headers by default", async () => {
const { calls, fetch } = stub({})
await Models.make({ fetch }).providers()
expect([...headers(calls[0]!).entries()]).toEqual([])
})
test("request headers override client headers", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch, headers: { "user-agent": "custom", "x-one": "client", "x-two": "client" } })
await client.providers({ headers: { "x-two": "request" } })
const sent = headers(calls[0]!)
expect(sent.get("user-agent")).toBe("custom")
expect(sent.get("x-one")).toBe("client")
expect(sent.get("x-two")).toBe("request")
})
test("abort signal is passed through", async () => {
const { calls, fetch } = stub({})
const controller = new AbortController()
await Models.make({ fetch }).providers({ signal: controller.signal })
expect(calls[0]?.init.signal).toBe(controller.signal)
})
test("stateless: every call fetches again", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch })
await client.providers()
await client.providers()
expect(calls.length).toBe(2)
})
test("network failure throws Transport with cause", async () => {
const failure = new Error("boom")
const client = Models.make({
fetch: (() => Promise.reject(failure)) as unknown as typeof globalThis.fetch,
})
const error = await client.providers().catch((error: unknown) => error)
expect(error).toBeInstanceOf(ModelsDevError)
expect((error as ModelsDevError).reason).toBe("Transport")
expect((error as ModelsDevError).cause).toBe(failure)
})
test("non-2xx throws UnexpectedStatus with the status in cause", async () => {
const { fetch } = stub({ message: "not found" }, { status: 404 })
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect(error).toBeInstanceOf(ModelsDevError)
expect((error as ModelsDevError).reason).toBe("UnexpectedStatus")
expect((error as ModelsDevError).cause).toEqual({ status: 404 })
})
test("invalid JSON throws MalformedResponse", async () => {
const fetch = (async () => new Response("not json")) as unknown as typeof globalThis.fetch
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect((error as ModelsDevError).reason).toBe("MalformedResponse")
})
test("empty body throws MalformedResponse", async () => {
const fetch = (async () => new Response("")) as unknown as typeof globalThis.fetch
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect((error as ModelsDevError).reason).toBe("MalformedResponse")
})
test("global fetch is resolved lazily so late polyfills work", async () => {
const original = globalThis.fetch
const client = Models.make()
try {
const { calls, fetch } = stub({ late: true })
globalThis.fetch = fetch
const result = await client.providers()
expect(result).toEqual({ late: true } as never)
expect(calls.length).toBe(1)
} finally {
globalThis.fetch = original
}
})