| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- import { describe, expect, it } from "vitest";
- import {
- decideCacheHitRateAnomalies,
- type CacheHitRateAlertMetric,
- type CacheHitRateAlertDecisionSettings,
- } from "@/lib/cache-hit-rate-alert/decision";
- function metric(
- input: Partial<CacheHitRateAlertMetric> & { providerId: number; model: string }
- ): CacheHitRateAlertMetric {
- return {
- providerId: input.providerId,
- model: input.model,
- totalRequests: input.totalRequests ?? 100,
- denominatorTokens: input.denominatorTokens ?? 10000,
- hitRateTokens: input.hitRateTokens ?? 0,
- eligibleRequests: input.eligibleRequests ?? 100,
- eligibleDenominatorTokens: input.eligibleDenominatorTokens ?? 10000,
- hitRateTokensEligible: input.hitRateTokensEligible ?? input.hitRateTokens ?? 0,
- };
- }
- const defaultSettings: CacheHitRateAlertDecisionSettings = {
- absMin: 0.05,
- dropRel: 0.3,
- dropAbs: 0.1,
- minEligibleRequests: 20,
- minEligibleTokens: 0,
- topN: 10,
- };
- describe("decideCacheHitRateAnomalies", () => {
- it("should return empty when topN is 0", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.2 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, topN: 0 },
- });
- expect(anomalies).toHaveLength(0);
- });
- it("should prefer historical baseline over today/prev", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.2 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.4 })],
- today: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.35 })],
- historical: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5 })],
- settings: defaultSettings,
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baselineSource).toBe("historical");
- });
- it("should fall back to today baseline when historical kind-sample is insufficient", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.2 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.4 })],
- today: [
- metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5, eligibleRequests: 50 }),
- ],
- historical: [
- metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.9, eligibleRequests: 1 }),
- ],
- settings: defaultSettings,
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baselineSource).toBe("today");
- });
- it("should fall back to prev baseline when historical/today kind-samples are insufficient", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.1 })],
- prev: [
- metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.6, eligibleRequests: 50 }),
- ],
- today: [
- metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.9, eligibleRequests: 1 }),
- ],
- historical: [
- metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.9, eligibleRequests: 1 }),
- ],
- settings: { ...defaultSettings, absMin: 0.01 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baselineSource).toBe("prev");
- });
- it("should treat baseline as insufficient when eligible tokens below minEligibleTokens", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({
- providerId: 1,
- model: "m",
- eligibleRequests: 50,
- eligibleDenominatorTokens: 2000,
- hitRateTokensEligible: 0.1,
- }),
- ],
- prev: [],
- today: [
- metric({
- providerId: 1,
- model: "m",
- eligibleRequests: 50,
- eligibleDenominatorTokens: 2000,
- hitRateTokensEligible: 0.6,
- }),
- ],
- historical: [
- metric({
- providerId: 1,
- model: "m",
- eligibleRequests: 50,
- eligibleDenominatorTokens: 10,
- hitRateTokensEligible: 0.9,
- }),
- ],
- settings: { ...defaultSettings, absMin: 0.01, minEligibleTokens: 1000 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baselineSource).toBe("today");
- });
- it("should fall back to overall when eligible sample is insufficient", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({
- providerId: 1,
- model: "m",
- totalRequests: 100,
- denominatorTokens: 10000,
- hitRateTokens: 0.1,
- eligibleRequests: 1,
- eligibleDenominatorTokens: 100,
- hitRateTokensEligible: 0,
- }),
- ],
- prev: [
- metric({
- providerId: 1,
- model: "m",
- totalRequests: 100,
- denominatorTokens: 10000,
- hitRateTokens: 0.5,
- eligibleRequests: 1,
- eligibleDenominatorTokens: 100,
- hitRateTokensEligible: 0.5,
- }),
- ],
- today: [],
- historical: [],
- settings: defaultSettings,
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].current.kind).toBe("overall");
- expect(anomalies[0].baseline?.kind).toBe("overall");
- expect(anomalies[0].reasonCodes).toContain("eligible_insufficient");
- });
- it("should fall back to overall when eligible tokens are insufficient", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({
- providerId: 1,
- model: "m",
- totalRequests: 50,
- denominatorTokens: 2000,
- hitRateTokens: 0.1,
- eligibleRequests: 50,
- eligibleDenominatorTokens: 10,
- hitRateTokensEligible: 0.9,
- }),
- ],
- prev: [
- metric({
- providerId: 1,
- model: "m",
- totalRequests: 50,
- denominatorTokens: 2000,
- hitRateTokens: 0.6,
- eligibleRequests: 50,
- eligibleDenominatorTokens: 10,
- hitRateTokensEligible: 0.9,
- }),
- ],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, minEligibleTokens: 1000 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].current.kind).toBe("overall");
- expect(anomalies[0].baseline?.kind).toBe("overall");
- expect(anomalies[0].reasonCodes).toContain("eligible_insufficient");
- expect(anomalies[0].reasonCodes).toContain("use_overall");
- });
- it("should not compare eligible current against overall baseline", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({
- providerId: 1,
- model: "m",
- eligibleRequests: 100,
- eligibleDenominatorTokens: 10000,
- hitRateTokensEligible: 0.2,
- totalRequests: 100,
- denominatorTokens: 10000,
- hitRateTokens: 0.2,
- }),
- ],
- prev: [
- metric({
- providerId: 1,
- model: "m",
- // baseline eligible 不足,但 overall 足够
- eligibleRequests: 1,
- eligibleDenominatorTokens: 100,
- hitRateTokensEligible: 0.9,
- totalRequests: 100,
- denominatorTokens: 10000,
- hitRateTokens: 0.9,
- }),
- ],
- today: [],
- historical: [],
- settings: defaultSettings,
- });
- expect(anomalies).toHaveLength(0);
- });
- it("should filter invalid metrics in map inputs", () => {
- const current = new Map<string, CacheHitRateAlertMetric>([
- ["k1", metric({ providerId: 1, model: "", hitRateTokensEligible: 0 })],
- ["k2", metric({ providerId: 2, model: "m", hitRateTokensEligible: 0 })],
- ]);
- const prev = new Map<string, CacheHitRateAlertMetric>([
- ["k1", metric({ providerId: 1, model: "", hitRateTokensEligible: 0.2 })],
- ["k2", metric({ providerId: 2, model: "m", hitRateTokensEligible: 0.2 })],
- ]);
- const anomalies = decideCacheHitRateAnomalies({
- current,
- prev,
- today: new Map<string, CacheHitRateAlertMetric>(),
- historical: new Map<string, CacheHitRateAlertMetric>(),
- settings: { ...defaultSettings, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].providerId).toBe(2);
- expect(anomalies[0].model).toBe("m");
- });
- it("should return empty when eligible and overall samples are insufficient", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({
- providerId: 1,
- model: "m",
- totalRequests: 1,
- denominatorTokens: 10,
- hitRateTokens: 0,
- eligibleRequests: 1,
- eligibleDenominatorTokens: 10,
- hitRateTokensEligible: 0,
- }),
- ],
- prev: [],
- today: [],
- historical: [],
- settings: { ...defaultSettings, minEligibleRequests: 20, minEligibleTokens: 1000 },
- });
- expect(anomalies).toHaveLength(0);
- });
- it("should trigger drop_abs_rel when thresholds are met", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.2 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].reasonCodes).toContain("drop_abs_rel");
- expect(anomalies[0].dropAbs).toBeCloseTo(0.3, 10);
- });
- it("should not trigger drop_abs_rel when only dropAbs is met (AND)", () => {
- // baseline=0.5, current=0.375
- // dropAbs=0.125 >= 0.1(满足),dropRel=0.125/0.5=0.25 < 0.3(不满足)
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.375 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, dropAbs: 0.1, dropRel: 0.3 },
- });
- expect(anomalies).toHaveLength(0);
- });
- it("should not trigger drop_abs_rel when only dropRel is met (AND)", () => {
- // baseline=0.25, current=0.15625
- // dropAbs=0.09375 < 0.1(不满足),dropRel=0.09375/0.25=0.375 >= 0.3(满足)
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.15625 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.25 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, dropAbs: 0.1, dropRel: 0.3 },
- });
- expect(anomalies).toHaveLength(0);
- });
- it("should trigger abs_min when current is below absMin", () => {
- const shouldTrigger = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.03 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.2 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(shouldTrigger).toHaveLength(1);
- expect(shouldTrigger[0].reasonCodes).toContain("abs_min");
- const shouldNotTrigger = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.06 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.04 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(shouldNotTrigger).toHaveLength(0);
- });
- it("abs_min should not trigger when current equals absMin", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.05 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.5 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.05, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(anomalies).toHaveLength(0);
- });
- it("abs_min 在缺失基线时也应触发", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.01 })],
- prev: [],
- today: [],
- historical: [],
- settings: { ...defaultSettings, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baselineSource).toBeNull();
- expect(anomalies[0].baseline).toBeNull();
- expect(anomalies[0].deltaAbs).toBeNull();
- expect(anomalies[0].deltaRel).toBeNull();
- expect(anomalies[0].dropAbs).toBeNull();
- expect(anomalies[0].reasonCodes).toContain("baseline_missing");
- expect(anomalies[0].reasonCodes).toContain("abs_min");
- });
- it("dropAbs 在 current 高于 baseline 且仅触发 abs_min 时应 clamp 为 0", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.04 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.01 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.05, dropAbs: 0.1, dropRel: 0.3 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].reasonCodes).toContain("abs_min");
- expect(anomalies[0].reasonCodes).not.toContain("drop_abs_rel");
- expect(anomalies[0].dropAbs).toBe(0);
- });
- it("should set deltaRel to null when baseline hit rate is 0", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, dropAbs: 0.9, dropRel: 0.9 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].baseline?.hitRateTokens).toBe(0);
- expect(anomalies[0].deltaRel).toBeNull();
- });
- it("should trigger drop_abs_rel when thresholds are met exactly (>=)", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.3 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.4 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, dropAbs: 0.1, dropRel: 0.25 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].reasonCodes).toContain("drop_abs_rel");
- expect(anomalies[0].dropAbs).toBeCloseTo(0.1, 10);
- });
- it("should not add drop_abs_rel when only dropAbs is met (AND) even if abs_min triggers", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.04 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.06 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.05, dropAbs: 0.01, dropRel: 0.5 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].reasonCodes).toContain("abs_min");
- expect(anomalies[0].reasonCodes).not.toContain("drop_abs_rel");
- });
- it("should not add drop_abs_rel when only dropRel is met (AND) even if abs_min triggers", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.02 })],
- prev: [metric({ providerId: 1, model: "m", hitRateTokensEligible: 0.04 })],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.05, dropAbs: 0.03, dropRel: 0.5 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].reasonCodes).toContain("abs_min");
- expect(anomalies[0].reasonCodes).not.toContain("drop_abs_rel");
- });
- it("should sort by severity and respect topN", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({ providerId: 1, model: "a", hitRateTokensEligible: 0.1 }),
- metric({ providerId: 2, model: "b", hitRateTokensEligible: 0.25 }),
- ],
- prev: [
- metric({ providerId: 1, model: "a", hitRateTokensEligible: 0.6 }),
- metric({ providerId: 2, model: "b", hitRateTokensEligible: 0.5 }),
- ],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, topN: 1 },
- });
- expect(anomalies).toHaveLength(1);
- expect(anomalies[0].providerId).toBe(1);
- expect(anomalies[0].model).toBe("a");
- });
- it("should break severity ties by providerId/model for deterministic ordering", () => {
- const anomalies = decideCacheHitRateAnomalies({
- current: [
- metric({ providerId: 2, model: "b", hitRateTokensEligible: 0.1 }),
- metric({ providerId: 1, model: "a", hitRateTokensEligible: 0.1 }),
- ],
- prev: [
- metric({ providerId: 2, model: "b", hitRateTokensEligible: 0.6 }),
- metric({ providerId: 1, model: "a", hitRateTokensEligible: 0.6 }),
- ],
- today: [],
- historical: [],
- settings: { ...defaultSettings, absMin: 0.01, topN: 2 },
- });
- expect(anomalies).toHaveLength(2);
- expect(anomalies[0].providerId).toBe(1);
- expect(anomalies[0].model).toBe("a");
- expect(anomalies[1].providerId).toBe(2);
- expect(anomalies[1].model).toBe("b");
- });
- });
|