Files
kilocode/packages/kilo-gateway/test/api/models.test.ts
T
chrarnoldusandkiloconnect[bot] 7e8f4a7cbd fix(gateway): keep image-output models in chat list
Stop filtering models by output_modalities so tool-capable image and
multimodal models (including openrouter/auto) appear in the gateway list.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-07-20 09:31:18 +00:00

333 lines
8.6 KiB
TypeScript

// Verifies fetchKiloModels typed result and 401 fallback behaviour.
import { test, expect } from "bun:test"
import { fetchKiloModels } from "../../src/api/models.js"
const VALID_RESPONSE = JSON.stringify({
data: [
{
id: "test/model-a",
name: "Test Model A",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
isFree: false,
mayTrainOnYourPrompts: true,
hasUserByokAvailable: true,
},
],
})
const VALID_BENCH_RESPONSE = JSON.stringify({
data: [
{
id: "test/model-a",
name: "Test Model A",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
terminalBench: {
overallScore: 0.551,
avgAttemptCostUsd: 53.37,
},
},
],
})
const VALID_AUTO_ROUTING_RESPONSE = JSON.stringify({
data: [
{
id: "kilo-auto/efficient",
name: "Kilo Auto Efficient",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
autoRouting: {
models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"],
},
},
],
})
const INVALID_BENCH_RESPONSE = JSON.stringify({
data: [
{
id: "test/model-a",
name: "Test Model A",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
terminalBench: {
overallScore: 0.551,
},
},
],
})
function stubFetch(fn: (input: string | URL | Request, init?: RequestInit) => Promise<Response>) {
;(globalThis as any).fetch = fn
}
test("returns empty models and error when both auth and public requests return 401", async () => {
const orig = globalThis.fetch
stubFetch(async () => new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }))
const result = await fetchKiloModels({ kilocodeToken: "bad-token" })
;(globalThis as any).fetch = orig
expect(result.models).toEqual({})
expect(result.error).toBeDefined()
})
test("falls back to public endpoint on 401 and returns models", async () => {
const orig = globalThis.fetch
let callCount = 0
stubFetch(async () => {
callCount++
if (callCount === 1) {
return new Response("Unauthorized", { status: 401, statusText: "Unauthorized" })
}
return new Response(VALID_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
})
})
const result = await fetchKiloModels({
kilocodeToken: "expired-token",
kilocodeOrganizationId: "org-123",
})
;(globalThis as any).fetch = orig
expect(callCount).toBe(2)
expect(result.error).toBeUndefined()
expect(Object.keys(result.models).length).toBeGreaterThan(0)
})
test("returns error with kind=network on fetch exception", async () => {
const orig = globalThis.fetch
stubFetch(async () => {
throw new Error("network error")
})
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.models).toEqual({})
expect(result.error?.kind).toBe("network")
})
test("returns error with kind=http on non-auth HTTP error (e.g. 500)", async () => {
const orig = globalThis.fetch
stubFetch(async () => new Response("Server Error", { status: 500, statusText: "Internal Server Error" }))
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.models).toEqual({})
expect(result.error?.kind).toBe("http")
expect(result.error?.status).toBe(500)
})
test("returns models without error on success", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(VALID_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["test/model-a"]).toMatchObject({
isFree: false,
mayTrainOnYourPrompts: true,
hasUserByokAvailable: true,
})
})
test("preserves Terminal Bench metadata as a dedicated model field", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(VALID_BENCH_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["test/model-a"].terminalBench).toEqual({
overallScore: 0.551,
avgAttemptCostUsd: 53.37,
})
})
test("preserves Auto Efficient routing metadata as a dedicated model field", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(VALID_AUTO_ROUTING_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["kilo-auto/efficient"].autoRouting).toEqual({
models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"],
})
})
test("omits malformed Terminal Bench metadata without rejecting the catalog", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(INVALID_BENCH_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["test/model-a"].terminalBench).toBeUndefined()
})
test("returns error with kind=schema when response body is invalid JSON", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response("not valid json{{{{", {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.models).toEqual({})
expect(result.error?.kind).toBe("schema")
})
const MIXED_MODALITY_RESPONSE = JSON.stringify({
data: [
{
id: "openrouter/auto",
name: "Auto Router",
context_length: 2000000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["text", "image"],
},
supported_parameters: ["tools", "temperature"],
},
{
id: "openrouter/auto-beta",
name: "Auto Router (Beta)",
context_length: 2000000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["text", "image"],
},
supported_parameters: ["tools", "temperature"],
},
{
id: "black-forest-labs/flux-1.1-pro",
name: "FLUX 1.1 Pro",
context_length: 4096,
max_completion_tokens: 4096,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["image"],
},
supported_parameters: ["tools"],
},
{
id: "test/no-tools",
name: "No Tools Model",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["temperature"],
},
{
id: "test/model-a",
name: "Test Model A",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
},
],
})
test("keeps image-output models with tools and drops models without tools", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(MIXED_MODALITY_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["openrouter/auto"]).toBeDefined()
expect(result.models["openrouter/auto-beta"]).toBeDefined()
expect(result.models["black-forest-labs/flux-1.1-pro"]).toBeDefined()
expect(result.models["test/model-a"]).toBeDefined()
expect(result.models["test/no-tools"]).toBeUndefined()
})