168 lines
5.3 KiB
TypeScript
Executable File
168 lines
5.3 KiB
TypeScript
Executable File
import { assert, assertEquals } from "@std/assert";
|
|
import { Router } from "../../../packages/core/src/mod.ts";
|
|
import { registerCatalogRoutes } from "./catalog.ts";
|
|
import { type AppContext, NullToolExecutor, VERSION } from "../context.ts";
|
|
import { ProviderManager } from "../../../packages/providers/src/mod.ts";
|
|
import { Metrics } from "../../../packages/telemetry/src/metrics.ts";
|
|
import { LogBus } from "../../../packages/telemetry/src/logbus.ts";
|
|
import { VirtualKeyManager } from "../../../packages/governance/src/virtual_keys.ts";
|
|
import { UsageTracker } from "../../../packages/telemetry/src/usagestore.ts";
|
|
import { MCPRegistry } from "../../../packages/mcp/src/registry.ts";
|
|
import { PluginManager } from "../../../packages/plugins/src/lifecycle.ts";
|
|
|
|
// The catalog route is not yet wired into main.ts (the orchestrator applies the
|
|
// registration lines), so these tests register it on a bare Router directly.
|
|
|
|
const base = "http://gateway.test";
|
|
|
|
function usageRecord(provider: string, model: string, cost: number) {
|
|
return {
|
|
ts: new Date().toISOString(),
|
|
provider,
|
|
model,
|
|
promptTokens: 10,
|
|
completionTokens: 20,
|
|
totalTokens: 30,
|
|
costMicroUsd: cost,
|
|
durationMs: 5,
|
|
status: 200,
|
|
stream: false,
|
|
cacheHit: false,
|
|
};
|
|
}
|
|
|
|
function baseContext(
|
|
providers: ProviderManager,
|
|
usage: UsageTracker,
|
|
): AppContext {
|
|
return {
|
|
providers,
|
|
metrics: new Metrics(),
|
|
logBus: new LogBus(),
|
|
usage,
|
|
virtualKeys: new VirtualKeyManager(),
|
|
mcp: new MCPRegistry(),
|
|
plugins: new PluginManager(),
|
|
toolExecutor: new NullToolExecutor(),
|
|
version: VERSION,
|
|
};
|
|
}
|
|
|
|
function makeContext(): AppContext {
|
|
const providers = new ProviderManager([
|
|
{
|
|
id: "openai",
|
|
type: "openai",
|
|
apiKey: "k",
|
|
enabled: true,
|
|
models: ["gpt-4o", "gpt-4o-mini"],
|
|
priority: 0,
|
|
},
|
|
{
|
|
id: "local",
|
|
type: "lmstudio",
|
|
baseUrl: "http://localhost:1234/v1",
|
|
enabled: true,
|
|
models: ["gpt-4o", "local-llama"], // gpt-4o shared with openai
|
|
priority: 0,
|
|
},
|
|
]);
|
|
const usage = new UsageTracker();
|
|
usage.record(usageRecord("openai", "gpt-4o", 2_000_000));
|
|
usage.record(usageRecord("openai", "gpt-4o-mini", 500_000));
|
|
usage.record(usageRecord("local", "local-llama", 1_000_000));
|
|
return baseContext(providers, usage);
|
|
}
|
|
|
|
function catalogRouter(ctx: AppContext): Router {
|
|
const router = new Router();
|
|
registerCatalogRoutes(router, ctx);
|
|
return router;
|
|
}
|
|
|
|
Deno.test("GET /api/catalog joins providers with 24h analytics", async () => {
|
|
const router = catalogRouter(makeContext());
|
|
const res = await router.handle(new Request(`${base}/api/catalog`));
|
|
assertEquals(res.status, 200);
|
|
const body = await res.json();
|
|
|
|
const byId = new Map(body.providers.map((p: { id: string }) => [p.id, p]));
|
|
const openai = byId.get("openai") as {
|
|
type: string;
|
|
custom: boolean;
|
|
models: string[];
|
|
traffic24h: number;
|
|
cost24h: number;
|
|
};
|
|
assertEquals(openai.type, "openai");
|
|
assertEquals(openai.custom, false);
|
|
assertEquals(openai.models, ["gpt-4o", "gpt-4o-mini"]);
|
|
assertEquals(openai.traffic24h, 2);
|
|
assertEquals(openai.cost24h, 2.5); // (2_000_000 + 500_000) micro-USD
|
|
|
|
const local = byId.get("local") as { custom: boolean; traffic24h: number };
|
|
assertEquals(local.custom, true); // lmstudio is a generic custom endpoint
|
|
assertEquals(local.traffic24h, 1);
|
|
|
|
// Totals: 2 providers, 3 distinct models (gpt-4o shared), 3 requests, 3.5c.
|
|
assertEquals(body.totals.providers, 2);
|
|
assertEquals(body.totals.models, 3);
|
|
assertEquals(body.totals.requests24h, 3);
|
|
assertEquals(body.totals.cost24h, 3.5);
|
|
});
|
|
|
|
Deno.test("GET /api/catalog omits disabled providers", async () => {
|
|
const providers = new ProviderManager([
|
|
{
|
|
id: "openai",
|
|
type: "openai",
|
|
apiKey: "k",
|
|
enabled: true,
|
|
models: ["gpt-4o", "gpt-4o-mini"],
|
|
priority: 0,
|
|
},
|
|
{
|
|
id: "retired",
|
|
type: "openai",
|
|
apiKey: "k",
|
|
enabled: false,
|
|
// gpt-4o-mini shared with openai; retired-only exposes the exclusion.
|
|
models: ["gpt-4o-mini", "retired-only"],
|
|
priority: 0,
|
|
},
|
|
]);
|
|
const router = catalogRouter(baseContext(providers, new UsageTracker()));
|
|
const res = await router.handle(new Request(`${base}/api/catalog`));
|
|
assertEquals(res.status, 200);
|
|
const body = await res.json();
|
|
|
|
const ids = body.providers.map((p: { id: string }) => p.id);
|
|
assert(ids.includes("openai"));
|
|
assert(!ids.includes("retired")); // disabled provider absent from listing
|
|
|
|
// Totals derive from the enabled provider only: 1 provider, 2 distinct
|
|
// models (the disabled account's retired-only is excluded).
|
|
assertEquals(body.totals.providers, 1);
|
|
assertEquals(body.totals.models, 2);
|
|
});
|
|
|
|
Deno.test("GET /api/catalog reports zeros when no analytics exist", async () => {
|
|
const providers = new ProviderManager([
|
|
{
|
|
id: "openai",
|
|
type: "openai",
|
|
apiKey: "k",
|
|
enabled: true,
|
|
models: ["gpt-4o"],
|
|
priority: 0,
|
|
},
|
|
]);
|
|
const router = catalogRouter(baseContext(providers, new UsageTracker()));
|
|
const res = await router.handle(new Request(`${base}/api/catalog`));
|
|
const body = await res.json();
|
|
assertEquals(body.providers[0].traffic24h, 0);
|
|
assertEquals(body.providers[0].cost24h, 0);
|
|
assertEquals(body.totals.requests24h, 0);
|
|
assert(Array.isArray(body.providers));
|
|
});
|