From 94c8f930a9cdf7132361ed4d93a7a22b38d0a90e Mon Sep 17 00:00:00 2001 From: hank9999 Date: Mon, 27 Apr 2026 19:42:50 +0100 Subject: [PATCH] fix(dashboard): order model leaderboard by total cost The model rankings query was sorting by request count, which contradicted the "Cost Leaderboard" framing and diverged from the user and provider scopes that both order by sum(cost_usd). Switch the ORDER BY to sum(cost_usd) DESC with request count as tiebreaker, matching the provider model sub-rows pattern. --- src/repository/leaderboard.ts | 2 +- .../leaderboard-provider-metrics.test.ts | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/repository/leaderboard.ts b/src/repository/leaderboard.ts index 18ace83f7..4c29545c0 100644 --- a/src/repository/leaderboard.ts +++ b/src/repository/leaderboard.ts @@ -1153,7 +1153,7 @@ async function findModelLeaderboardWithTimezone( .from(usageLedger) .where(and(LEDGER_BILLING_CONDITION, buildDateCondition(period, timezone, dateRange))) .groupBy(modelField) - .orderBy(desc(sql`count(*)`)); // 按请求数排序 + .orderBy(desc(sql`sum(${usageLedger.costUsd})`), desc(sql`count(*)`)); return rankings .filter((entry) => entry.model !== null && entry.model !== "") diff --git a/tests/unit/repository/leaderboard-provider-metrics.test.ts b/tests/unit/repository/leaderboard-provider-metrics.test.ts index ced8b33ed..2cc659d14 100644 --- a/tests/unit/repository/leaderboard-provider-metrics.test.ts +++ b/tests/unit/repository/leaderboard-provider-metrics.test.ts @@ -691,3 +691,52 @@ describe("Model Leaderboard basis handling", () => { }); }); }); + +describe("Model Leaderboard sort order", () => { + beforeEach(() => { + vi.resetModules(); + selectCallIndex = 0; + chainMocks = []; + mockSelect.mockClear(); + mocks.resolveSystemTimezone.mockResolvedValue("UTC"); + mocks.getSystemSettings.mockResolvedValue({ billingModelSource: "redirected" }); + }); + + it("orders by total cost descending with request count as tiebreaker", async () => { + chainMocks = [ + createChainMock([ + { + model: "expensive-low-volume", + totalRequests: 5, + totalCost: "50.0", + totalTokens: 1000, + successRate: 1.0, + }, + { + model: "cheap-high-volume", + totalRequests: 200, + totalCost: "1.0", + totalTokens: 100000, + successRate: 1.0, + }, + ]), + ]; + + const { findDailyModelLeaderboard } = await import("@/repository/leaderboard"); + const result = await findDailyModelLeaderboard(); + + expect(result).toHaveLength(2); + expect(result[0].model).toBe("expensive-low-volume"); + expect(result[0].totalCost).toBe(50); + expect(result[1].model).toBe("cheap-high-volume"); + expect(result[1].totalCost).toBe(1); + + const orderByMock = chainMocks[0].orderBy; + expect(orderByMock).toHaveBeenCalledTimes(1); + + const args = orderByMock.mock.calls[0]; + expect(args).toHaveLength(2); + expect(JSON.stringify(args[0])).toContain("sum"); + expect(JSON.stringify(args[1])).toContain("count"); + }); +});