mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
Converted amounts no longer start with ≈; it took room and the USD value is still shown on hover. Since the rate is entered by hand, converted amounts now show at most two decimals (yen keeps two fewer), instead of the 4–6 used for USD in logs and hover text. An amount that would round to zero is written with two significant digits rather than as 0.00.
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
SPEED_ESTIMATE_MIN_OUTPUT_TOKENS,
|
|
SPEED_MIN_OUTPUT_TOKENS,
|
|
formatEstimatedTokensPerSecond,
|
|
formatFirstTokenSeconds,
|
|
formatMoney,
|
|
formatMoneyCompact,
|
|
formatMoneyShort,
|
|
formatMoneyTick,
|
|
formatOutputTokensPerSecond,
|
|
formatTokensCompact,
|
|
formatTokensPerSecond,
|
|
formatTokensShort,
|
|
resolveMoneyDisplay,
|
|
resolveSidebarToday,
|
|
getAggregateTokensPerSecond,
|
|
getEstimatedTokensPerSecond,
|
|
getOutputTokensPerSecond,
|
|
getLocaleFromLanguage,
|
|
sumSpeedTotals,
|
|
USD_DISPLAY,
|
|
} from "@/components/usage/format";
|
|
|
|
describe("usage format helpers", () => {
|
|
it("formats Traditional Chinese token units with Traditional characters", () => {
|
|
expect(formatTokensShort(12_345, "zh-TW")).toBe("1.2 萬");
|
|
expect(formatTokensShort(123_456_789, "zh-Hant", 2)).toBe("1.23 億");
|
|
});
|
|
|
|
it("resolves Traditional Chinese locale aliases", () => {
|
|
expect(getLocaleFromLanguage("zh_TW")).toBe("zh-TW");
|
|
expect(getLocaleFromLanguage("zh-HK")).toBe("zh-TW");
|
|
});
|
|
|
|
it("resolves Turkish to a Turkish date locale", () => {
|
|
expect(getLocaleFromLanguage("tr")).toBe("tr-TR");
|
|
expect(getLocaleFromLanguage("tr-TR")).toBe("tr-TR");
|
|
});
|
|
|
|
it("calculates speed from generation time after the first token", () => {
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 120,
|
|
latencyMs: 10_000,
|
|
firstTokenMs: 4_000,
|
|
}),
|
|
).toBe(20);
|
|
});
|
|
|
|
it("ignores durationMs: speed is always output / (latency - first token)", () => {
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 120,
|
|
latencyMs: 10_000,
|
|
firstTokenMs: 4_000,
|
|
durationMs: 3_000,
|
|
} as Parameters<typeof getOutputTokensPerSecond>[0]),
|
|
).toBe(20);
|
|
});
|
|
|
|
it("does not compute speed without first-token timing (session logs, non-streaming)", () => {
|
|
expect(
|
|
getOutputTokensPerSecond({ outputTokens: 1_200, latencyMs: 10_000 }),
|
|
).toBeNull();
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 1_200,
|
|
latencyMs: undefined,
|
|
firstTokenMs: 1_000,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("skips requests with fewer than 100 output tokens", () => {
|
|
expect(SPEED_MIN_OUTPUT_TOKENS).toBe(100);
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 99,
|
|
latencyMs: 1_000,
|
|
firstTokenMs: 900,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 100,
|
|
latencyMs: 2_000,
|
|
firstTokenMs: 1_000,
|
|
}),
|
|
).toBe(100);
|
|
});
|
|
|
|
it("does not show speed when the generation time is not positive", () => {
|
|
expect(
|
|
formatOutputTokensPerSecond({
|
|
outputTokens: 0,
|
|
latencyMs: 10_000,
|
|
firstTokenMs: 1_000,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
formatOutputTokensPerSecond({
|
|
outputTokens: 120,
|
|
latencyMs: 4_000,
|
|
firstTokenMs: 4_000,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
formatOutputTokensPerSecond({
|
|
outputTokens: 120,
|
|
latencyMs: 3_000,
|
|
firstTokenMs: 4_000,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("hides speed when the generation window is a transport burst", () => {
|
|
// 2842 tps 实例:输出在首字后 19ms 内全部到达,算出来的是传输突发
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 154,
|
|
latencyMs: 3_444,
|
|
firstTokenMs: 3_425,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
getOutputTokensPerSecond({
|
|
outputTokens: 150,
|
|
latencyMs: 1_100,
|
|
firstTokenMs: 1_000,
|
|
}),
|
|
).toBe(1500);
|
|
});
|
|
|
|
it("formats speed with integer or single-decimal precision", () => {
|
|
expect(
|
|
formatOutputTokensPerSecond({
|
|
outputTokens: 121,
|
|
latencyMs: 11_000,
|
|
firstTokenMs: 1_000,
|
|
}),
|
|
).toBe("12");
|
|
expect(formatTokensPerSecond(0.25)).toBe("0.3");
|
|
expect(formatTokensPerSecond(null)).toBeNull();
|
|
});
|
|
|
|
it("estimates speed for session-log requests from the whole duration", () => {
|
|
expect(SPEED_ESTIMATE_MIN_OUTPUT_TOKENS).toBe(200);
|
|
const session = {
|
|
outputTokens: 1_800,
|
|
latencyMs: 20_000,
|
|
dataSource: "session_log",
|
|
};
|
|
// 1800 token / 20 s,等首字的时间也算在内
|
|
expect(getEstimatedTokensPerSecond(session)).toBe(90);
|
|
expect(formatEstimatedTokensPerSecond(session)).toBe("90");
|
|
// 估算的不算精确速度
|
|
expect(getOutputTokensPerSecond(session)).toBeNull();
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, dataSource: "codex_session" }),
|
|
).toBe(90);
|
|
// 推出了首字的会话日志:仍按整段耗时估,不当成精确速度
|
|
const withFirstToken = { ...session, firstTokenMs: 4_000 };
|
|
expect(getEstimatedTokensPerSecond(withFirstToken)).toBe(90);
|
|
expect(getOutputTokensPerSecond(withFirstToken)).toBeNull();
|
|
expect(sumSpeedTotals([withFirstToken])).toEqual({
|
|
outputTokens: 0,
|
|
generationMs: 0,
|
|
});
|
|
});
|
|
|
|
it("formats first-token time in seconds", () => {
|
|
expect(formatFirstTokenSeconds(2_640)).toBe("2.6s");
|
|
expect(formatFirstTokenSeconds(0)).toBe("0.0s");
|
|
expect(formatFirstTokenSeconds(null)).toBeNull();
|
|
expect(formatFirstTokenSeconds(undefined)).toBeNull();
|
|
expect(formatFirstTokenSeconds(-1)).toBeNull();
|
|
});
|
|
|
|
it("does not estimate speed for routed, short, or untimed requests", () => {
|
|
const session = {
|
|
outputTokens: 1_800,
|
|
latencyMs: 20_000,
|
|
dataSource: "session_log",
|
|
};
|
|
// 路由服务的请求:非流式没有首字,也不估
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, dataSource: "proxy" }),
|
|
).toBeNull();
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, dataSource: undefined }),
|
|
).toBeNull();
|
|
// 输出不到 200
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, outputTokens: 199 }),
|
|
).toBeNull();
|
|
// 没估出耗时
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, latencyMs: 0 }),
|
|
).toBeNull();
|
|
// 耗时不到 1 秒
|
|
expect(
|
|
getEstimatedTokensPerSecond({ ...session, latencyMs: 999 }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("aggregates speed as total output over total generation time, not a mean of rates", () => {
|
|
const logs = [
|
|
// 1000 token / 10 s = 100 tok/s
|
|
{ outputTokens: 1_000, latencyMs: 11_000, firstTokenMs: 1_000 },
|
|
// 200 token / 0.5 s = 400 tok/s(逐条平均会被它拉高)
|
|
{ outputTokens: 200, latencyMs: 1_500, firstTokenMs: 1_000 },
|
|
// 不计:输出不到 100
|
|
{ outputTokens: 50, latencyMs: 1_100, firstTokenMs: 1_000 },
|
|
// 不计:没有首字
|
|
{ outputTokens: 5_000, latencyMs: 9_000 },
|
|
];
|
|
const totals = sumSpeedTotals(logs);
|
|
expect(totals).toEqual({ outputTokens: 1_200, generationMs: 10_500 });
|
|
const tps = getAggregateTokensPerSecond(
|
|
totals.outputTokens,
|
|
totals.generationMs,
|
|
);
|
|
expect(tps).toBeCloseTo(114.29, 2);
|
|
expect(getAggregateTokensPerSecond(0, 0)).toBeNull();
|
|
expect(getAggregateTokensPerSecond(500, 0)).toBeNull();
|
|
expect(getAggregateTokensPerSecond(undefined, 1_000)).toBeNull();
|
|
});
|
|
|
|
it("formats compact token counts with K/M/B and three significant digits", () => {
|
|
expect(formatTokensCompact(9_999, "en-US")).toBe("9,999");
|
|
expect(formatTokensCompact(48_210, "en-US")).toBe("48.2K");
|
|
expect(formatTokensCompact(189_400, "en-US")).toBe("189K");
|
|
expect(formatTokensCompact(999_960, "en-US")).toBe("1M");
|
|
expect(formatTokensCompact(18_200_000, "en-US")).toBe("18.2M");
|
|
expect(formatTokensCompact(2_340_000_000, "en-US")).toBe("2.34B");
|
|
expect(formatTokensCompact("x")).toBe("--");
|
|
});
|
|
});
|
|
|
|
describe("usage money formatting", () => {
|
|
it("keeps the USD output the old fmtUsd produced", () => {
|
|
expect(formatMoney(1.23456, 2, USD_DISPLAY)).toBe("$1.23");
|
|
expect(formatMoney("0.000123", 6, USD_DISPLAY)).toBe("$0.000123");
|
|
expect(formatMoney(null, 4, USD_DISPLAY)).toBe("--");
|
|
expect(formatMoney("x", 4, USD_DISPLAY, "")).toBe("");
|
|
});
|
|
|
|
it("trims compact amounts by magnitude", () => {
|
|
expect(
|
|
[0.0081, 0.0123, 1.25].map((usd) => formatMoneyCompact(usd, USD_DISPLAY)),
|
|
).toEqual(["$0.0081", "$0.012", "$1.25"]);
|
|
});
|
|
|
|
it("formats axis ticks without fixing decimals", () => {
|
|
expect(formatMoneyTick(0.5, USD_DISPLAY)).toBe("$0.5");
|
|
expect(formatMoneyTick(undefined, USD_DISPLAY)).toBe("$0");
|
|
});
|
|
});
|
|
|
|
describe("display currency", () => {
|
|
it("falls back to USD and the preset rate for unusable settings", () => {
|
|
expect(resolveMoneyDisplay(undefined)).toEqual(USD_DISPLAY);
|
|
expect(resolveMoneyDisplay({ currency: "GBP" })).toEqual(USD_DISPLAY);
|
|
// 汇率只在显示非美元时生效
|
|
expect(
|
|
resolveMoneyDisplay({ currency: "USD", exchangeRates: { CNY: 7 } }),
|
|
).toEqual(USD_DISPLAY);
|
|
expect(resolveMoneyDisplay({ currency: "CNY" })).toEqual({
|
|
currency: "CNY",
|
|
rate: 6.8,
|
|
});
|
|
for (const bad of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
expect(
|
|
resolveMoneyDisplay({ currency: "CNY", exchangeRates: { CNY: bad } })
|
|
.rate,
|
|
).toBe(6.8);
|
|
}
|
|
expect(
|
|
resolveMoneyDisplay({ currency: "CNY", exchangeRates: { CNY: 7.25 } }),
|
|
).toEqual({ currency: "CNY", rate: 7.25 });
|
|
});
|
|
|
|
it("writes converted amounts with the plain currency symbol", () => {
|
|
const cny = { currency: "CNY" as const, rate: 7 };
|
|
expect(formatMoney(1.5, 2, cny)).toBe("¥10.50");
|
|
expect(formatMoney(0, 2, cny)).toBe("¥0.00");
|
|
expect(formatMoney(null, 2, cny)).toBe("--");
|
|
expect(formatMoneyCompact(0.001, cny)).toBe("¥0.007");
|
|
expect(formatMoneyCompact(0.0015, cny)).toBe("¥0.011");
|
|
// 刻度值画图前已换算,只加符号
|
|
expect(formatMoneyTick(50, cny)).toBe("¥50");
|
|
});
|
|
|
|
it("writes each currency with its own symbol and groups thousands", () => {
|
|
expect(formatMoney(30470.55, 2, USD_DISPLAY)).toBe("$30,470.55");
|
|
expect(formatMoney(-1234, 0, USD_DISPLAY)).toBe("$-1,234");
|
|
expect(formatMoney(2, 2, { currency: "EUR", rate: 0.87 })).toBe("€1.74");
|
|
expect(formatMoney(2, 2, { currency: "TWD", rate: 32 })).toBe("NT$64.00");
|
|
expect(formatMoney(100, 2, { currency: "TRY", rate: 48 })).toBe(
|
|
"₺4,800.00",
|
|
);
|
|
expect(formatMoneyTick(20000, { currency: "JPY", rate: 158 })).toBe(
|
|
"¥20,000",
|
|
);
|
|
});
|
|
|
|
it("keeps converted amounts to two decimals at most", () => {
|
|
const twd = { currency: "TWD" as const, rate: 32 };
|
|
// 悬停用的 6 位、日志用的 4 位,换算后都只留两位
|
|
expect(formatMoney(64.912461, 6, twd)).toBe("NT$2,077.20");
|
|
expect(formatMoney(0.0505, 4, twd)).toBe("NT$1.62");
|
|
expect(formatMoney(1.5, 1, twd)).toBe("NT$48.0");
|
|
// 小到四舍五入成 0 的一笔写两位有效数字,不写成 0.00
|
|
expect(formatMoney(0.0001, 4, twd)).toBe("NT$0.0032");
|
|
expect(formatMoney(0, 4, twd)).toBe("NT$0.00");
|
|
// 美元照调用方给的位数
|
|
expect(formatMoney(0.0001, 4, USD_DISPLAY)).toBe("$0.0001");
|
|
expect(formatMoney(64.912461, 6, USD_DISPLAY)).toBe("$64.912461");
|
|
});
|
|
|
|
it("shows yen with two fewer decimals", () => {
|
|
const jpy = { currency: "JPY" as const, rate: 158 };
|
|
expect(formatMoney(0.0012, 4, jpy)).toBe("¥0.19");
|
|
expect(formatMoney(30470.55, 2, jpy)).toBe("¥4,814,347");
|
|
expect(formatMoney(1.5, 1, jpy)).toBe("¥237");
|
|
expect(formatMoneyShort(0.5, jpy)).toBe("¥79");
|
|
});
|
|
|
|
it("drops the decimals in narrow spots once the amount reaches 100", () => {
|
|
const cny = { currency: "CNY" as const, rate: 6.8 };
|
|
expect(formatMoneyShort(41.2, USD_DISPLAY)).toBe("$41.20");
|
|
expect(formatMoneyShort(123.456, USD_DISPLAY)).toBe("$123");
|
|
expect(formatMoneyShort(41.2, cny)).toBe("¥280");
|
|
expect(formatMoneyShort(14.5, cny)).toBe("¥98.60");
|
|
expect(formatMoneyShort(undefined, cny)).toBe("--");
|
|
});
|
|
});
|
|
|
|
describe("sidebar Today mode", () => {
|
|
it("defaults to cost and ignores unknown values", () => {
|
|
expect(resolveSidebarToday(undefined)).toBe("cost");
|
|
expect(resolveSidebarToday({ sidebarToday: "requests" })).toBe("cost");
|
|
expect(resolveSidebarToday({ sidebarToday: "tokens" })).toBe("tokens");
|
|
expect(resolveSidebarToday({ sidebarToday: "hidden" })).toBe("hidden");
|
|
});
|
|
});
|