import { describe, expect, it, vi } from "vitest" ;
import { discoverKilocodeModels, KILOCODE_MODELS_URL } from "./provider-models.js" ;
function makeGatewayModel(overrides: Record<string, unknown> = {}) {
return {
id: "anthropic/claude-sonnet-4" ,
name: "Anthropic: Claude Sonnet 4" ,
created: 1700000000 ,
description: "A model" ,
context_length: 200000 ,
architecture: {
input_modalities: ["text" , "image" ],
output_modalities: ["text" ],
tokenizer: "Claude" ,
},
top_provider: {
is_moderated: false ,
max_completion_tokens: 8192 ,
},
pricing: {
prompt: "0.000003" ,
completion: "0.000015" ,
input_cache_read: "0.0000003" ,
input_cache_write: "0.00000375" ,
},
supported_parameters: ["max_tokens" , "temperature" , "tools" , "reasoning" ],
...overrides,
};
}
function makeAutoModel(overrides: Record<string, unknown> = {}) {
return makeGatewayModel({
id: "kilo/auto" ,
name: "Kilo: Auto" ,
context_length: 1000000 ,
architecture: {
input_modalities: ["text" , "image" ],
output_modalities: ["text" ],
tokenizer: "Other" ,
},
top_provider: {
is_moderated: false ,
max_completion_tokens: 128000 ,
},
pricing: {
prompt: "0.000005" ,
completion: "0.000025" ,
},
supported_parameters: ["max_tokens" , "temperature" , "tools" , "reasoning" , "include_reasoning" ],
...overrides,
});
}
async function withFetchPathTest(
mockFetch: ReturnType<typeof vi.fn>,
runAssertions: () => Promise<void >,
) {
const origNodeEnv = process.env.NODE_ENV;
const origVitest = process.env.VITEST;
delete process.env.NODE_ENV;
delete process.env.VITEST;
vi.stubGlobal("fetch" , mockFetch);
try {
await runAssertions();
} finally {
if (origNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = origNodeEnv;
}
if (origVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = origVitest;
}
vi.unstubAllGlobals();
}
}
describe("discoverKilocodeModels" , () => {
it("returns static catalog in test environment" , async () => {
const models = await discoverKilocodeModels();
expect(models.length).toBeGreaterThan(0 );
expect(models.some((m) => m.id === "kilo/auto" )).toBe(true );
});
it("static catalog has correct defaults for kilo/auto" , async () => {
const models = await discoverKilocodeModels();
const auto = models.find((m) => m.id === "kilo/auto" );
expect(auto).toBeDefined();
expect(auto?.name).toBe("Kilo Auto" );
expect(auto?.reasoning).toBe(true );
expect(auto?.input).toEqual(["text" , "image" ]);
expect(auto?.contextWindow).toBe(1000000 );
expect(auto?.maxTokens).toBe(128000 );
expect(auto?.cost).toEqual({ input: 0 , output: 0 , cacheRead: 0 , cacheWrite: 0 });
});
});
describe("discoverKilocodeModels (fetch path)" , () => {
it("parses gateway models with correct pricing conversion" , async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true ,
json: () =>
Promise.resolve({
data: [makeAutoModel(), makeGatewayModel()],
}),
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
expect(mockFetch).toHaveBeenCalledWith(
KILOCODE_MODELS_URL,
expect.objectContaining({
headers: { Accept: "application/json" },
}),
);
expect(models.length).toBe(2 );
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4" );
expect(sonnet).toBeDefined();
expect(sonnet?.cost.input).toBeCloseTo(3 .0 );
expect(sonnet?.cost.output).toBeCloseTo(15 .0 );
expect(sonnet?.cost.cacheRead).toBeCloseTo(0 .3 );
expect(sonnet?.cost.cacheWrite).toBeCloseTo(3 .75 );
expect(sonnet?.input).toEqual(["text" , "image" ]);
expect(sonnet?.reasoning).toBe(true );
expect(sonnet?.contextWindow).toBe(200000 );
expect(sonnet?.maxTokens).toBe(8192 );
});
});
it("falls back to static catalog on network error" , async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error("network error" ));
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
expect(models.length).toBeGreaterThan(0 );
expect(models.some((m) => m.id === "kilo/auto" )).toBe(true );
});
});
it("falls back to static catalog on HTTP error" , async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false ,
status: 500 ,
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
expect(models.length).toBeGreaterThan(0 );
expect(models.some((m) => m.id === "kilo/auto" )).toBe(true );
});
});
it("ensures kilo/auto is present even when API doesn't return it" , async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true ,
json: () =>
Promise.resolve({
data: [makeGatewayModel()],
}),
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
expect(models.some((m) => m.id === "kilo/auto" )).toBe(true );
expect(models.some((m) => m.id === "anthropic/claude-sonnet-4" )).toBe(true );
});
});
it("detects text-only models without image modality" , async () => {
const textOnlyModel = makeGatewayModel({
id: "some/text-model" ,
architecture: {
input_modalities: ["text" ],
output_modalities: ["text" ],
},
supported_parameters: ["max_tokens" , "temperature" ],
});
const mockFetch = vi.fn().mockResolvedValue({
ok: true ,
json: () => Promise.resolve({ data: [textOnlyModel] }),
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
const textModel = models.find((m) => m.id === "some/text-model" );
expect(textModel?.input).toEqual(["text" ]);
expect(textModel?.reasoning).toBe(false );
});
});
it("keeps a later valid duplicate when an earlier entry is malformed" , async () => {
const malformedAutoModel = makeAutoModel({
name: "Broken Kilo Auto" ,
pricing: undefined,
});
const mockFetch = vi.fn().mockResolvedValue({
ok: true ,
json: () =>
Promise.resolve({
data: [malformedAutoModel, makeAutoModel(), makeGatewayModel()],
}),
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
const auto = models.find((m) => m.id === "kilo/auto" );
expect(auto).toBeDefined();
expect(auto?.name).toBe("Kilo: Auto" );
expect(auto?.cost.input).toBeCloseTo(5 .0 );
expect(models.some((m) => m.id === "anthropic/claude-sonnet-4" )).toBe(true );
});
});
});
Messung V0.5 in Prozent C=99 H=100 G=99
¤ Dauer der Verarbeitung: 0.34 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland