diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json
index cecc03b88..f662d356e 100644
--- a/messages/en/dashboard.json
+++ b/messages/en/dashboard.json
@@ -356,20 +356,20 @@
"performance": {
"title": "Performance",
"ttfb": "TTFB",
- "tfft": "TFFT",
- "tfftShort": "TFFT",
+ "ttft": "TTFT",
+ "ttftShort": "TTFT",
"duration": "Total Duration",
"outputRate": "Output Rate",
"outputTokens": "Output Tokens"
},
"performanceTab": {
"noPerformanceData": "No performance data available",
- "tfftGauge": "Time to First Token",
+ "ttftGauge": "Time to First Token",
"outputRateGauge": "Output Rate",
"latencyBreakdown": "Latency Breakdown",
"generationTime": "Generation Time",
"segmentTtfb": "TTFB",
- "segmentTfft": "Token Wait",
+ "segmentTtft": "Token Wait",
"segmentTotal": "Total",
"assessment": {
"excellent": "Excellent",
@@ -689,7 +689,7 @@
"totalConsumedAmount": "Total Spend",
"successRate": "Success Rate",
"avgResponseTime": "Avg Response Time",
- "avgTtfbMs": "Avg TFFT",
+ "avgTtftMs": "Avg TTFT",
"avgTokensPerSecond": "Avg tok/s",
"avgCostPerRequest": "Avg Cost/Req",
"avgCostPerMillionTokens": "Avg Cost/1M Tokens",
diff --git a/messages/en/settings/statusPage.json b/messages/en/settings/statusPage.json
index 38d4d2fc8..a67dc4075 100644
--- a/messages/en/settings/statusPage.json
+++ b/messages/en/settings/statusPage.json
@@ -3,7 +3,7 @@
"description": "Configure the public status page's stats window, chart bucket size, and which groups/models should be exposed.",
"form": {
"windowHours": "Stats Window (hours)",
- "windowHoursDesc": "Window length used to compute TFFT and availability, and the total span the chart covers. Default: 24 hours.",
+ "windowHoursDesc": "Window length used to compute TTFT and availability, and the total span the chart covers. Default: 24 hours.",
"aggregationIntervalMinutes": "Chart Bucket (minutes)",
"aggregationIntervalMinutesDesc": "Length of each bucket on the chart timeline; controls chart granularity. Choose 5 / 15 / 30 / 60 minutes.",
"aggregationIntervalMinutesInvalid": "Public status aggregation interval must be one of 5, 15, 30, or 60 minutes.",
@@ -46,7 +46,7 @@
"heroPrimary": "AI SERVICES",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "Updated",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"history": "History",
"freshnessWindow": "Snapshot freshness",
@@ -76,7 +76,7 @@
},
"tooltip": {
"availability": "Availability",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"samples": "Samples",
"inferredFromNeighbors": "No requests in this window — inferred from neighbors",
diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json
index 3843684ba..05c297057 100644
--- a/messages/ja/dashboard.json
+++ b/messages/ja/dashboard.json
@@ -356,20 +356,20 @@
"performance": {
"title": "パフォーマンス",
"ttfb": "TTFB",
- "tfft": "TFFT",
- "tfftShort": "TFFT",
+ "ttft": "TTFT",
+ "ttftShort": "TTFT",
"duration": "総所要時間",
"outputRate": "出力速度",
"outputTokens": "出力トークン"
},
"performanceTab": {
"noPerformanceData": "パフォーマンスデータがありません",
- "tfftGauge": "初トークン到達時間",
+ "ttftGauge": "初トークン到達時間",
"outputRateGauge": "出力速度",
"latencyBreakdown": "レイテンシ内訳",
"generationTime": "生成時間",
"segmentTtfb": "TTFB",
- "segmentTfft": "トークン待機",
+ "segmentTtft": "トークン待機",
"segmentTotal": "合計",
"assessment": {
"excellent": "優秀",
@@ -689,7 +689,7 @@
"totalConsumedAmount": "総消費額",
"successRate": "成功率(%)",
"avgResponseTime": "平均応答時間",
- "avgTtfbMs": "平均TFFT",
+ "avgTtftMs": "平均TTFT",
"avgTokensPerSecond": "平均トークン/秒",
"avgCostPerRequest": "平均リクエスト単価",
"avgCostPerMillionTokens": "100万トークンあたりコスト",
diff --git a/messages/ja/settings/statusPage.json b/messages/ja/settings/statusPage.json
index e6574d771..51f2e61d7 100644
--- a/messages/ja/settings/statusPage.json
+++ b/messages/ja/settings/statusPage.json
@@ -3,7 +3,7 @@
"description": "公開ステータスページの統計ウィンドウ、チャートのバケットサイズ、および公開するグループ/モデルを設定します。",
"form": {
"windowHours": "統計ウィンドウ(時間)",
- "windowHoursDesc": "TFFT と可用率の算出に使う統計期間であり、チャートがカバーする総期間でもあります。既定値は 24 時間です。",
+ "windowHoursDesc": "TTFT と可用率の算出に使う統計期間であり、チャートがカバーする総期間でもあります。既定値は 24 時間です。",
"aggregationIntervalMinutes": "チャートのバケット(分)",
"aggregationIntervalMinutesDesc": "チャート時間軸の各バケットの長さで、チャートの粒度を決定します。5 / 15 / 30 / 60 分から選択できます。",
"aggregationIntervalMinutesInvalid": "公開ステータスの集計間隔は 5、15、30、60 分のいずれかである必要があります。",
@@ -46,7 +46,7 @@
"heroPrimary": "AI SERVICES",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "更新",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"history": "履歴",
"freshnessWindow": "スナップショット有効期限",
@@ -76,7 +76,7 @@
},
"tooltip": {
"availability": "可用率",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"samples": "サンプル数",
"inferredFromNeighbors": "この期間はリクエストがないため、隣接データから推定",
diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json
index 0e0d09cb4..d4e3ea0d9 100644
--- a/messages/ru/dashboard.json
+++ b/messages/ru/dashboard.json
@@ -356,20 +356,20 @@
"performance": {
"title": "Производительность",
"ttfb": "TTFB",
- "tfft": "TFFT",
- "tfftShort": "TFFT",
+ "ttft": "TTFT",
+ "ttftShort": "TTFT",
"duration": "Общее время",
"outputRate": "Скорость вывода",
"outputTokens": "Токены вывода"
},
"performanceTab": {
"noPerformanceData": "Нет данных о производительности",
- "tfftGauge": "Время до первого токена",
+ "ttftGauge": "Время до первого токена",
"outputRateGauge": "Скорость вывода",
"latencyBreakdown": "Разбивка задержки",
"generationTime": "Время генерации",
"segmentTtfb": "TTFB",
- "segmentTfft": "Ожидание токена",
+ "segmentTtft": "Ожидание токена",
"segmentTotal": "Всего",
"assessment": {
"excellent": "Отлично",
@@ -689,7 +689,7 @@
"totalConsumedAmount": "Общие расходы",
"successRate": "Процент успеха",
"avgResponseTime": "Среднее время ответа",
- "avgTtfbMs": "Средний TFFT",
+ "avgTtftMs": "Средний TTFT",
"avgTokensPerSecond": "Средн. ток/с",
"avgCostPerRequest": "Ср. стоимость/запрос",
"avgCostPerMillionTokens": "Ср. стоимость/1М токенов",
diff --git a/messages/ru/settings/statusPage.json b/messages/ru/settings/statusPage.json
index 640c0984d..3758f02fd 100644
--- a/messages/ru/settings/statusPage.json
+++ b/messages/ru/settings/statusPage.json
@@ -3,7 +3,7 @@
"description": "Настройте окно статистики, размер бакета графика и группы/модели, которые будут показаны публично.",
"form": {
"windowHours": "Окно статистики (часы)",
- "windowHoursDesc": "Длина окна для расчёта TFFT и доступности, а также общий период, покрываемый графиком. По умолчанию: 24 часа.",
+ "windowHoursDesc": "Длина окна для расчёта TTFT и доступности, а также общий период, покрываемый графиком. По умолчанию: 24 часа.",
"aggregationIntervalMinutes": "Бакет графика (минуты)",
"aggregationIntervalMinutesDesc": "Длина каждого бакета на оси времени графика; определяет детализацию графика. Допустимые значения: 5 / 15 / 30 / 60 минут.",
"aggregationIntervalMinutesInvalid": "Интервал агрегации публичного статуса должен быть одним из 5, 15, 30 или 60 минут.",
@@ -46,7 +46,7 @@
"heroPrimary": "AI SERVICES",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "Обновлено",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"history": "История",
"freshnessWindow": "Свежесть снимка",
@@ -76,7 +76,7 @@
},
"tooltip": {
"availability": "Доступность",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"samples": "Выборки",
"inferredFromNeighbors": "Запросов нет — состояние выведено из соседних интервалов",
diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json
index 6c78bba6f..bb26357b4 100644
--- a/messages/zh-CN/dashboard.json
+++ b/messages/zh-CN/dashboard.json
@@ -356,20 +356,20 @@
"performance": {
"title": "性能数据",
"ttfb": "首字节时间(TTFB)",
- "tfft": "首 Token 时间(TFFT)",
- "tfftShort": "TFFT",
+ "ttft": "首 Token 时间(TTFT)",
+ "ttftShort": "TTFT",
"duration": "总耗时",
"outputRate": "输出速率",
"outputTokens": "输出 Tokens"
},
"performanceTab": {
"noPerformanceData": "暂无性能数据",
- "tfftGauge": "首 Token 时间",
+ "ttftGauge": "首 Token 时间",
"outputRateGauge": "输出速率",
"latencyBreakdown": "延迟分解",
"generationTime": "生成时间",
"segmentTtfb": "TTFB",
- "segmentTfft": "等待首 Token",
+ "segmentTtft": "等待首 Token",
"segmentTotal": "总计",
"assessment": {
"excellent": "优秀",
@@ -689,7 +689,7 @@
"totalConsumedAmount": "总消耗金额",
"successRate": "成功率",
"avgResponseTime": "平均响应时间",
- "avgTtfbMs": "平均 TFFT",
+ "avgTtftMs": "平均 TTFT",
"avgTokensPerSecond": "平均输出速率",
"avgCostPerRequest": "平均单次请求成本",
"avgCostPerMillionTokens": "平均百万 Token 成本",
diff --git a/messages/zh-CN/settings/statusPage.json b/messages/zh-CN/settings/statusPage.json
index 992c4f387..2e4ec8911 100644
--- a/messages/zh-CN/settings/statusPage.json
+++ b/messages/zh-CN/settings/statusPage.json
@@ -3,7 +3,7 @@
"description": "配置公开状态页面的统计窗口、图表分桶,以及需要对外展示的分组和模型。",
"form": {
"windowHours": "统计窗口(小时)",
- "windowHoursDesc": "用于计算 TFFT 与在线率的统计窗口长度,也是图表覆盖的总时间跨度。默认 24 小时。",
+ "windowHoursDesc": "用于计算 TTFT 与在线率的统计窗口长度,也是图表覆盖的总时间跨度。默认 24 小时。",
"aggregationIntervalMinutes": "图表分桶(分钟)",
"aggregationIntervalMinutesDesc": "图表时间线每个分桶的时长,决定图表粒度。可选 5 / 15 / 30 / 60 分钟。",
"aggregationIntervalMinutesInvalid": "公开状态聚合间隔只能是 5、15、30、60 分钟之一。",
@@ -46,7 +46,7 @@
"heroPrimary": "AI 服务",
"heroSecondary": "服务状态面板",
"generatedAt": "更新于",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"history": "历史",
"freshnessWindow": "快照新鲜期剩余",
@@ -76,7 +76,7 @@
},
"tooltip": {
"availability": "可用率",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"samples": "样本数",
"inferredFromNeighbors": "该时段无请求,根据相邻时段状态推断",
diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json
index 52e5efda8..0ac264d7f 100644
--- a/messages/zh-TW/dashboard.json
+++ b/messages/zh-TW/dashboard.json
@@ -356,20 +356,20 @@
"performance": {
"title": "效能資料",
"ttfb": "首字節時間(TTFB)",
- "tfft": "首 Token 時間(TFFT)",
- "tfftShort": "TFFT",
+ "ttft": "首 Token 時間(TTFT)",
+ "ttftShort": "TTFT",
"duration": "總耗時",
"outputRate": "輸出速率",
"outputTokens": "輸出 Tokens"
},
"performanceTab": {
"noPerformanceData": "暫無效能資料",
- "tfftGauge": "首 Token 時間",
+ "ttftGauge": "首 Token 時間",
"outputRateGauge": "輸出速率",
"latencyBreakdown": "延遲分解",
"generationTime": "生成時間",
"segmentTtfb": "TTFB",
- "segmentTfft": "等待首 Token",
+ "segmentTtft": "等待首 Token",
"segmentTotal": "總計",
"assessment": {
"excellent": "優秀",
@@ -689,7 +689,7 @@
"totalConsumedAmount": "總消耗金額",
"successRate": "成功率(%)",
"avgResponseTime": "平均回覆時間",
- "avgTtfbMs": "平均 TFFT(ms)",
+ "avgTtftMs": "平均 TTFT(ms)",
"avgTokensPerSecond": "平均輸出速率",
"avgCostPerRequest": "平均每次請求成本",
"avgCostPerMillionTokens": "平均每百萬 Token 成本",
diff --git a/messages/zh-TW/settings/statusPage.json b/messages/zh-TW/settings/statusPage.json
index c6d879e49..8cfccf1f1 100644
--- a/messages/zh-TW/settings/statusPage.json
+++ b/messages/zh-TW/settings/statusPage.json
@@ -3,7 +3,7 @@
"description": "設定公開狀態頁面的統計視窗、圖表分桶,以及需要對外展示的分組和模型。",
"form": {
"windowHours": "統計視窗(小時)",
- "windowHoursDesc": "用於計算 TFFT 與在線率的統計視窗長度,也是圖表覆蓋的總時間跨度。預設 24 小時。",
+ "windowHoursDesc": "用於計算 TTFT 與在線率的統計視窗長度,也是圖表覆蓋的總時間跨度。預設 24 小時。",
"aggregationIntervalMinutes": "圖表分桶(分鐘)",
"aggregationIntervalMinutesDesc": "圖表時間軸每個分桶的時長,決定圖表粒度。可選 5 / 15 / 30 / 60 分鐘。",
"aggregationIntervalMinutesInvalid": "公開狀態聚合間隔只能是 5、15、30、60 分鐘之一。",
@@ -46,7 +46,7 @@
"heroPrimary": "AI 服務",
"heroSecondary": "服務狀態面板",
"generatedAt": "更新於",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"history": "歷史",
"freshnessWindow": "快照新鮮期剩餘",
@@ -76,7 +76,7 @@
},
"tooltip": {
"availability": "可用率",
- "ttfb": "TFFT",
+ "ttft": "TTFT",
"tps": "TPS",
"samples": "樣本數",
"inferredFromNeighbors": "該時段無請求,依相鄰時段狀態推斷",
diff --git a/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx b/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
index 7183ff0f9..236c232b9 100644
--- a/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
+++ b/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
@@ -340,14 +340,14 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
getValue: (row) => row.successRate,
},
{
- header: t("columns.avgTtfbMs"),
+ header: t("columns.avgTtftMs"),
className: "text-right",
cell: (row) => {
- const val = row.avgTtfbMs;
+ const val = row.avgTtftMs;
return val && val > 0 ? `${Math.round(val).toLocaleString()} ms` : "-";
},
- sortKey: "avgTtfbMs",
- getValue: (row) => row.avgTtfbMs ?? 0,
+ sortKey: "avgTtftMs",
+ getValue: (row) => row.avgTtftMs ?? 0,
},
{
header: t("columns.avgTokensPerSecond"),
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx
index 0410bd2bd..10e9edddb 100644
--- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx
@@ -264,19 +264,20 @@ const messages = {
performance: {
title: "Performance",
ttfb: "TTFB",
- tfft: "TFFT",
+ ttft: "TTFT",
+ ttftShort: "TTFT",
duration: "Duration",
outputRate: "Output rate",
outputTokens: "Output Tokens",
},
performanceTab: {
noPerformanceData: "No performance data",
- tfftGauge: "Time to First Token",
+ ttftGauge: "Time to First Token",
outputRateGauge: "Output Rate",
latencyBreakdown: "Latency Breakdown",
generationTime: "Generation Time",
segmentTtfb: "TTFB",
- segmentTfft: "Token Wait",
+ segmentTtft: "Token Wait",
segmentTotal: "Total",
assessment: {
excellent: "Excellent",
@@ -600,7 +601,7 @@ describe("error-details-dialog layout", () => {
inputTokens={100}
outputTokens={80}
durationMs={900}
- tfftMs={100}
+ ttftMs={100}
firstByteMs={100}
/>
);
@@ -621,7 +622,7 @@ describe("error-details-dialog layout", () => {
inputTokens={100}
outputTokens={0}
durationMs={null}
- tfftMs={null}
+ ttftMs={null}
firstByteMs={null}
/>
);
@@ -642,7 +643,7 @@ describe("error-details-dialog layout", () => {
inputTokens={null}
outputTokens={80}
durationMs={900}
- tfftMs={100}
+ ttftMs={100}
firstByteMs={100}
/>
);
@@ -666,7 +667,7 @@ describe("error-details-dialog layout", () => {
inputTokens={null}
outputTokens={300}
durationMs={1000}
- tfftMs={950}
+ ttftMs={950}
firstByteMs={950}
/>
);
@@ -692,7 +693,7 @@ describe("error-details-dialog layout", () => {
inputTokens={null}
outputTokens={50}
durationMs={1000}
- tfftMs={500}
+ ttftMs={500}
firstByteMs={500}
/>
);
@@ -1273,7 +1274,7 @@ describe("error-details-dialog tabs", () => {
providerChain={null}
sessionId={null}
durationMs={1000}
- tfftMs={200}
+ ttftMs={200}
firstByteMs={200}
outputTokens={500}
/>
@@ -1388,7 +1389,7 @@ describe("error-details-dialog routing trace", () => {
outcome: "success",
statusCode: 200,
durationMs: 12_000,
- ttfbMs: 3_000,
+ ttftMs: 3_000,
attemptsPerRequest: 3,
maxActiveAttempts: 2,
rounds: 1,
@@ -1488,7 +1489,7 @@ describe("error-details-dialog routing trace", () => {
summary: {
...discoveryTrace.summary!,
durationMs: 20_000,
- ttfbMs: 19_000,
+ ttftMs: 19_000,
attemptsPerRequest: 1,
rounds: 1,
fallbackPromotions: 1,
@@ -1880,7 +1881,7 @@ describe("error-details-dialog routing trace", () => {
outcome: "success",
statusCode: 200,
durationMs: 7_000,
- ttfbMs: 1_500,
+ ttftMs: 1_500,
attemptsPerRequest: 4,
maxActiveAttempts: 4,
rounds: 1,
@@ -2022,7 +2023,7 @@ describe("error-details-dialog routing trace", () => {
outcome: "success",
statusCode: 200,
durationMs: 2_000,
- ttfbMs: 1_100,
+ ttftMs: 1_100,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx
index 48e56939a..9239526fd 100644
--- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx
@@ -7,7 +7,7 @@ interface LatencyBreakdownBarProps {
/** Time to first byte in milliseconds (null on rows persisted before it was recorded) */
firstByteMs: number | null;
/** Time to first token in milliseconds */
- tfftMs: number | null;
+ ttftMs: number | null;
/** Total duration in milliseconds */
durationMs: number | null;
/** Optional className */
@@ -25,7 +25,7 @@ function formatMs(ms: number): string {
export function LatencyBreakdownBar({
firstByteMs,
- tfftMs,
+ ttftMs,
durationMs,
className,
showLabels = true,
@@ -34,20 +34,20 @@ export function LatencyBreakdownBar({
// Handle null/invalid values
if (
- tfftMs === null ||
+ ttftMs === null ||
durationMs === null ||
- tfftMs < 0 ||
+ ttftMs < 0 ||
durationMs <= 0 ||
- tfftMs > durationMs
+ ttftMs > durationMs
) {
return null;
}
- // 历史行没有真 TTFB:首段退化为整个 TFFT,中间段消失
+ // 历史行没有真 TTFB:首段退化为整个 TTFT,中间段消失
const ttfbMs =
- firstByteMs !== null && firstByteMs >= 0 && firstByteMs <= tfftMs ? firstByteMs : tfftMs;
- const tokenWaitMs = tfftMs - ttfbMs;
- const generationMs = durationMs - tfftMs;
+ firstByteMs !== null && firstByteMs >= 0 && firstByteMs <= ttftMs ? firstByteMs : ttftMs;
+ const tokenWaitMs = ttftMs - ttfbMs;
+ const generationMs = durationMs - ttftMs;
const percent = (ms: number) => (ms / durationMs) * 100;
// Minimum width for visibility (3%)
@@ -65,7 +65,7 @@ export function LatencyBreakdownBar({
{
key: "tokenWait",
ms: tokenWaitMs,
- label: t("segmentTfft"),
+ label: t("segmentTtft"),
barClass: "bg-violet-500",
dotClass: "bg-violet-500",
},
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx
index b7d73813f..148ff6168 100644
--- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx
@@ -10,31 +10,31 @@ import type { PerformanceTabProps } from "../types";
import { LatencyBreakdownBar } from "./LatencyBreakdownBar";
/**
- * Get TFFT performance assessment
+ * Get TTFT performance assessment
* Thresholds: <1s excellent, <2s good, <3s warning, >=3s poor
*/
-function getTfftAssessment(tfftMs: number | null): {
+function getTtftAssessment(ttftMs: number | null): {
label: string;
color: string;
bgColor: string;
} | null {
- if (tfftMs === null) return null;
+ if (ttftMs === null) return null;
- if (tfftMs < 1000) {
+ if (ttftMs < 1000) {
return {
label: "excellent",
color: "text-emerald-600",
bgColor: "bg-emerald-50 dark:bg-emerald-950/20",
};
}
- if (tfftMs < 2000) {
+ if (ttftMs < 2000) {
return {
label: "good",
color: "text-blue-600",
bgColor: "bg-blue-50 dark:bg-blue-950/20",
};
}
- if (tfftMs < 3000) {
+ if (ttftMs < 3000) {
return {
label: "warning",
color: "text-amber-600",
@@ -88,7 +88,7 @@ function getOutputRateAssessment(rate: number | null): {
export function PerformanceTab({
durationMs,
- tfftMs,
+ ttftMs,
firstByteMs,
outputTokens,
}: PerformanceTabProps) {
@@ -96,7 +96,7 @@ export function PerformanceTab({
// Normalize undefined to null for consistent handling
const normalizedDurationMs = durationMs ?? null;
- const normalizedTfftMs = tfftMs ?? null;
+ const normalizedTtftMs = ttftMs ?? null;
const normalizedFirstByteMs = firstByteMs ?? null;
const normalizedOutputTokens = outputTokens ?? null;
@@ -107,16 +107,16 @@ export function PerformanceTab({
);
const hideRate = shouldHideOutputRate(outputRate, normalizedDurationMs, normalizedFirstByteMs);
const generationMs =
- normalizedDurationMs !== null && normalizedTfftMs !== null
- ? normalizedDurationMs - normalizedTfftMs
+ normalizedDurationMs !== null && normalizedTtftMs !== null
+ ? normalizedDurationMs - normalizedTtftMs
: null;
- const tfftAssessment = getTfftAssessment(normalizedTfftMs);
+ const ttftAssessment = getTtftAssessment(normalizedTtftMs);
const rateAssessment = getOutputRateAssessment(outputRate);
const hasData =
normalizedDurationMs !== null ||
- normalizedTfftMs !== null ||
+ normalizedTtftMs !== null ||
normalizedFirstByteMs !== null ||
(outputRate !== null && !hideRate) ||
normalizedOutputTokens !== null;
@@ -134,17 +134,17 @@ export function PerformanceTab({
{/* Gauges Row */}
- {/* TFFT Gauge */}
- {normalizedTfftMs !== null && (
+ {/* TTFT Gauge */}
+ {normalizedTtftMs !== null && (
-
{t("performanceTab.tfftGauge")}
+
{t("performanceTab.ttftGauge")}
- {normalizedTfftMs >= 1000
- ? `${(normalizedTfftMs / 1000).toFixed(2)}s`
- : `${Math.round(normalizedTfftMs)}ms`}
+ {normalizedTtftMs >= 1000
+ ? `${(normalizedTtftMs / 1000).toFixed(2)}s`
+ : `${Math.round(normalizedTtftMs)}ms`}
- {tfftAssessment && (
-
- {t(`performanceTab.assessment.${tfftAssessment.label}`)}
+ {ttftAssessment && (
+
+ {t(`performanceTab.assessment.${ttftAssessment.label}`)}
)}
@@ -206,7 +206,7 @@ export function PerformanceTab({
{/* Latency Breakdown Bar */}
- {normalizedTfftMs !== null && normalizedDurationMs !== null && (
+ {normalizedTtftMs !== null && normalizedDurationMs !== null && (
@@ -215,7 +215,7 @@ export function PerformanceTab({
@@ -236,13 +236,13 @@ export function PerformanceTab({
)}
- {normalizedTfftMs !== null && (
+ {normalizedTtftMs !== null && (
- {t("performance.tfft")}
+ {t("performance.ttft")}
- {normalizedTfftMs >= 1000
- ? `${(normalizedTfftMs / 1000).toFixed(2)}s`
- : `${Math.round(normalizedTfftMs)}ms`}
+ {normalizedTtftMs >= 1000
+ ? `${(normalizedTtftMs / 1000).toFixed(2)}s`
+ : `${Math.round(normalizedTtftMs)}ms`}
)}
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx
index 718a787a5..cde88e871 100644
--- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx
@@ -52,7 +52,7 @@ interface ErrorDetailsDialogProps {
hedgeLosers?: HedgeLoserBilling[] | null;
context1mApplied?: boolean | null;
durationMs?: number | null;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
externalOpen?: boolean;
onExternalOpenChange?: (open: boolean) => void;
@@ -101,7 +101,7 @@ export function ErrorDetailsDialog({
hedgeLosers,
context1mApplied,
durationMs,
- tfftMs,
+ ttftMs,
firstByteMs,
externalOpen,
onExternalOpenChange,
@@ -255,7 +255,7 @@ export function ErrorDetailsDialog({
hedgeLosers,
context1mApplied,
durationMs,
- tfftMs,
+ ttftMs,
firstByteMs,
};
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts
index 97fcf4e37..fda32610c 100644
--- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts
+++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts
@@ -79,7 +79,7 @@ export interface TabSharedProps {
/** Total request duration in ms */
durationMs?: number | null;
/** Time to first token in ms */
- tfftMs?: number | null;
+ ttftMs?: number | null;
/** Time to first byte in ms (null on rows persisted before it was recorded) */
firstByteMs?: number | null;
}
diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
index 8644b2ef9..2fe44776b 100644
--- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
@@ -507,7 +507,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "success",
statusCode: 200,
durationMs: 2_000,
- ttfbMs: 1_500,
+ ttftMs: 1_500,
attemptsPerRequest: 4,
maxActiveAttempts: 2,
rounds: 2,
@@ -592,7 +592,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "success",
statusCode: 200,
durationMs: 2_000,
- ttfbMs: 1_000,
+ ttftMs: 1_000,
attemptsPerRequest: 1,
maxActiveAttempts: 1,
rounds: 0,
@@ -665,7 +665,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "success",
statusCode: 200,
durationMs: 3_000,
- ttfbMs: 2_000,
+ ttftMs: 2_000,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
@@ -734,7 +734,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "failed",
statusCode: 503,
durationMs: 2_000,
- ttfbMs: null,
+ ttftMs: null,
attemptsPerRequest: 1,
maxActiveAttempts: 1,
rounds: 0,
@@ -813,7 +813,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "success",
statusCode: 200,
durationMs: 2_000,
- ttfbMs: 1_000,
+ ttftMs: 1_000,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
@@ -1015,7 +1015,7 @@ describe("provider-chain-popover Discovery summary", () => {
outcome: "success",
statusCode: 200,
durationMs: 1_000,
- ttfbMs: 1_000,
+ ttftMs: 1_000,
attemptsPerRequest: 1,
maxActiveAttempts: 1,
rounds: 1,
diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
index dfa68d367..d1a7117a8 100644
--- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
@@ -93,7 +93,7 @@ function makeLog(overrides: Partial
): UsageLogRow {
costBreakdown: null,
hedgeLosers: null,
durationMs: 100,
- tfftMs: 50,
+ ttftMs: 50,
firstByteMs: 50,
errorMessage: null,
providerChain: null,
@@ -454,7 +454,7 @@ describe("usage-logs-table multiplier badge", () => {
const html = renderToStaticMarkup(
{
// tok/s should NOT appear
expect(html).not.toContain("tok/s");
- // TFFT 行仍应出现
- expect(html).toContain("logs.details.performance.tfft");
+ // TTFT 行仍应出现
+ expect(html).toContain("logs.details.performance.ttft");
});
test("shows tok/s when conditions are normal", () => {
@@ -476,7 +476,7 @@ describe("usage-logs-table multiplier badge", () => {
const html = renderToStaticMarkup(
{
// tok/s should appear
expect(html).toContain("tok/s");
- // TFFT 行同样应出现
- expect(html).toContain("logs.details.performance.tfft");
+ // TTFT 行同样应出现
+ expect(html).toContain("logs.details.performance.ttft");
});
test("renders swap indicator on cacheTtl badge when swapCacheTtlApplied is true", () => {
diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
index 072cfa481..535530b1e 100644
--- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
@@ -592,9 +592,9 @@ export function UsageLogsTable({
log.firstByteMs
);
const secondLine = [
- log.tfftMs != null &&
- log.tfftMs > 0 &&
- `${t("logs.details.performance.tfft")} ${formatDuration(log.tfftMs)}`,
+ log.ttftMs != null &&
+ log.ttftMs > 0 &&
+ `${t("logs.details.performance.ttft")} ${formatDuration(log.ttftMs)}`,
rate !== null && !hideRate && `${rate.toFixed(0)} tok/s`,
]
.filter(Boolean)
@@ -618,10 +618,10 @@ export function UsageLogsTable({
{t("logs.details.performance.duration")}:{" "}
{formatDuration(log.durationMs)}
- {log.tfftMs != null && (
+ {log.ttftMs != null && (
- {t("logs.details.performance.tfft")}:{" "}
- {formatDuration(log.tfftMs)}
+ {t("logs.details.performance.ttft")}:{" "}
+ {formatDuration(log.ttftMs)}
)}
{log.firstByteMs != null && (
@@ -679,7 +679,7 @@ export function UsageLogsTable({
hedgeLosers={log.hedgeLosers}
context1mApplied={log.context1mApplied}
durationMs={log.durationMs}
- tfftMs={log.tfftMs}
+ ttftMs={log.ttftMs}
firstByteMs={log.firstByteMs}
externalOpen={dialogState.logId === log.id ? true : undefined}
onExternalOpenChange={(open) => {
diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
index b7a6bd093..b2459e1b6 100644
--- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
@@ -157,7 +157,7 @@ function makeLog(overrides: Partial
): UsageLogRow {
costBreakdown: null,
hedgeLosers: null,
durationMs: 100,
- tfftMs: 50,
+ ttftMs: 50,
firstByteMs: 50,
errorMessage: null,
providerChain: null,
@@ -558,7 +558,7 @@ describe("virtualized-logs-table multiplier badge", () => {
// durationMs=1000, firstByteMs=950 => generationTimeMs=50, ratio=0.05 < 0.1
// outputTokens=300 => rate = 300 / 0.05 = 6000 > 5000 => should hide
mockLogs = [
- makeLog({ id: 1, durationMs: 1000, tfftMs: 950, firstByteMs: 950, outputTokens: 300 }),
+ makeLog({ id: 1, durationMs: 1000, ttftMs: 950, firstByteMs: 950, outputTokens: 300 }),
];
const html = renderToStaticMarkup(
@@ -566,8 +566,8 @@ describe("virtualized-logs-table multiplier badge", () => {
// tok/s should NOT appear
expect(html).not.toContain("tok/s");
- // TFFT 行仍应出现
- expect(html).toContain("logs.details.performance.tfft");
+ // TTFT 行仍应出现
+ expect(html).toContain("logs.details.performance.ttft");
});
test("shows tok/s when conditions are normal", () => {
@@ -580,7 +580,7 @@ describe("virtualized-logs-table multiplier badge", () => {
// durationMs=1000, firstByteMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1
// outputTokens=50 => rate = 50 / 0.5 = 100 <= 5000 => should show
mockLogs = [
- makeLog({ id: 1, durationMs: 1000, tfftMs: 500, firstByteMs: 500, outputTokens: 50 }),
+ makeLog({ id: 1, durationMs: 1000, ttftMs: 500, firstByteMs: 500, outputTokens: 50 }),
];
const html = renderToStaticMarkup(
@@ -588,28 +588,28 @@ describe("virtualized-logs-table multiplier badge", () => {
// tok/s should appear
expect(html).toContain("tok/s");
- // TFFT 行同样应出现
- expect(html).toContain("logs.details.performance.tfft");
+ // TTFT 行同样应出现
+ expect(html).toContain("logs.details.performance.ttft");
});
- test("性能列使用 TFFT 缩写", () => {
+ test("性能列使用 TTFT 缩写", () => {
const { trigger } = renderPerformanceWithLog({
durationMs: 1000,
- tfftMs: 500,
+ ttftMs: 500,
firstByteMs: 250,
});
- expect(trigger.textContent).toContain("logs.details.performance.tfftShort");
+ expect(trigger.textContent).toContain("logs.details.performance.ttftShort");
});
- test("性能 Tooltip 保留 TFFT 和 TTFB 完整术语", () => {
+ test("性能 Tooltip 保留 TTFT 和 TTFB 完整术语", () => {
const { tooltip } = renderPerformanceWithLog({
durationMs: 1000,
- tfftMs: 500,
+ ttftMs: 500,
firstByteMs: 250,
});
- expect(tooltip.textContent).toContain("logs.details.performance.tfft");
+ expect(tooltip.textContent).toContain("logs.details.performance.ttft");
expect(tooltip.textContent).toContain("logs.details.performance.ttfb");
});
diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
index 2502e7b77..6565ad4e7 100644
--- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
@@ -1170,9 +1170,9 @@ export function VirtualizedLogsTable({
log.durationMs,
log.firstByteMs
);
- const tfftLine =
- log.tfftMs != null && log.tfftMs > 0
- ? `${t("logs.details.performance.tfftShort")} ${formatDuration(log.tfftMs)}`
+ const ttftLine =
+ log.ttftMs != null && log.ttftMs > 0
+ ? `${t("logs.details.performance.ttftShort")} ${formatDuration(log.ttftMs)}`
: null;
const rateLine =
rate !== null && !hideRate ? `${rate.toFixed(0)} tok/s` : null;
@@ -1183,9 +1183,9 @@ export function VirtualizedLogsTable({
{formatDuration(log.durationMs)}
- {tfftLine && (
+ {ttftLine && (
- {tfftLine}
+ {ttftLine}
)}
{rateLine && (
@@ -1200,10 +1200,10 @@ export function VirtualizedLogsTable({
{t("logs.details.performance.duration")}:{" "}
{formatDuration(log.durationMs)}
- {log.tfftMs != null && (
+ {log.ttftMs != null && (
- {t("logs.details.performance.tfft")}:{" "}
- {formatDuration(log.tfftMs)}
+ {t("logs.details.performance.ttft")}:{" "}
+ {formatDuration(log.ttftMs)}
)}
{log.firstByteMs != null && (
@@ -1267,7 +1267,7 @@ export function VirtualizedLogsTable({
hedgeLosers={log.hedgeLosers}
context1mApplied={log.context1mApplied}
durationMs={log.durationMs}
- tfftMs={log.tfftMs}
+ ttftMs={log.ttftMs}
firstByteMs={log.firstByteMs}
externalOpen={dialogState.logId === log.id ? true : undefined}
onExternalOpenChange={(open) => {
diff --git a/src/app/[locale]/status/[slug]/page.tsx b/src/app/[locale]/status/[slug]/page.tsx
index 2190592df..f19a41c4b 100644
--- a/src/app/[locale]/status/[slug]/page.tsx
+++ b/src/app/[locale]/status/[slug]/page.tsx
@@ -69,7 +69,7 @@ export default async function PublicStatusGroupPage({
generatedAt: t("statusPage.public.generatedAt"),
history: t("statusPage.public.history"),
availability: t("statusPage.public.availability"),
- ttfb: t("statusPage.public.ttfb"),
+ ttft: t("statusPage.public.ttft"),
freshnessWindow: t("statusPage.public.freshnessWindow"),
fresh: t("statusPage.public.fresh"),
stale: t("statusPage.public.stale"),
@@ -92,7 +92,7 @@ export default async function PublicStatusGroupPage({
},
tooltip: {
availability: t("statusPage.public.tooltip.availability"),
- ttfb: t("statusPage.public.tooltip.ttfb"),
+ ttft: t("statusPage.public.tooltip.ttft"),
tps: t("statusPage.public.tooltip.tps"),
historyAriaLabel: t("statusPage.public.tooltip.historyAriaLabel"),
},
diff --git a/src/app/[locale]/status/_components/public-status-timeline.tsx b/src/app/[locale]/status/_components/public-status-timeline.tsx
index d41293847..dc60c553b 100644
--- a/src/app/[locale]/status/_components/public-status-timeline.tsx
+++ b/src/app/[locale]/status/_components/public-status-timeline.tsx
@@ -1,13 +1,13 @@
"use client";
import { useMemo, useState } from "react";
+import { formatTtft } from "@/app/[locale]/status/_lib/format-ttft";
import { cn } from "@/lib/utils";
import type { FilledTimelineCell } from "../_lib/fill-display-timeline";
-import { formatTtfb } from "../_lib/format-ttfb";
export interface PublicStatusTimelineLabels {
availability: string;
- ttfb: string;
+ ttft: string;
tps: string;
noData: string;
historyAriaLabel: string;
@@ -89,7 +89,7 @@ export function PublicStatusTimeline({
: formatRange(activeBucket.bucketStart, activeBucket.bucketEnd, locale, timeZone),
availability:
activeBucket.availabilityPct === null ? "—" : `${activeBucket.availabilityPct.toFixed(2)}%`,
- ttfb: formatTtfb(activeBucket.ttfbMs),
+ ttft: formatTtft(activeBucket.ttftMs),
tps: activeBucket.tps === null ? "—" : activeBucket.tps.toFixed(1),
};
}, [activeBucket, activeIsPlaceholder, locale, timeZone]);
@@ -138,7 +138,7 @@ export function PublicStatusTimeline({
{activeSummary.availability}
- {labels.ttfb} {activeSummary.ttfb}
+ {labels.ttft} {activeSummary.ttft}
{labels.tps} {activeSummary.tps}
diff --git a/src/app/[locale]/status/_components/public-status-view.tsx b/src/app/[locale]/status/_components/public-status-view.tsx
index 36be96a78..52414891c 100644
--- a/src/app/[locale]/status/_components/public-status-view.tsx
+++ b/src/app/[locale]/status/_components/public-status-view.tsx
@@ -17,6 +17,7 @@ import {
} from "@dnd-kit/sortable";
import { Activity } from "lucide-react";
import { startTransition, useEffect, useMemo, useState } from "react";
+import { formatTtft } from "@/app/[locale]/status/_lib/format-ttft";
import { Badge } from "@/components/ui/badge";
import type { PublicStatusPayload } from "@/lib/public-status/payload";
import {
@@ -28,7 +29,6 @@ import { getPublicStatusVendorIconComponent } from "@/lib/public-status/vendor-i
import { cn } from "@/lib/utils";
import { type DisplayState, deriveLatestModelState } from "../_lib/derive-display-state";
import { fillDisplayTimeline } from "../_lib/fill-display-timeline";
-import { formatTtfb } from "../_lib/format-ttfb";
import {
clearGroupOrder,
loadCollapsedSet,
@@ -39,7 +39,7 @@ import {
} from "../_lib/group-order-store";
import {
CHART_BUCKETS,
- computeAvgTtfb,
+ computeAvgTtft,
computeUptimePct,
sliceTimelineForChart,
} from "../_lib/timeline-windows";
@@ -70,7 +70,7 @@ interface PublicStatusViewProps {
generatedAt: string;
history: string;
availability: string;
- ttfb: string;
+ ttft: string;
freshnessWindow: string;
fresh: string;
stale: string;
@@ -88,7 +88,7 @@ interface PublicStatusViewProps {
};
tooltip: {
availability: string;
- ttfb: string;
+ ttft: string;
tps: string;
historyAriaLabel: string;
};
@@ -272,9 +272,9 @@ export function PublicStatusView({
useServerSummary && model.availabilityPct !== null
? model.availabilityPct
: computeUptimePct(model.timeline);
- const ttfb24h = computeAvgTtfb(model.timeline);
+ const ttft24h = computeAvgTtft(model.timeline);
const latest = deriveCurrentModelState(viewModel);
- return { model, chartCells, uptime24h, ttfb24h, latest };
+ return { model, chartCells, uptime24h, ttft24h, latest };
});
const issueCount = derivedModels.filter((d) => d.latest === "failed").length;
const groupState = aggregateByFailed(derivedModels.map((d) => d.latest));
@@ -362,7 +362,7 @@ export function PublicStatusView({
const timelineLabels: PublicStatusTimelineLabels = {
availability: labels.tooltip.availability,
- ttfb: labels.tooltip.ttfb,
+ ttft: labels.tooltip.ttft,
tps: labels.tooltip.tps,
noData: labels.noData,
historyAriaLabel: labels.tooltip.historyAriaLabel,
@@ -459,7 +459,7 @@ export function PublicStatusView({
>
{entry.derivedModels.map(
- ({ model, chartCells, uptime24h, ttfb24h, latest }) => {
+ ({ model, chartCells, uptime24h, ttft24h, latest }) => {
const variant = badgeVariant(latest);
const { Icon } = getPublicStatusVendorIconComponent({
modelName: model.publicModelKey,
@@ -511,13 +511,13 @@ export function PublicStatusView({
- {labels.ttfb}{" "}
+ {labels.ttft}{" "}
({rangeHours}H)
- {formatTtfb(ttfb24h)}
+ {formatTtft(ttft24h)}
diff --git a/src/app/[locale]/status/_lib/format-ttfb.ts b/src/app/[locale]/status/_lib/format-ttft.ts
similarity index 68%
rename from src/app/[locale]/status/_lib/format-ttfb.ts
rename to src/app/[locale]/status/_lib/format-ttft.ts
index aec03c853..804c8ad10 100644
--- a/src/app/[locale]/status/_lib/format-ttfb.ts
+++ b/src/app/[locale]/status/_lib/format-ttft.ts
@@ -1,4 +1,4 @@
-export function formatTtfb(ms: number | null): string {
+export function formatTtft(ms: number | null): string {
if (ms === null) return "—";
if (ms >= 10000) {
return `${(ms / 1000).toFixed(2)} s`;
diff --git a/src/app/[locale]/status/_lib/timeline-windows.ts b/src/app/[locale]/status/_lib/timeline-windows.ts
index 6e9da7486..6f4cc0a91 100644
--- a/src/app/[locale]/status/_lib/timeline-windows.ts
+++ b/src/app/[locale]/status/_lib/timeline-windows.ts
@@ -24,12 +24,12 @@ export function computeUptimePct(timeline: PublicStatusTimelineBucket[]): number
return Number((weightedSum / sampleTotal).toFixed(2));
}
-export function computeAvgTtfb(timeline: PublicStatusTimelineBucket[]): number | null {
+export function computeAvgTtft(timeline: PublicStatusTimelineBucket[]): number | null {
let weightedSum = 0;
let sampleTotal = 0;
for (const bucket of timeline) {
- if (bucket.sampleCount > 0 && bucket.ttfbMs !== null) {
- weightedSum += bucket.ttfbMs * bucket.sampleCount;
+ if (bucket.sampleCount > 0 && bucket.ttftMs !== null) {
+ weightedSum += bucket.ttftMs * bucket.sampleCount;
sampleTotal += bucket.sampleCount;
}
}
diff --git a/src/app/[locale]/status/page.tsx b/src/app/[locale]/status/page.tsx
index baa0c4f83..35d5f0ec9 100644
--- a/src/app/[locale]/status/page.tsx
+++ b/src/app/[locale]/status/page.tsx
@@ -38,7 +38,7 @@ export default async function PublicStatusPage({
generatedAt: t("generatedAt"),
history: t("history"),
availability: t("availability"),
- ttfb: t("ttfb"),
+ ttft: t("ttft"),
freshnessWindow: t("freshnessWindow"),
fresh: t("fresh"),
stale: t("stale"),
@@ -61,7 +61,7 @@ export default async function PublicStatusPage({
},
tooltip: {
availability: t("tooltip.availability"),
- ttfb: t("tooltip.ttfb"),
+ ttft: t("tooltip.ttft"),
tps: t("tooltip.tps"),
historyAriaLabel: t("tooltip.historyAriaLabel"),
},
diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts
index 1dfc34fcd..9e7dd1670 100644
--- a/src/app/v1/_lib/proxy/forwarder.ts
+++ b/src/app/v1/_lib/proxy/forwarder.ts
@@ -1776,7 +1776,7 @@ export class ProxyForwarder {
if (gateFirstByteAt !== null) {
session.recordFirstByte(gateFirstByteAt);
}
- session.recordTfft();
+ session.recordTtft();
if (gate.commitMarker) {
gateChainAudit = {
@@ -5107,7 +5107,7 @@ export class ProxyForwarder {
session.recordFirstByte(attempt.firstByteAt);
}
if (contentGateCommitted) {
- session.recordTfft();
+ session.recordTtft();
}
if (attempt.thresholdTimer) {
@@ -6139,7 +6139,7 @@ export class ProxyForwarder {
if (attempt.firstByteAt != null) {
session.recordFirstByte(attempt.firstByteAt);
}
- session.recordTfft();
+ session.recordTtft();
if (attempt.session !== session)
ProxyForwarder.syncWinningAttemptSession(session, attempt.session);
@@ -6590,7 +6590,7 @@ export class ProxyForwarder {
}
if (!item.value || item.value.byteLength === 0) continue;
// 首字节时刻先挂在 attempt 上;DiscoveryValidityParser 的 ready 判定同样基于内容,
- // 不在此记录会让 discovery 模式的 TTFB 恒等于 TFFT。
+ // 不在此记录会让 discovery 模式的 TTFB 恒等于 TTFT。
attempt.firstByteAt ??= Date.now();
attempt.chunks.push(item.value);
const validity = attempt.parser.push(item.value);
diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts
index d50581955..31d7d8f40 100644
--- a/src/app/v1/_lib/proxy/response-handler.ts
+++ b/src/app/v1/_lib/proxy/response-handler.ts
@@ -2773,7 +2773,7 @@ export class ProxyResponseHandler {
details: {
statusCode: finalizedStatusCode,
...errorDetails,
- tfftMs: session.tfftMs ?? duration,
+ ttftMs: session.ttftMs ?? duration,
firstByteMs: session.firstByteMs ?? duration,
providerChain: session.getProviderChain(),
routingTrace: session.finalizeRoutingTrace(finalizedStatusCode),
@@ -2971,7 +2971,7 @@ export class ProxyResponseHandler {
const terminalDetails: MessageRequestTerminalDetails = {
statusCode: finalizedStatusCode,
...errorDetails,
- tfftMs: session.tfftMs ?? duration,
+ ttftMs: session.ttftMs ?? duration,
firstByteMs: session.firstByteMs ?? duration,
providerChain: session.getProviderChain(),
routingTrace: session.finalizeRoutingTrace(finalizedStatusCode),
@@ -3318,7 +3318,7 @@ export class ProxyResponseHandler {
statusCode: statusCode,
inputTokens: usageMetrics?.input_tokens,
outputTokens: usageMetrics?.output_tokens,
- tfftMs: session.tfftMs ?? duration,
+ ttftMs: session.ttftMs ?? duration,
firstByteMs: session.firstByteMs ?? duration,
cacheCreationInputTokens: usageMetrics?.cache_creation_input_tokens,
cacheReadInputTokens: usageMetrics?.cache_read_input_tokens,
@@ -3778,7 +3778,7 @@ export class ProxyResponseHandler {
clearIdleTimer();
if (isFirstChunk) {
isFirstChunk = false;
- session.recordTfft();
+ session.recordTtft();
clearResponseTimeoutOnce(value.byteLength);
}
AsyncTaskManager.touch(taskId);
@@ -4741,7 +4741,7 @@ export class ProxyResponseHandler {
durationMs: duration,
inputTokens: usageForCost?.input_tokens,
outputTokens: usageForCost?.output_tokens,
- tfftMs: session.tfftMs,
+ ttftMs: session.ttftMs,
firstByteMs: session.firstByteMs,
cacheCreationInputTokens: usageForCost?.cache_creation_input_tokens,
cacheReadInputTokens: usageForCost?.cache_read_input_tokens,
@@ -4841,7 +4841,7 @@ export class ProxyResponseHandler {
});
if (isFirstChunk) {
- session.recordTfft();
+ session.recordTtft();
isFirstChunk = false;
if (clearResponseTimeoutOnce()) {
logger.debug("ResponseHandler: First chunk received, response timeout cleared", {
@@ -6371,7 +6371,7 @@ export async function finalizeRequestStats(
statusCode: statusCode,
durationMs: duration,
...(errorMessage ? { errorMessage } : {}),
- tfftMs: session.tfftMs ?? duration,
+ ttftMs: session.ttftMs ?? duration,
firstByteMs: session.firstByteMs ?? duration,
providerChain: session.getProviderChain(),
routingTrace: session.finalizeRoutingTrace(statusCode),
@@ -6485,7 +6485,7 @@ export async function finalizeRequestStats(
durationMs: duration,
inputTokens: normalizedUsage.input_tokens,
outputTokens: normalizedUsage.output_tokens,
- tfftMs: session.tfftMs ?? duration,
+ ttftMs: session.ttftMs ?? duration,
firstByteMs: session.firstByteMs ?? duration,
cacheCreationInputTokens: normalizedUsage.cache_creation_input_tokens,
cacheReadInputTokens: normalizedUsage.cache_read_input_tokens,
@@ -6752,7 +6752,7 @@ async function persistRequestFailure(options: {
errorMessage,
errorStack,
errorCause,
- tfftMs: phase === "non-stream" ? (session.tfftMs ?? duration) : session.tfftMs,
+ ttftMs: phase === "non-stream" ? (session.ttftMs ?? duration) : session.ttftMs,
firstByteMs: phase === "non-stream" ? (session.firstByteMs ?? duration) : session.firstByteMs,
providerChain: session.getProviderChain(),
routingTrace: session.finalizeRoutingTrace(statusCode),
diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts
index 1817c6aea..5c681b44b 100644
--- a/src/app/v1/_lib/proxy/session.ts
+++ b/src/app/v1/_lib/proxy/session.ts
@@ -152,10 +152,10 @@ export class ProxySession {
// Time To First Token (ms). Streaming: first chunk handed to the response handler,
// which under an enforcing stream gate is the first *content* frame. Non-stream: equals durationMs.
- tfftMs: number | null = null;
+ ttftMs: number | null = null;
// Time To First Byte (ms). First body byte from the upstream, reported by the stream gate.
- // Equals tfftMs whenever no gate ran (gate off/shadow, raw passthrough, non-SSE).
+ // Equals ttftMs whenever no gate ran (gate off/shadow, raw passthrough, non-SSE).
firstByteMs: number | null = null;
// Timestamp when guard pipeline finished and forwarding started (epoch ms).
@@ -566,22 +566,22 @@ export class ProxySession {
}
/**
- * Record Time To First Token (TFFT) for streaming responses.
+ * Record Time To First Token (TTFT) for streaming responses.
*
* Definition: first body chunk handed to the response handler. With the stream content
- * gate enforcing, that chunk is the first content frame, so this is TFFT, not TTFB.
- * Non-stream responses should persist TFFT as `durationMs` at finalize time.
+ * gate enforcing, that chunk is the first content frame, so this is TTFT, not TTFB.
+ * Non-stream responses should persist TTFT as `durationMs` at finalize time.
*
* Doubles as the TTFB fallback: paths where no gate ran never call `recordFirstByte`,
- * and there TTFB and TFFT are the same moment.
+ * and there TTFB and TTFT are the same moment.
*/
- recordTfft(): number {
- if (this.tfftMs !== null) {
- return this.tfftMs;
+ recordTtft(): number {
+ if (this.ttftMs !== null) {
+ return this.ttftMs;
}
const value = Math.max(0, Date.now() - this.startTime);
- this.tfftMs = value;
+ this.ttftMs = value;
if (this.firstByteMs === null) {
this.firstByteMs = value;
}
@@ -1005,7 +1005,7 @@ export class ProxySession {
outcome: resolvedOutcome,
statusCode,
durationMs: Math.max(0, now - this.routingTrace.startedAt),
- ttfbMs: this.tfftMs,
+ ttftMs: this.ttftMs,
};
}
const terminalEvent = this.routingTrace.events.find(
@@ -1049,7 +1049,7 @@ export class ProxySession {
winnerProviderId: summary.winnerProviderId,
winnerRound: summary.winnerRound,
elapsedMs: summary.durationMs,
- ttfbMs: summary.ttfbMs,
+ ttftMs: summary.ttftMs,
attemptsPerRequest: summary.attemptsPerRequest,
maxActiveAttempts: summary.maxActiveAttempts,
rounds: summary.rounds,
diff --git a/src/app/v1/_lib/proxy/warmup-guard.ts b/src/app/v1/_lib/proxy/warmup-guard.ts
index 997313ca7..654264797 100644
--- a/src/app/v1/_lib/proxy/warmup-guard.ts
+++ b/src/app/v1/_lib/proxy/warmup-guard.ts
@@ -80,7 +80,7 @@ export class ProxyWarmupGuard {
messagesCount: session.getMessagesLength(),
statusCode: 200,
durationMs,
- tfftMs: durationMs,
+ ttftMs: durationMs,
firstByteMs: durationMs,
// 不计费:显式写 NULL,避免前端误显示 “$0”
costUsd: null,
diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts
index 964ca4905..d40b91a9e 100644
--- a/src/drizzle/schema.ts
+++ b/src/drizzle/schema.ts
@@ -590,10 +590,10 @@ export const messageRequest = pgTable('message_request', {
// Token 使用信息
inputTokens: bigint('input_tokens', { mode: 'number' }),
outputTokens: bigint('output_tokens', { mode: 'number' }),
- // 首 Token 时间(TFFT)。列名 ttfb_ms 是历史遗留:流式输出门禁上线后,
- // 这个时间戳打在首个内容帧上,语义已是 TFFT 而非 TTFB。真 TTFB 见 firstByteMs。
- tfftMs: integer('ttfb_ms'),
- // 首字节时间(TTFB):上游响应体第一个字节到达。门禁旁路时等于 tfftMs。
+ // 首 Token 时间(TTFT)。列名 ttfb_ms 是历史遗留:流式输出门禁上线后,
+ // 这个时间戳打在首个内容帧上,语义已是 TTFT 而非 TTFB。真 TTFB 见 firstByteMs。
+ ttftMs: integer('ttfb_ms'),
+ // 首字节时间(TTFB):上游响应体第一个字节到达。门禁旁路时等于 ttftMs。
firstByteMs: integer('first_byte_ms'),
cacheCreationInputTokens: bigint('cache_creation_input_tokens', { mode: 'number' }),
cacheReadInputTokens: bigint('cache_read_input_tokens', { mode: 'number' }),
@@ -1199,8 +1199,8 @@ export const usageLedger = pgTable('usage_ledger', {
context1mApplied: boolean('context_1m_applied').default(false),
swapCacheTtlApplied: boolean('swap_cache_ttl_applied').default(false),
durationMs: integer('duration_ms'),
- // 列名 ttfb_ms 存的是 TFFT,见 messageRequest.tfftMs 的说明
- tfftMs: integer('ttfb_ms'),
+ // 列名 ttfb_ms 存的是 TTFT,见 messageRequest.ttftMs 的说明
+ ttftMs: integer('ttfb_ms'),
firstByteMs: integer('first_byte_ms'),
// 客户端 IP(从 message_request 拷贝;永久保留,避免被清理任务删除)
clientIp: varchar('client_ip', { length: 45 }),
diff --git a/src/lib/langfuse/emit-proxy-trace.ts b/src/lib/langfuse/emit-proxy-trace.ts
index 99d5fb508..48e9d80f0 100644
--- a/src/lib/langfuse/emit-proxy-trace.ts
+++ b/src/lib/langfuse/emit-proxy-trace.ts
@@ -77,7 +77,7 @@ function buildLangfuseSessionSnapshot(session: ProxySession): ProxySession {
userAgent: session.userAgent,
provider: session.provider,
messageContext: session.messageContext,
- tfftMs: session.tfftMs,
+ ttftMs: session.ttftMs,
firstByteMs: session.firstByteMs,
forwardStartTime: session.forwardStartTime,
forwardedRequestBody,
diff --git a/src/lib/langfuse/trace-proxy-request.ts b/src/lib/langfuse/trace-proxy-request.ts
index c69ab3441..3fb1aa237 100644
--- a/src/lib/langfuse/trace-proxy-request.ts
+++ b/src/lib/langfuse/trace-proxy-request.ts
@@ -190,11 +190,11 @@ export async function traceProxyRequest(ctx: TraceContext): Promise {
guardPipelineMs,
upstreamTotalMs:
guardPipelineMs != null ? Math.max(0, durationMs - guardPipelineMs) : durationMs,
- tfftFromForwardMs:
- guardPipelineMs != null && session.tfftMs != null
- ? Math.max(0, session.tfftMs - guardPipelineMs)
+ ttftFromForwardMs:
+ guardPipelineMs != null && session.ttftMs != null
+ ? Math.max(0, session.ttftMs - guardPipelineMs)
: null,
- tokenGenerationMs: session.tfftMs != null ? Math.max(0, durationMs - session.tfftMs) : null,
+ tokenGenerationMs: session.ttftMs != null ? Math.max(0, durationMs - session.ttftMs) : null,
failedAttempts: session.getProviderChain().filter((i) => !isSuccessReason(i.reason)).length,
providersAttempted: new Set(session.getProviderChain().map((i) => i.id)).size,
};
@@ -278,7 +278,7 @@ export async function traceProxyRequest(ctx: TraceContext): Promise {
keyName: messageContext?.key?.name,
// Timing
durationMs,
- tfftMs: session.tfftMs,
+ ttftMs: session.ttftMs,
firstByteMs: session.firstByteMs,
timingBreakdown,
// Flags
@@ -433,10 +433,10 @@ export async function traceProxyRequest(ctx: TraceContext): Promise {
{ asType: "generation", startTime: generationStartTime } as { asType: "generation" }
);
- // Set TTFB as completionStartTime
- if (session.tfftMs != null) {
+ // Set TTFT as completionStartTime
+ if (session.ttftMs != null) {
generation.update({
- completionStartTime: new Date(session.startTime + session.tfftMs),
+ completionStartTime: new Date(session.startTime + session.ttftMs),
});
}
diff --git a/src/lib/observability/discovery-metrics.ts b/src/lib/observability/discovery-metrics.ts
index 0e0706907..102734021 100644
--- a/src/lib/observability/discovery-metrics.ts
+++ b/src/lib/observability/discovery-metrics.ts
@@ -113,7 +113,7 @@ export class DiscoveryRequestMetrics {
outcome: context.outcome,
statusCode: context.statusCode,
durationMs: elapsedMs,
- ttfbMs: context.outcome === "success" ? elapsedMs : null,
+ ttftMs: context.outcome === "success" ? elapsedMs : null,
attemptsPerRequest: this.attempts,
maxActiveAttempts: this.maxActive,
rounds: this.maxRound,
@@ -129,7 +129,7 @@ export class DiscoveryRequestMetrics {
...this.identity,
...context,
elapsedMs,
- ttfbMs: context.outcome === "success" ? elapsedMs : null,
+ ttftMs: context.outcome === "success" ? elapsedMs : null,
attemptsPerRequest: this.attempts,
maxActiveAttempts: this.maxActive,
rounds: this.maxRound,
diff --git a/src/lib/public-status/aggregation-core.ts b/src/lib/public-status/aggregation-core.ts
index e16811863..1fe46f31b 100644
--- a/src/lib/public-status/aggregation-core.ts
+++ b/src/lib/public-status/aggregation-core.ts
@@ -16,7 +16,7 @@ export interface PublicStatusConfiguredGroup {
/**
* TPS = 输出 token / 生成窗口,生成窗口以真 TTFB 为起点。
*
- * firstByteMs 缺失即返回 null:流式门禁上线前的历史行只有 TFFT,用它当分母会排除
+ * firstByteMs 缺失即返回 null:流式门禁上线前的历史行只有 TTFT,用它当分母会排除
* 上游排队/中性帧窗口,系统性高估 TPS。
*/
export function computeTokensPerSecond(input: {
diff --git a/src/lib/public-status/aggregation.ts b/src/lib/public-status/aggregation.ts
index b403e002d..80138a1cc 100644
--- a/src/lib/public-status/aggregation.ts
+++ b/src/lib/public-status/aggregation.ts
@@ -39,7 +39,7 @@ export interface PublicStatusRequestRow {
model?: string | null;
originalModel?: string | null;
durationMs?: number | null;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
outputTokens?: number | null;
providerChain?: PublicStatusRequestChainItem[] | null;
@@ -177,7 +177,7 @@ export function buildPublicStatusPayloadFromRequests(input: {
type MutableBucket = {
successCount: number;
failureCount: number;
- ttfbValues: number[];
+ ttftValues: number[];
tpsValues: number[];
};
@@ -216,7 +216,7 @@ export function buildPublicStatusPayloadFromRequests(input: {
buckets: Array.from({ length: bucketCount }, () => ({
successCount: 0,
failureCount: 0,
- ttfbValues: [],
+ ttftValues: [],
tpsValues: [],
})),
},
@@ -324,9 +324,9 @@ export function buildPublicStatusPayloadFromRequests(input: {
bucket.failureCount += 1;
}
- // ttfbValues -> bucket.ttfbMs 是对外 payload 字段,装的是 TFFT
- if (outcome === "success" && typeof request.tfftMs === "number") {
- bucket.ttfbValues.push(request.tfftMs);
+ // ttftValues -> bucket.ttftMs 是对外 payload 字段,装的是 TTFT
+ if (outcome === "success" && typeof request.ttftMs === "number") {
+ bucket.ttftValues.push(request.ttftMs);
}
if (outcome === "success" && typeof tps === "number") {
bucket.tpsValues.push(tps);
@@ -351,7 +351,7 @@ export function buildPublicStatusPayloadFromRequests(input: {
timeline: rawTimeline,
});
- let latestTtfbMs: number | null = null;
+ let latestTtftMs: number | null = null;
let latestTps: number | null = null;
const timeline: PublicStatusTimelineBucket[] = (modelState?.buckets ?? []).map(
@@ -367,11 +367,11 @@ export function buildPublicStatusPayloadFromRequests(input: {
? 0
: null
: Number(((bucket.successCount / total) * 100).toFixed(2));
- const ttfbMs = median(bucket.ttfbValues);
+ const ttftMs = median(bucket.ttftValues);
const computedTps = median(bucket.tpsValues);
- if (ttfbMs !== null) {
- latestTtfbMs = ttfbMs;
+ if (ttftMs !== null) {
+ latestTtftMs = ttftMs;
}
if (computedTps !== null) {
latestTps = computedTps;
@@ -387,7 +387,7 @@ export function buildPublicStatusPayloadFromRequests(input: {
? "failed"
: "no_data",
availabilityPct,
- ttfbMs,
+ ttftMs,
tps: computedTps,
sampleCount: total,
};
@@ -416,7 +416,7 @@ export function buildPublicStatusPayloadFromRequests(input: {
requestTypeBadge: modelState?.requestTypeBadge ?? model.requestTypeBadge,
latestState,
availabilityPct,
- latestTtfbMs,
+ latestTtftMs,
latestTps,
timeline,
} satisfies PublicStatusPayload["groups"][number]["models"][number];
@@ -457,7 +457,7 @@ export async function queryPublicStatusRequests(input: {
model: messageRequest.model,
originalModel: messageRequest.originalModel,
durationMs: messageRequest.durationMs,
- tfftMs: messageRequest.tfftMs,
+ ttftMs: messageRequest.ttftMs,
firstByteMs: messageRequest.firstByteMs,
outputTokens: messageRequest.outputTokens,
statusCode: messageRequest.statusCode,
@@ -498,7 +498,7 @@ export async function queryPublicStatusRequests(input: {
model: row.model,
originalModel: row.originalModel,
durationMs: row.durationMs,
- tfftMs: row.tfftMs,
+ ttftMs: row.ttftMs,
firstByteMs: row.firstByteMs,
outputTokens: row.outputTokens,
providerChain: existingChain,
diff --git a/src/lib/public-status/openapi.ts b/src/lib/public-status/openapi.ts
index 9b0db65f6..a7da54ba3 100644
--- a/src/lib/public-status/openapi.ts
+++ b/src/lib/public-status/openapi.ts
@@ -21,7 +21,7 @@ const publicStatusTimelineBucketSchema = {
"bucketEnd",
"state",
"availabilityPct",
- "ttfbMs",
+ "ttftMs",
"tps",
"sampleCount",
],
@@ -33,7 +33,7 @@ const publicStatusTimelineBucketSchema = {
enum: [...PUBLIC_STATUS_FILTER_STATUS_VALUES],
},
availabilityPct: { type: ["number", "null"] },
- ttfbMs: { type: ["number", "null"] },
+ ttftMs: { type: ["number", "null"] },
tps: { type: ["number", "null"] },
sampleCount: { type: "number" },
},
@@ -48,7 +48,7 @@ const publicStatusModelSchema = {
"requestTypeBadge",
"latestState",
"availabilityPct",
- "latestTtfbMs",
+ "latestTtftMs",
"latestTps",
"timeline",
],
@@ -62,7 +62,7 @@ const publicStatusModelSchema = {
enum: [...PUBLIC_STATUS_FILTER_STATUS_VALUES],
},
availabilityPct: { type: ["number", "null"] },
- latestTtfbMs: { type: ["number", "null"] },
+ latestTtftMs: { type: ["number", "null"] },
latestTps: { type: ["number", "null"] },
timeline: {
type: "array",
@@ -384,7 +384,7 @@ export function appendPublicStatusOpenApi(
requestTypeBadge: "anthropic",
latestState: "failed",
availabilityPct: 20,
- latestTtfbMs: 900,
+ latestTtftMs: 900,
latestTps: null,
timeline: [],
},
diff --git a/src/lib/public-status/payload.ts b/src/lib/public-status/payload.ts
index b339432c1..1e5ab9f76 100644
--- a/src/lib/public-status/payload.ts
+++ b/src/lib/public-status/payload.ts
@@ -7,7 +7,7 @@ export interface PublicStatusTimelineBucket {
bucketEnd: string;
state: PublicStatusTimelineState;
availabilityPct: number | null;
- ttfbMs: number | null;
+ ttftMs: number | null;
tps: number | null;
sampleCount: number;
}
@@ -19,7 +19,7 @@ export interface PublicStatusModelSnapshot {
requestTypeBadge: string;
latestState: PublicStatusTimelineState;
availabilityPct: number | null;
- latestTtfbMs: number | null;
+ latestTtftMs: number | null;
latestTps: number | null;
timeline: PublicStatusTimelineBucket[];
}
diff --git a/src/lib/public-status/read-store.ts b/src/lib/public-status/read-store.ts
index 4dec7e75d..95eca5c08 100644
--- a/src/lib/public-status/read-store.ts
+++ b/src/lib/public-status/read-store.ts
@@ -123,7 +123,7 @@ function sanitizeTimelineBuckets(input: unknown): PublicStatusTimelineBucket[] {
bucketEnd: value.bucketEnd,
state: normalizeTimelineState(value.state),
availabilityPct: normalizeNullableNumber(value.availabilityPct),
- ttfbMs: normalizeNullableNumber(value.ttfbMs),
+ ttftMs: normalizeNullableNumber(value.ttftMs !== undefined ? value.ttftMs : value.ttfbMs),
tps: normalizeNullableNumber(value.tps),
sampleCount: value.sampleCount,
},
@@ -159,7 +159,9 @@ function sanitizeModelSnapshots(input: unknown): PublicStatusModelSnapshot[] {
requestTypeBadge: value.requestTypeBadge,
latestState: normalizeTimelineState(value.latestState),
availabilityPct: normalizeNullableNumber(value.availabilityPct),
- latestTtfbMs: normalizeNullableNumber(value.latestTtfbMs),
+ latestTtftMs: normalizeNullableNumber(
+ value.latestTtftMs !== undefined ? value.latestTtftMs : value.latestTtfbMs
+ ),
latestTps: normalizeNullableNumber(value.latestTps),
timeline: sanitizeTimelineBuckets(value.timeline),
},
diff --git a/src/lib/public-status/rollup-store.ts b/src/lib/public-status/rollup-store.ts
index 3026afc2c..49133b85f 100644
--- a/src/lib/public-status/rollup-store.ts
+++ b/src/lib/public-status/rollup-store.ts
@@ -39,7 +39,7 @@ export interface PublicStatusRollupEvent {
model?: string | null;
originalModel?: string | null;
durationMs?: number | null;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
outputTokens?: number | null;
providerChain?: ProviderChainItem[] | null;
@@ -340,8 +340,8 @@ export function buildPublicStatusRollupIncrements(input: {
}
}
- // ttfb_sum / ttfb_count 是既有 rollup 键名,存的是 TFFT(改名会作废已积累的桶)
- const tfftMs = normalizeNumber(input.event.tfftMs);
+ // ttfb_sum / ttfb_count 是既有 rollup 键名,存的是 TTFT(改名会作废已积累的桶)
+ const ttftMs = normalizeNumber(input.event.ttftMs);
const tps = computeTokensPerSecond({
outputTokens: input.event.outputTokens,
durationMs: input.event.durationMs,
@@ -369,9 +369,9 @@ export function buildPublicStatusRollupIncrements(input: {
metric: outcome === "success" ? "success" : "failure",
value: 1,
});
- if (outcome === "success" && tfftMs !== null) {
+ if (outcome === "success" && ttftMs !== null) {
increments.push(
- { groupId, modelKey, metric: "ttfb_sum", value: tfftMs },
+ { groupId, modelKey, metric: "ttfb_sum", value: ttftMs },
{ groupId, modelKey, metric: "ttfb_count", value: 1 }
);
}
@@ -713,7 +713,7 @@ export function buildPublicStatusPayloadFromRollups(input: {
});
const filledTimeline = applyBoundedGapFill({ timeline: rawTimeline });
- let latestTtfbMs: number | null = null;
+ let latestTtftMs: number | null = null;
let latestTps: number | null = null;
const timeline: PublicStatusTimelineBucket[] = aggregateBuckets.map((bucket, index) => {
const bucketStartMs = Date.parse(bucket.bucketStart);
@@ -726,11 +726,11 @@ export function buildPublicStatusPayloadFromRollups(input: {
? 0
: null
: Number(((bucket.successCount / total) * 100).toFixed(2));
- const ttfbMs = average(bucket.ttfbSum, bucket.ttfbCount);
+ const ttftMs = average(bucket.ttfbSum, bucket.ttfbCount);
const tps = average(bucket.tpsSum, bucket.tpsCount);
- if (ttfbMs !== null) {
- latestTtfbMs = ttfbMs;
+ if (ttftMs !== null) {
+ latestTtftMs = ttftMs;
}
if (tps !== null) {
latestTps = tps;
@@ -746,7 +746,7 @@ export function buildPublicStatusPayloadFromRollups(input: {
? "failed"
: "no_data",
availabilityPct,
- ttfbMs,
+ ttftMs,
tps,
sampleCount: total,
};
@@ -788,7 +788,7 @@ export function buildPublicStatusPayloadFromRollups(input: {
? "failed"
: "no_data",
availabilityPct,
- latestTtfbMs,
+ latestTtftMs,
latestTps,
timeline,
} satisfies PublicStatusPayload["groups"][number]["models"][number];
diff --git a/src/lib/redis/leaderboard-cache.ts b/src/lib/redis/leaderboard-cache.ts
index b3f6c0280..a4433c68c 100644
--- a/src/lib/redis/leaderboard-cache.ts
+++ b/src/lib/redis/leaderboard-cache.ts
@@ -66,8 +66,9 @@ export interface LeaderboardFilters {
/**
* 缓存值 shape 版本:条目结构变更时递增,避免 60s TTL 内新旧 payload 混用。
* v2: provider / providerCacheHitRate 条目新增 cacheCoefficientBp
+ * v3: leaderboard latency fields use avgTtftMs instead of avgTtfbMs
*/
-const CACHE_SHAPE_VERSION = "v2";
+const CACHE_SHAPE_VERSION = "v3";
/**
* 构建缓存键
@@ -103,22 +104,22 @@ function buildCacheKey(
const prefix = `leaderboard:${CACHE_SHAPE_VERSION}:${scope}`;
if (period === "custom" && dateRange) {
- // leaderboard:v2:{scope}:custom:2025-01-01_2025-01-15:USD
+ // leaderboard:v3:{scope}:custom:2025-01-01_2025-01-15:USD
return `${prefix}:custom:${dateRange.startDate}_${dateRange.endDate}:tz:${timezone}:${currencyDisplay}${providerTypeSuffix}${includeModelStatsSuffix}${userFilterSuffix}`;
} else if (period === "daily") {
- // leaderboard:v2:{scope}:daily:2025-01-15:USD
+ // leaderboard:v3:{scope}:daily:2025-01-15:USD
const dateStr = formatInTimeZone(now, timezone, "yyyy-MM-dd");
return `${prefix}:daily:${dateStr}:tz:${timezone}:${currencyDisplay}${providerTypeSuffix}${includeModelStatsSuffix}${userFilterSuffix}`;
} else if (period === "weekly") {
- // leaderboard:v2:{scope}:weekly:2025-W03:USD (ISO week)
+ // leaderboard:v3:{scope}:weekly:2025-W03:USD (ISO week)
const weekStr = formatInTimeZone(now, timezone, "yyyy-'W'ww");
return `${prefix}:weekly:${weekStr}:tz:${timezone}:${currencyDisplay}${providerTypeSuffix}${includeModelStatsSuffix}${userFilterSuffix}`;
} else if (period === "monthly") {
- // leaderboard:v2:{scope}:monthly:2025-01:USD
+ // leaderboard:v3:{scope}:monthly:2025-01:USD
const monthStr = formatInTimeZone(now, timezone, "yyyy-MM");
return `${prefix}:monthly:${monthStr}:tz:${timezone}:${currencyDisplay}${providerTypeSuffix}${includeModelStatsSuffix}${userFilterSuffix}`;
} else {
- // allTime: leaderboard:v2:{scope}:allTime:USD (no date component)
+ // allTime: leaderboard:v3:{scope}:allTime:USD (no date component)
return `${prefix}:allTime:tz:${timezone}:${currencyDisplay}${providerTypeSuffix}${includeModelStatsSuffix}${userFilterSuffix}`;
}
}
diff --git a/src/lib/redis/live-chain-store.test.ts b/src/lib/redis/live-chain-store.test.ts
index 54e3370b9..0785456f2 100644
--- a/src/lib/redis/live-chain-store.test.ts
+++ b/src/lib/redis/live-chain-store.test.ts
@@ -144,7 +144,7 @@ describe("inferPhase", () => {
outcome,
statusCode: outcome === "success" ? 200 : 503,
durationMs: 100,
- ttfbMs: null,
+ ttftMs: null,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
@@ -173,7 +173,7 @@ describe("inferPhase", () => {
outcome: "success",
statusCode: 200,
durationMs: 100,
- ttfbMs: 20,
+ ttftMs: 20,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
@@ -197,7 +197,7 @@ describe("inferPhase", () => {
outcome: "success",
statusCode: 200,
durationMs: 100,
- ttfbMs: 20,
+ ttftMs: 20,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
diff --git a/src/lib/utils/performance-formatter.test.ts b/src/lib/utils/performance-formatter.test.ts
index fb13f6c4e..64c3c127f 100644
--- a/src/lib/utils/performance-formatter.test.ts
+++ b/src/lib/utils/performance-formatter.test.ts
@@ -8,17 +8,17 @@ describe("calculateOutputRate", () => {
});
it("firstByteMs 缺失返回 null,不再回退到总耗时", () => {
- // 门禁上线前的历史行只有 TFFT。用总耗时兜底会把上游排队算进生成时间。
+ // 门禁上线前的历史行只有 TTFT。用总耗时兜底会把上游排队算进生成时间。
expect(calculateOutputRate(50, 1000, null)).toBeNull();
});
- it("TTFB 大于 TFFT 会让 TPS 偏高,TTFB 基准才是准确值", () => {
- const basedOnTfft = calculateOutputRate(50, 1000, 900);
+ it("TTFT 大于 TTFB 会让 TPS 偏高,TTFB 基准才是准确值", () => {
+ const basedOnTtft = calculateOutputRate(50, 1000, 900);
const basedOnTtfb = calculateOutputRate(50, 1000, 200);
- expect(basedOnTfft).toBe(500);
+ expect(basedOnTtft).toBe(500);
expect(basedOnTtfb).toBe(62.5);
- expect(basedOnTtfb!).toBeLessThan(basedOnTfft!);
+ expect(basedOnTtfb!).toBeLessThan(basedOnTtft!);
});
it("生成窗口非正、无 token、无耗时都返回 null", () => {
diff --git a/src/repository/leaderboard.ts b/src/repository/leaderboard.ts
index 7f404b38d..a4b78bef5 100644
--- a/src/repository/leaderboard.ts
+++ b/src/repository/leaderboard.ts
@@ -68,7 +68,7 @@ export interface ProviderLeaderboardEntry {
totalCost: number;
totalTokens: number;
successRate: number | null; // 0-1 之间的小数,UI 层负责格式化为百分比
- avgTtfbMs: number; // 毫秒
+ avgTtftMs: number; // 毫秒
avgTokensPerSecond: number; // tok/s(仅统计流式且可计算的请求)
avgCostPerRequest: number | null; // totalCost / totalRequests, null when totalRequests === 0
avgCostPerMillionTokens: number | null; // totalCost * 1_000_000 / totalTokens, null when totalTokens === 0
@@ -91,7 +91,7 @@ export interface ModelProviderStat {
totalCost: number;
totalTokens: number;
successRate: number | null; // 0-1
- avgTtfbMs: number; // 毫秒
+ avgTtftMs: number; // 毫秒
avgTokensPerSecond: number; // tok/s
avgCostPerRequest: number | null;
avgCostPerMillionTokens: number | null;
@@ -663,8 +663,8 @@ async function findProviderLeaderboardWithTimezone(
0::double precision
)`;
const successRateExpr = LEDGER_SUCCESS_RATE_EXPR;
- // 展示用的均值走 ttfb_ms 列,该列存的是 TFFT(见 schema.ts)
- const avgTtfbMsExpr = sql`COALESCE(avg(${usageLedger.tfftMs})::double precision, 0::double precision)`;
+ // 展示用的均值走 ttfb_ms 列,该列存的是 TTFT(见 schema.ts)
+ const avgTtftMsExpr = sql`COALESCE(avg(${usageLedger.ttftMs})::double precision, 0::double precision)`;
// TPS 必须以真 TTFB 为基准;first_byte_ms 为 NULL 的历史行由 IS NOT NULL 排除
const avgTokensPerSecondExpr = sql`COALESCE(
avg(
@@ -694,7 +694,7 @@ async function findProviderLeaderboardWithTimezone(
totalCost: totalCostExpr,
totalTokens: totalTokensExpr,
successRate: successRateExpr,
- avgTtfbMs: avgTtfbMsExpr,
+ avgTtftMs: avgTtftMsExpr,
avgTokensPerSecond: avgTokensPerSecondExpr,
})
.from(usageLedger)
@@ -725,7 +725,7 @@ async function findProviderLeaderboardWithTimezone(
totalCost,
totalTokens,
successRate: clampRatio01Nullable(entry.successRate),
- avgTtfbMs: entry.avgTtfbMs ?? 0,
+ avgTtftMs: entry.avgTtftMs ?? 0,
avgTokensPerSecond: entry.avgTokensPerSecond ?? 0,
cacheCoefficientBp: cacheCoefficients.get(entry.providerId)?.coefficientBp ?? null,
...avgCosts,
@@ -751,7 +751,7 @@ async function findProviderLeaderboardWithTimezone(
totalCost: totalCostExpr,
totalTokens: totalTokensExpr,
successRate: successRateExpr,
- avgTtfbMs: avgTtfbMsExpr,
+ avgTtftMs: avgTtftMsExpr,
avgTokensPerSecond: avgTokensPerSecondExpr,
})
.from(usageLedger)
@@ -780,7 +780,7 @@ async function findProviderLeaderboardWithTimezone(
totalCost,
totalTokens,
successRate: basisDisclosureRequired ? null : clampRatio01Nullable(row.successRate),
- avgTtfbMs: row.avgTtfbMs ?? 0,
+ avgTtftMs: row.avgTtftMs ?? 0,
avgTokensPerSecond: row.avgTokensPerSecond ?? 0,
rowIdentityBasis: billingModelSource,
successRateBasis: basisDisclosureRequired ? "unavailable" : "original",
diff --git a/src/repository/message-write-buffer.ts b/src/repository/message-write-buffer.ts
index 2f517a31b..552b58c80 100644
--- a/src/repository/message-write-buffer.ts
+++ b/src/repository/message-write-buffer.ts
@@ -17,7 +17,7 @@ export type MessageRequestUpdatePatch = {
statusCode?: number;
inputTokens?: number;
outputTokens?: number;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
cacheCreationInputTokens?: number;
cacheReadInputTokens?: number;
@@ -268,8 +268,8 @@ const COLUMN_MAP: Record = {
statusCode: "status_code",
inputTokens: "input_tokens",
outputTokens: "output_tokens",
- // ttfb_ms 是 TFFT 的历史列名,见 schema.ts 的说明
- tfftMs: "ttfb_ms",
+ // ttfb_ms 是 TTFT 的历史列名,见 schema.ts 的说明
+ ttftMs: "ttfb_ms",
firstByteMs: "first_byte_ms",
cacheCreationInputTokens: "cache_creation_input_tokens",
cacheReadInputTokens: "cache_read_input_tokens",
diff --git a/src/repository/message.ts b/src/repository/message.ts
index 46dfd4e88..448555221 100644
--- a/src/repository/message.ts
+++ b/src/repository/message.ts
@@ -60,7 +60,7 @@ type PublicStatusFinalDetails = {
durationMs?: number;
statusCode?: number;
outputTokens?: number;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
providerChain?: CreateMessageRequestData["provider_chain"];
errorMessage?: string;
@@ -207,7 +207,7 @@ function queuePublicStatusRollupForFinalDetails(
model: details.model ?? seed.model,
originalModel: seed.originalModel,
durationMs: seed.durationMs,
- tfftMs: details.tfftMs,
+ ttftMs: details.ttftMs,
firstByteMs: details.firstByteMs,
outputTokens: details.outputTokens,
providerChain: details.providerChain,
@@ -572,7 +572,7 @@ export type MessageRequestDetailsUpdate = {
statusCode?: number;
inputTokens?: number;
outputTokens?: number;
- tfftMs?: number | null;
+ ttftMs?: number | null;
firstByteMs?: number | null;
cacheCreationInputTokens?: number;
cacheReadInputTokens?: number;
@@ -633,8 +633,8 @@ export async function updateMessageRequestDetails(
if (details.outputTokens !== undefined) {
updateData.outputTokens = details.outputTokens;
}
- if (details.tfftMs !== undefined) {
- updateData.tfftMs = details.tfftMs;
+ if (details.ttftMs !== undefined) {
+ updateData.ttftMs = details.ttftMs;
}
if (details.firstByteMs !== undefined) {
updateData.firstByteMs = details.firstByteMs;
@@ -918,7 +918,7 @@ export async function findMessageRequestById(id: number): Promise finiteNumber(summary[key]) === undefined) ||
- !(summary.ttfbMs === null || finiteNumber(summary.ttfbMs) !== undefined) ||
+ !(ttftMs === null || finiteNumber(ttftMs) !== undefined) ||
!(summary.winnerProviderId === null || finiteNumber(summary.winnerProviderId) !== undefined) ||
!(summary.winnerRound === null || finiteNumber(summary.winnerRound) !== undefined)
) {
@@ -246,7 +247,7 @@ function normalizeRoutingTraceSummary(value: unknown): RoutingTraceSummaryV1 | u
outcome: summary.outcome as RoutingTraceRequestOutcome,
statusCode: summary.statusCode as number,
durationMs: summary.durationMs as number,
- ttfbMs: summary.ttfbMs as number | null,
+ ttftMs: ttftMs as number | null,
attemptsPerRequest: summary.attemptsPerRequest as number,
maxActiveAttempts: summary.maxActiveAttempts as number,
rounds: summary.rounds as number,
diff --git a/tests/integration/proxy-hedge-lifecycle.test.ts b/tests/integration/proxy-hedge-lifecycle.test.ts
index 566c84dfc..fb5271efc 100644
--- a/tests/integration/proxy-hedge-lifecycle.test.ts
+++ b/tests/integration/proxy-hedge-lifecycle.test.ts
@@ -644,7 +644,7 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
pathName: "first-byte hedge",
},
])(
- "records TFFT at the enforced Responses gate commit before downstream reads ($pathName path)",
+ "records TTFT at the enforced Responses gate commit before downstream reads ($pathName path)",
async ({ expectedMode, firstByteTimeoutStreamingMs }) => {
const upstream = await startUpstream();
const client = new AbortController();
@@ -666,16 +666,16 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
await upstream.write(stream.neutralPrefix.join(""));
await neutralPrefixConsumption.consumed;
expect(session.firstByteMs).toBeNull();
- expect(session.tfftMs).toBeNull();
+ expect(session.ttftMs).toBeNull();
// When: sequence 5 arrives, it is the first user-visible content boundary.
now.mockReturnValue(10_125);
await upstream.write(stream.firstContent);
const forwardedResponse = await forwarded;
- // Then: TTFB and TFFT remain distinct before any downstream read occurs.
+ // Then: TTFB and TTFT remain distinct before any downstream read occurs.
expect(session.firstByteMs).toBe(50);
- expect(session.tfftMs).toBe(125);
+ expect(session.ttftMs).toBe(125);
expect(session.getRoutingTrace()?.mode).toBe(expectedMode);
const firstByteMsAtCommit = session.firstByteMs;
@@ -689,7 +689,7 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
await agents.released;
expect(session.firstByteMs).toBe(firstByteMsAtCommit);
- expect(session.tfftMs).toBe(125);
+ expect(session.ttftMs).toBe(125);
expect(session.getProviderChain()).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -710,7 +710,7 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
}
);
- it("records TFFT when a Discovery Responses winner commits before downstream reads", async () => {
+ it("records TTFT when a Discovery Responses winner commits before downstream reads", async () => {
const [loser, winner] = await Promise.all([startUpstream(), startUpstream()]);
const client = new AbortController();
const now = vi.spyOn(Date, "now");
@@ -726,7 +726,7 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
winningProvider.providerType = "codex";
state.providers.push(winningProvider);
const session = await createSession(initialProvider, "/v1/responses", client.signal);
- session.sessionId = "integration-discovery-tfft";
+ session.sessionId = "integration-discovery-ttft";
const agents = watchAgentReleases(2);
const stream = responsesStreamFixture("resp_discovery", "msg_discovery");
@@ -735,16 +735,16 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
now.mockReturnValue(10_050);
await winner.write(stream.neutralPrefix.join(""));
await neutralPrefixConsumption.consumed;
- expect(session.tfftMs).toBeNull();
+ expect(session.ttftMs).toBeNull();
// When: sequence 5 makes the alternative ready and Discovery commits it.
now.mockReturnValue(10_125);
await winner.write(stream.firstContent);
const forwardedResponse = await forwarded;
- // Then: TFFT is fixed at winner commit, before ResponseHandler reads the stream.
+ // Then: TTFT is fixed at winner commit, before ResponseHandler reads the stream.
expect(session.firstByteMs).toBe(50);
- expect(session.tfftMs).toBe(125);
+ expect(session.ttftMs).toBe(125);
const firstByteMsAtCommit = session.firstByteMs;
now.mockReturnValue(10_900);
@@ -758,7 +758,7 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p
await agents.released;
expect(session.firstByteMs).toBe(firstByteMsAtCommit);
- expect(session.tfftMs).toBe(125);
+ expect(session.ttftMs).toBe(125);
expect(loser.abortCount()).toBe(1);
expect(winner.abortCount()).toBe(0);
expect(agents.pool.getPoolStats().activeRequests).toBe(0);
diff --git a/tests/unit/api/leaderboard-route.test.ts b/tests/unit/api/leaderboard-route.test.ts
index 748ecc650..8e8cc205f 100644
--- a/tests/unit/api/leaderboard-route.test.ts
+++ b/tests/unit/api/leaderboard-route.test.ts
@@ -111,7 +111,7 @@ describe("GET /api/leaderboard", () => {
totalCost: 5.0,
totalTokens: 500000,
successRate: 0.95,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 50,
avgCostPerRequest: 0.05,
avgCostPerMillionTokens: 10.0,
@@ -151,7 +151,7 @@ describe("GET /api/leaderboard", () => {
totalCost: 0,
totalTokens: 0,
successRate: 0,
- avgTtfbMs: 0,
+ avgTtftMs: 0,
avgTokensPerSecond: 0,
avgCostPerRequest: null,
avgCostPerMillionTokens: null,
@@ -307,7 +307,7 @@ describe("GET /api/leaderboard", () => {
totalCost: 1.5,
totalTokens: 1000,
successRate: 1,
- avgTtfbMs: 100,
+ avgTtftMs: 100,
avgTokensPerSecond: 20,
avgCostPerRequest: 0.15,
avgCostPerMillionTokens: 1500,
@@ -318,7 +318,7 @@ describe("GET /api/leaderboard", () => {
totalCost: 1.0,
totalTokens: 600,
successRate: 1,
- avgTtfbMs: 110,
+ avgTtftMs: 110,
avgTokensPerSecond: 25,
avgCostPerRequest: 0.1667,
avgCostPerMillionTokens: 1666.7,
@@ -359,7 +359,7 @@ describe("GET /api/leaderboard", () => {
totalCost: 1.0,
totalTokens: 1000,
successRate: 1,
- avgTtfbMs: 100,
+ avgTtftMs: 100,
avgTokensPerSecond: 20,
avgCostPerRequest: 0.1,
avgCostPerMillionTokens: 1000,
diff --git a/tests/unit/api/public-status-api-contract.test.ts b/tests/unit/api/public-status-api-contract.test.ts
index 3cfc8593f..78a9a6d92 100644
--- a/tests/unit/api/public-status-api-contract.test.ts
+++ b/tests/unit/api/public-status-api-contract.test.ts
@@ -195,7 +195,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -213,7 +213,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "anthropic",
latestState: "failed",
availabilityPct: 20,
- latestTtfbMs: 900,
+ latestTtftMs: 900,
latestTps: null,
timeline: [
{
@@ -221,7 +221,7 @@ describe("GET /api/public-status contract", () => {
bucketEnd: "2026-04-22T09:35:00.000Z",
state: "failed",
availabilityPct: 20,
- ttfbMs: 900,
+ ttftMs: 900,
tps: null,
sampleCount: 8,
},
@@ -291,7 +291,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -302,7 +302,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 390,
+ latestTtftMs: 390,
latestTps: null,
timeline: [],
},
@@ -348,7 +348,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "anthropic",
latestState: "failed",
availabilityPct: 20,
- latestTtfbMs: 900,
+ latestTtftMs: 900,
latestTps: null,
timeline: [
{
@@ -356,7 +356,7 @@ describe("GET /api/public-status contract", () => {
bucketEnd: "2026-04-22T09:35:00.000Z",
state: "failed",
availabilityPct: 20,
- ttfbMs: 900,
+ ttftMs: 900,
tps: null,
sampleCount: 8,
},
@@ -404,7 +404,7 @@ describe("GET /api/public-status contract", () => {
requestTypeBadge: "anthropic",
latestState: "failed",
availabilityPct: 20,
- latestTtfbMs: 900,
+ latestTtftMs: 900,
latestTps: null,
timeline: [
{
@@ -412,7 +412,7 @@ describe("GET /api/public-status contract", () => {
bucketEnd: "2026-04-22T09:35:00.000Z",
state: "failed",
availabilityPct: 20,
- ttfbMs: 900,
+ ttftMs: 900,
tps: null,
sampleCount: 8,
},
diff --git a/tests/unit/app/status/derive-display-state.test.ts b/tests/unit/app/status/derive-display-state.test.ts
index 54d6ddac8..f52c44dc1 100644
--- a/tests/unit/app/status/derive-display-state.test.ts
+++ b/tests/unit/app/status/derive-display-state.test.ts
@@ -14,7 +14,7 @@ function makeBucket(
bucketEnd: "2026-04-22T00:05:00.000Z",
state: "operational",
availabilityPct: 100,
- ttfbMs: 200,
+ ttftMs: 200,
tps: 50,
sampleCount: 10,
...overrides,
diff --git a/tests/unit/app/status/fill-display-timeline.test.ts b/tests/unit/app/status/fill-display-timeline.test.ts
index 559c1ddbd..4f68c688b 100644
--- a/tests/unit/app/status/fill-display-timeline.test.ts
+++ b/tests/unit/app/status/fill-display-timeline.test.ts
@@ -12,7 +12,7 @@ function bucket(
bucketEnd: `2026-04-22T00:${String(index + 1).padStart(2, "0")}:00.000Z`,
state,
availabilityPct: pct,
- ttfbMs: state === "no_data" ? null : 200,
+ ttftMs: state === "no_data" ? null : 200,
tps: state === "no_data" ? null : 50,
sampleCount: state === "no_data" ? 0 : 5,
};
diff --git a/tests/unit/app/status/timeline-windows.test.ts b/tests/unit/app/status/timeline-windows.test.ts
index 77c24d5f4..7730333a8 100644
--- a/tests/unit/app/status/timeline-windows.test.ts
+++ b/tests/unit/app/status/timeline-windows.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import type { PublicStatusTimelineBucket } from "@/lib/public-status/payload";
import {
CHART_BUCKETS,
- computeAvgTtfb,
+ computeAvgTtft,
computeUptimePct,
sliceTimelineForChart,
} from "@/app/[locale]/status/_lib/timeline-windows";
@@ -16,7 +16,7 @@ function makeBucket(
bucketEnd: "",
state: "operational",
availabilityPct: 100,
- ttfbMs: 200,
+ ttftMs: 200,
tps: 50,
sampleCount: 10,
...overrides,
@@ -68,23 +68,23 @@ describe("computeUptimePct", () => {
});
});
-describe("computeAvgTtfb", () => {
+describe("computeAvgTtft", () => {
it("returns null when no samples", () => {
- expect(computeAvgTtfb([])).toBeNull();
+ expect(computeAvgTtft([])).toBeNull();
});
it("computes weighted integer average", () => {
- const result = computeAvgTtfb([
- makeBucket(0, { ttfbMs: 100, sampleCount: 4 }),
- makeBucket(1, { ttfbMs: 300, sampleCount: 4 }),
+ const result = computeAvgTtft([
+ makeBucket(0, { ttftMs: 100, sampleCount: 4 }),
+ makeBucket(1, { ttftMs: 300, sampleCount: 4 }),
]);
expect(result).toBe(200);
});
- it("skips buckets with null ttfb", () => {
- const result = computeAvgTtfb([
- makeBucket(0, { ttfbMs: 200, sampleCount: 5 }),
- makeBucket(1, { ttfbMs: null, sampleCount: 5 }),
+ it("skips buckets with null ttft", () => {
+ const result = computeAvgTtft([
+ makeBucket(0, { ttftMs: 200, sampleCount: 5 }),
+ makeBucket(1, { ttftMs: null, sampleCount: 5 }),
]);
expect(result).toBe(200);
});
diff --git a/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx b/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx
index c67dbfb3e..9f38f8c22 100644
--- a/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx
+++ b/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx
@@ -50,7 +50,7 @@ vi.mock("@/actions/usage-logs", () => ({
costUsd: "0.000001",
costMultiplier: null,
durationMs: 10,
- tfftMs: 5,
+ ttftMs: 5,
errorMessage: null,
providerChain: null,
blockedBy: null,
diff --git a/tests/unit/dashboard-logs-warmup-ui.test.tsx b/tests/unit/dashboard-logs-warmup-ui.test.tsx
index 1f78f16a3..0c22f7e3c 100644
--- a/tests/unit/dashboard-logs-warmup-ui.test.tsx
+++ b/tests/unit/dashboard-logs-warmup-ui.test.tsx
@@ -75,7 +75,7 @@ describe("UsageLogsTable - warmup 跳过展示", () => {
costUsd: null,
costMultiplier: null,
durationMs: 0,
- tfftMs: 0,
+ ttftMs: 0,
errorMessage: null,
providerChain: null,
blockedBy: "warmup",
@@ -128,7 +128,7 @@ describe("UsageLogsTable - cache badge alignment", () => {
costUsd: "0.000001",
costMultiplier: null,
durationMs: 10,
- tfftMs: 5,
+ ttftMs: 5,
errorMessage: null,
providerChain: null,
blockedBy: null,
diff --git a/tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx b/tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx
index 59e659059..860f11dca 100644
--- a/tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx
+++ b/tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx
@@ -140,7 +140,7 @@ describe("LeaderboardView cache coefficient column", () => {
totalCostFormatted: "$4.20",
totalTokens: 2400,
successRate: 0.9,
- avgTtfbMs: 150,
+ avgTtftMs: 150,
avgTokensPerSecond: 42,
avgCostPerRequest: 0.35,
avgCostPerMillionTokens: 1750,
diff --git a/tests/unit/error-details-dialog-warmup-ui.test.tsx b/tests/unit/error-details-dialog-warmup-ui.test.tsx
index eacb43e36..a120d9314 100644
--- a/tests/unit/error-details-dialog-warmup-ui.test.tsx
+++ b/tests/unit/error-details-dialog-warmup-ui.test.tsx
@@ -164,7 +164,7 @@ describe("ErrorDetailsDialog - warmup skip indicator", () => {
costMultiplier={null}
context1mApplied={false}
durationMs={null}
- tfftMs={null}
+ ttftMs={null}
externalOpen
/>
);
diff --git a/tests/unit/langfuse/langfuse-trace.test.ts b/tests/unit/langfuse/langfuse-trace.test.ts
index 0c4bd8262..e02246ae9 100644
--- a/tests/unit/langfuse/langfuse-trace.test.ts
+++ b/tests/unit/langfuse/langfuse-trace.test.ts
@@ -100,7 +100,7 @@ function createMockSession(overrides: Record = {}) {
user: { id: 7, name: "testuser" },
key: { name: "default-key" },
},
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
forwardStartTime: startTime + 5,
forwardedRequestBody: null,
@@ -464,12 +464,12 @@ describe("traceProxyRequest", () => {
expect(llmCall[1].metadata.originalModel).toBe("claude-sonnet-4-20250514");
});
- test("should set completionStartTime from tfftMs", async () => {
+ test("should set completionStartTime from ttftMs", async () => {
const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request");
const startTime = Date.now() - 500;
await traceProxyRequest({
- session: createMockSession({ startTime, tfftMs: 200 }),
+ session: createMockSession({ startTime, ttftMs: 200 }),
responseHeaders: new Headers(),
durationMs: 500,
statusCode: 200,
@@ -890,7 +890,7 @@ describe("traceProxyRequest", () => {
session: createMockSession({
startTime,
forwardStartTime,
- tfftMs: 105,
+ ttftMs: 105,
firstByteMs: 105,
getProviderChain: () => [
{ id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 },
@@ -906,8 +906,8 @@ describe("traceProxyRequest", () => {
const expectedTimingBreakdown = {
guardPipelineMs: 5,
upstreamTotalMs: 495,
- tfftFromForwardMs: 100, // tfftMs(105) - guardPipelineMs(5)
- tokenGenerationMs: 395, // durationMs(500) - tfftMs(105)
+ ttftFromForwardMs: 100, // ttftMs(105) - guardPipelineMs(5)
+ tokenGenerationMs: 395, // durationMs(500) - ttftMs(105)
failedAttempts: 1, // only retry_failed is non-success
providersAttempted: 2, // 2 unique provider ids
};
diff --git a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
index 69ecff895..98b0288d7 100644
--- a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
+++ b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
@@ -211,8 +211,8 @@ function makeSession(clientAbortSignal: AbortSignal | null, stream: boolean): Pr
shouldPersistSessionDebugArtifacts: () => false,
shouldTrackSessionObservability: () => false,
getResolvedPricingByBillingSource: async () => null,
- recordTfft: vi.fn(),
- tfftMs: null,
+ recordTtft: vi.fn(),
+ ttftMs: null,
firstByteMs: null,
addProviderToChain: vi.fn(),
clearResponseTimeout: vi.fn(),
diff --git a/tests/unit/proxy/response-handler-client-abort-drain.test.ts b/tests/unit/proxy/response-handler-client-abort-drain.test.ts
index 447864632..e97233285 100644
--- a/tests/unit/proxy/response-handler-client-abort-drain.test.ts
+++ b/tests/unit/proxy/response-handler-client-abort-drain.test.ts
@@ -296,7 +296,7 @@ function createSession(
sessionId: null,
specialSettings: [],
startTime: Date.now(),
- tfftMs: null,
+ ttftMs: null,
firstByteMs: null,
userAgent: "Go-http-client/1.1",
userName: "admin",
@@ -318,7 +318,7 @@ function createSession(
getResolvedPricingByBillingSource: async () => null,
getSpecialSettings: () => [],
isHeaderModified: () => false,
- recordTfft: vi.fn(),
+ recordTtft: vi.fn(),
releaseAgent: vi.fn(),
setContext1mApplied: vi.fn(),
shouldPersistSessionDebugArtifacts: () => false,
@@ -1423,7 +1423,7 @@ describe("ProxyResponseHandler stream client abort finalization", () => {
await downstream.text();
await drainAsyncTasks();
- expect(session.recordTfft).not.toHaveBeenCalled();
+ expect(session.recordTtft).not.toHaveBeenCalled();
expect(session.clearResponseTimeout).toHaveBeenCalledTimes(1);
});
diff --git a/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts b/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
index 7a4483048..2fb9c50d5 100644
--- a/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
+++ b/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
@@ -232,8 +232,8 @@ function createSession(opts?: { sessionId?: string | null }): ProxySession {
getCurrentModel: () => "test-model",
getProviderChain: () => session.providerChain,
getCachedPriceDataByBillingSource: async () => testPriceData,
- recordTfft: () => 100,
- tfftMs: null,
+ recordTtft: () => 100,
+ ttftMs: null,
firstByteMs: null,
getRequestSequence: () => 1,
addProviderToChain: function (
diff --git a/tests/unit/proxy/response-handler-lease-decrement.test.ts b/tests/unit/proxy/response-handler-lease-decrement.test.ts
index 6ae2968bf..83f0de8c3 100644
--- a/tests/unit/proxy/response-handler-lease-decrement.test.ts
+++ b/tests/unit/proxy/response-handler-lease-decrement.test.ts
@@ -211,8 +211,8 @@ function createSession(opts: {
source: "cloud_exact" as const,
priceData: testPriceData,
}),
- recordTfft: () => 100,
- tfftMs: null,
+ recordTtft: () => 100,
+ ttftMs: null,
firstByteMs: null,
getRequestSequence: () => 1,
});
diff --git a/tests/unit/proxy/response-handler-non200.test.ts b/tests/unit/proxy/response-handler-non200.test.ts
index d7160ee19..666de4da3 100644
--- a/tests/unit/proxy/response-handler-non200.test.ts
+++ b/tests/unit/proxy/response-handler-non200.test.ts
@@ -194,8 +194,8 @@ function createSession(opts: {
getCurrentModel: () => redirectedModel,
getProviderChain: () => session.providerChain,
getCachedPriceDataByBillingSource: async () => testPriceData,
- recordTfft: () => 100,
- tfftMs: null,
+ recordTtft: () => 100,
+ ttftMs: null,
firstByteMs: null,
getRequestSequence: () => 1,
addProviderToChain: function (
diff --git a/tests/unit/proxy/routing-trace.test.ts b/tests/unit/proxy/routing-trace.test.ts
index 4950e3d0f..9317fd80e 100644
--- a/tests/unit/proxy/routing-trace.test.ts
+++ b/tests/unit/proxy/routing-trace.test.ts
@@ -63,7 +63,7 @@ function makeTraceSession(startTime = 1_000): ProxySession {
liveObservabilityClosed: false,
routingTraceTerminalLogged: false,
providerChain: [],
- ttfbMs: null,
+ ttftMs: null,
});
return session;
}
@@ -292,7 +292,7 @@ describe("ProxySession routing trace recorder", () => {
outcome: "success",
statusCode: 200,
durationMs: 50,
- ttfbMs: 50,
+ ttftMs: 50,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
diff --git a/tests/unit/proxy/session-ttfb-tfft.test.ts b/tests/unit/proxy/session-ttfb-ttft.test.ts
similarity index 76%
rename from tests/unit/proxy/session-ttfb-tfft.test.ts
rename to tests/unit/proxy/session-ttfb-ttft.test.ts
index 798557ec0..e0e85acef 100644
--- a/tests/unit/proxy/session-ttfb-tfft.test.ts
+++ b/tests/unit/proxy/session-ttfb-ttft.test.ts
@@ -38,27 +38,27 @@ function createSession(startTime: number): ProxySession {
});
}
-describe("ProxySession TTFB / TFFT", () => {
- it("门控旁路时 recordTfft 同时补齐 TTFB(两者同一时刻)", () => {
+describe("ProxySession TTFB / TTFT", () => {
+ it("门控旁路时 recordTtft 同时补齐 TTFB(两者同一时刻)", () => {
const session = createSession(Date.now() - 1_200);
- const tfft = session.recordTfft();
+ const ttft = session.recordTtft();
- expect(session.tfftMs).toBe(tfft);
- expect(session.firstByteMs).toBe(tfft);
+ expect(session.ttftMs).toBe(ttft);
+ expect(session.firstByteMs).toBe(ttft);
});
- it("门控提交时先记 TTFB,recordTfft 不覆盖它", () => {
+ it("门控提交时先记 TTFB,recordTtft 不覆盖它", () => {
const startTime = Date.now() - 3_000;
const session = createSession(startTime);
session.recordFirstByte(startTime + 400);
- const tfft = session.recordTfft();
+ const ttft = session.recordTtft();
expect(session.firstByteMs).toBe(400);
- expect(session.tfftMs).toBe(tfft);
- // TTFB 必须早于 TFFT,否则延迟分解与 TPS 分母都会失真
- expect(session.firstByteMs!).toBeLessThan(session.tfftMs!);
+ expect(session.ttftMs).toBe(ttft);
+ // TTFB 必须早于 TTFT,否则延迟分解与 TPS 分母都会失真
+ expect(session.firstByteMs!).toBeLessThan(session.ttftMs!);
});
it("recordFirstByte 首写生效:failover 后不会被后续尝试改写", () => {
@@ -80,11 +80,11 @@ describe("ProxySession TTFB / TFFT", () => {
expect(session.firstByteMs).toBe(0);
});
- it("recordTfft 幂等:重复调用不改变已记录的值", () => {
+ it("recordTtft 幂等:重复调用不改变已记录的值", () => {
const session = createSession(Date.now() - 800);
- const first = session.recordTfft();
- const second = session.recordTfft();
+ const first = session.recordTtft();
+ const second = session.recordTtft();
expect(second).toBe(first);
expect(session.firstByteMs).toBe(first);
diff --git a/tests/unit/public-status/aggregation-core-tps.test.ts b/tests/unit/public-status/aggregation-core-tps.test.ts
index b2e2f2f23..9a716bc0b 100644
--- a/tests/unit/public-status/aggregation-core-tps.test.ts
+++ b/tests/unit/public-status/aggregation-core-tps.test.ts
@@ -15,8 +15,8 @@ describe("computeTokensPerSecond", () => {
expect(computeTokensPerSecond({ outputTokens: 50, durationMs: 1000 })).toBeNull();
});
- it("TTFB 基准得到的 TPS 低于(被门控放大的)TFFT 基准", () => {
- const basedOnTfft = computeTokensPerSecond({
+ it("TTFB 基准得到的 TPS 低于(被门控放大的)TTFT 基准", () => {
+ const basedOnTtft = computeTokensPerSecond({
outputTokens: 50,
durationMs: 1000,
firstByteMs: 900,
@@ -27,7 +27,7 @@ describe("computeTokensPerSecond", () => {
firstByteMs: 200,
});
- expect(basedOnTfft).toBe(500);
+ expect(basedOnTtft).toBe(500);
expect(basedOnTtfb).toBe(62.5);
});
diff --git a/tests/unit/public-status/aggregation.test.ts b/tests/unit/public-status/aggregation.test.ts
index 7d19ed7e4..6435fcddb 100644
--- a/tests/unit/public-status/aggregation.test.ts
+++ b/tests/unit/public-status/aggregation.test.ts
@@ -34,7 +34,7 @@ describe("public-status aggregation", () => {
createdAt: "2026-04-21T10:10:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1000,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 80,
providerChain: [
@@ -52,7 +52,7 @@ describe("public-status aggregation", () => {
createdAt: "2026-04-21T10:40:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1400,
- tfftMs: 300,
+ ttftMs: 300,
firstByteMs: 300,
outputTokens: 60,
providerChain: [
@@ -103,7 +103,7 @@ describe("public-status aggregation", () => {
createdAt: "2026-04-21T10:25:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1500,
- tfftMs: 500,
+ ttftMs: 500,
firstByteMs: 500,
outputTokens: null,
providerChain: [
@@ -229,7 +229,7 @@ describe("public-status aggregation", () => {
createdAt: "2026-04-21T10:10:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
@@ -256,19 +256,19 @@ describe("public-status aggregation", () => {
const successfulModel = result.groups[1]?.models[0];
expect(failedModel?.availabilityPct).toBe(0);
- expect(failedModel?.latestTtfbMs).toBeNull();
+ expect(failedModel?.latestTtftMs).toBeNull();
expect(failedModel?.latestTps).toBeNull();
expect(failedModel?.timeline.find((bucket) => bucket.sampleCount > 0)).toMatchObject({
sampleCount: 1,
- ttfbMs: null,
+ ttftMs: null,
tps: null,
});
expect(successfulModel?.availabilityPct).toBe(100);
- expect(successfulModel?.latestTtfbMs).toBe(200);
+ expect(successfulModel?.latestTtftMs).toBe(200);
expect(successfulModel?.latestTps).toBe(50);
expect(successfulModel?.timeline.find((bucket) => bucket.sampleCount > 0)).toMatchObject({
sampleCount: 1,
- ttfbMs: 200,
+ ttftMs: 200,
tps: 50,
});
});
diff --git a/tests/unit/public-status/public-status-view.test.tsx b/tests/unit/public-status/public-status-view.test.tsx
index 72d8bd3e8..83b5851f6 100644
--- a/tests/unit/public-status/public-status-view.test.tsx
+++ b/tests/unit/public-status/public-status-view.test.tsx
@@ -108,7 +108,7 @@ function buildPayload(overrides: Partial = {}): PublicStatu
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -173,7 +173,7 @@ function buildLabels() {
generatedAt: "Updated",
history: "History",
availability: "Availability",
- ttfb: "TTFB",
+ ttft: "TTFT",
freshnessWindow: "Snapshot freshness",
fresh: "Fresh",
stale: "Stale",
@@ -196,7 +196,7 @@ function buildLabels() {
},
tooltip: {
availability: "Availability tooltip",
- ttfb: "TTFB tooltip",
+ ttft: "TTFT tooltip",
tps: "TPS tooltip",
historyAriaLabel: "History aria",
},
@@ -346,7 +346,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -386,7 +386,7 @@ describe("public-status view", () => {
requestTypeBadge: "gemini",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -429,7 +429,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -447,7 +447,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "failed",
availabilityPct: 50,
- latestTtfbMs: 700,
+ latestTtftMs: 700,
latestTps: null,
timeline: [],
},
@@ -477,7 +477,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.9,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -542,7 +542,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "failed",
availabilityPct: 0,
- latestTtfbMs: null,
+ latestTtftMs: null,
latestTps: null,
timeline: [],
},
@@ -569,7 +569,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 100,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [
{
@@ -577,7 +577,7 @@ describe("public-status view", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "operational",
availabilityPct: 100,
- ttfbMs: 420,
+ ttftMs: 420,
tps: null,
sampleCount: 1,
},
@@ -639,7 +639,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 100,
- latestTtfbMs: 420,
+ latestTtftMs: 420,
latestTps: null,
timeline: [],
},
@@ -650,7 +650,7 @@ describe("public-status view", () => {
requestTypeBadge: "openaiCompatible",
latestState: "failed",
availabilityPct: 0,
- latestTtfbMs: null,
+ latestTtftMs: null,
latestTps: null,
timeline: [],
},
diff --git a/tests/unit/public-status/read-store.test.ts b/tests/unit/public-status/read-store.test.ts
index fca5ca0c1..406d828fa 100644
--- a/tests/unit/public-status/read-store.test.ts
+++ b/tests/unit/public-status/read-store.test.ts
@@ -407,7 +407,7 @@ describe("readPublicStatusPayload", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.5,
- latestTtfbMs: 120,
+ latestTtftMs: 120,
latestTps: 4.2,
endpointUrl: "https://internal.example.com",
timeline: [
@@ -416,7 +416,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "operational",
availabilityPct: 99.5,
- ttfbMs: 120,
+ ttftMs: 120,
tps: 4.2,
sampleCount: 10,
providerFailures: 1,
@@ -461,7 +461,7 @@ describe("readPublicStatusPayload", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.5,
- latestTtfbMs: 120,
+ latestTtftMs: 120,
latestTps: 4.2,
timeline: [
{
@@ -469,7 +469,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "operational",
availabilityPct: 99.5,
- ttfbMs: 120,
+ ttftMs: 120,
tps: 4.2,
sampleCount: 10,
},
@@ -481,6 +481,82 @@ describe("readPublicStatusPayload", () => {
});
});
+ it("reads legacy TTFB field names from snapshots and exposes only TTFT names", async () => {
+ const triggerRebuildHint = vi.fn();
+ const redis = createRedisReader({
+ [buildPublicStatusManifestKey({
+ configVersion: "cfg-1",
+ intervalMinutes: 5,
+ rangeHours: 24,
+ })]: {
+ configVersion: "cfg-1",
+ intervalMinutes: 5,
+ rangeHours: 24,
+ generation: "gen-1",
+ sourceGeneration: "gen-1",
+ coveredFrom: "2026-04-20T10:00:00.000Z",
+ coveredTo: "2026-04-21T10:00:00.000Z",
+ generatedAt: "2026-04-21T09:59:00.000Z",
+ freshUntil: "2026-04-21T10:04:00.000Z",
+ rebuildState: "idle",
+ lastCompleteGeneration: "gen-1",
+ },
+ [buildPublicStatusCurrentSnapshotKey({
+ intervalMinutes: 5,
+ rangeHours: 24,
+ generation: "gen-1",
+ })]: {
+ sourceGeneration: "gen-1",
+ generatedAt: "2026-04-21T09:59:00.000Z",
+ freshUntil: "2026-04-21T10:04:00.000Z",
+ groups: [
+ {
+ publicGroupSlug: "openai",
+ displayName: "OpenAI",
+ explanatoryCopy: null,
+ models: [
+ {
+ publicModelKey: "gpt-4.1",
+ label: "GPT-4.1",
+ vendorIconKey: "openai",
+ requestTypeBadge: "openaiCompatible",
+ latestTtfbMs: 120,
+ timeline: [
+ {
+ bucketStart: "2026-04-21T09:55:00.000Z",
+ bucketEnd: "2026-04-21T10:00:00.000Z",
+ state: "operational",
+ availabilityPct: 99.5,
+ ttfbMs: 110,
+ tps: 4.2,
+ sampleCount: 10,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ const payload = await readPublicStatusPayload({
+ intervalMinutes: 5,
+ rangeHours: 24,
+ nowIso: "2026-04-21T10:01:00.000Z",
+ configVersion: "cfg-1",
+ hasConfiguredGroups: true,
+ redis,
+ triggerRebuildHint,
+ });
+
+ expect(payload.groups[0]?.models[0]).toMatchObject({
+ latestTtftMs: 120,
+ timeline: [{ ttftMs: 110 }],
+ });
+ expect(JSON.stringify(payload)).not.toContain("ttfbMs");
+ expect(JSON.stringify(payload)).not.toContain("latestTtfbMs");
+ });
+
it("preserves degraded latestState and timeline states from redis snapshots", async () => {
const triggerRebuildHint = vi.fn();
const redis = createRedisReader({
@@ -522,7 +598,7 @@ describe("readPublicStatusPayload", () => {
requestTypeBadge: "openaiCompatible",
latestState: "degraded",
availabilityPct: 92.5,
- latestTtfbMs: 180,
+ latestTtftMs: 180,
latestTps: 3.1,
timeline: [
{
@@ -530,7 +606,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "degraded",
availabilityPct: 92.5,
- ttfbMs: 180,
+ ttftMs: 180,
tps: 3.1,
sampleCount: 8,
},
@@ -560,7 +636,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "degraded",
availabilityPct: 92.5,
- ttfbMs: 180,
+ ttftMs: 180,
tps: 3.1,
sampleCount: 8,
},
@@ -627,7 +703,7 @@ describe("readPublicStatusPayload", () => {
requestTypeBadge: "openaiCompatible",
latestState: "operational",
availabilityPct: 99.5,
- latestTtfbMs: 120,
+ latestTtftMs: 120,
latestTps: 4.2,
timeline: [
{
@@ -635,7 +711,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T09:50:00.000Z",
state: "operational",
availabilityPct: 99.1,
- ttfbMs: 110,
+ ttftMs: 110,
tps: 4,
sampleCount: Number.NaN,
},
@@ -644,7 +720,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T09:55:00.000Z",
state: "operational",
availabilityPct: 99.3,
- ttfbMs: 115,
+ ttftMs: 115,
tps: 4.1,
sampleCount: Number.POSITIVE_INFINITY,
},
@@ -653,7 +729,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "operational",
availabilityPct: 99.5,
- ttfbMs: 120,
+ ttftMs: 120,
tps: 4.2,
sampleCount: 10,
},
@@ -685,7 +761,7 @@ describe("readPublicStatusPayload", () => {
bucketEnd: "2026-04-21T10:00:00.000Z",
state: "operational",
availabilityPct: 99.5,
- ttfbMs: 120,
+ ttftMs: 120,
tps: 4.2,
sampleCount: 10,
},
diff --git a/tests/unit/public-status/rebuild-worker.test.ts b/tests/unit/public-status/rebuild-worker.test.ts
index 05c6f4bfc..dd02fa73e 100644
--- a/tests/unit/public-status/rebuild-worker.test.ts
+++ b/tests/unit/public-status/rebuild-worker.test.ts
@@ -47,7 +47,7 @@ async function importAggregationModule() {
originalModel?: string | null;
model?: string | null;
durationMs?: number | null;
- ttfbMs?: number | null;
+ ttftMs?: number | null;
outputTokens?: number | null;
providerChain?: Array<{
id: number;
@@ -200,7 +200,7 @@ describe("public-status rebuild worker", () => {
createdAt: "2026-04-21T10:10:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1000,
- ttfbMs: 200,
+ ttftMs: 200,
outputTokens: 80,
providerChain: [
{
@@ -217,7 +217,7 @@ describe("public-status rebuild worker", () => {
createdAt: "2026-04-21T10:40:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1400,
- ttfbMs: 300,
+ ttftMs: 300,
outputTokens: 60,
providerChain: [
{
diff --git a/tests/unit/public-status/rollup-store.test.ts b/tests/unit/public-status/rollup-store.test.ts
index 466f9207d..d465f6249 100644
--- a/tests/unit/public-status/rollup-store.test.ts
+++ b/tests/unit/public-status/rollup-store.test.ts
@@ -42,7 +42,7 @@ describe("public-status rollup store", () => {
createdAt: "2026-04-21T10:02:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
@@ -150,7 +150,7 @@ describe("public-status rollup store", () => {
createdAt: "2026-04-21T10:02:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
@@ -227,7 +227,7 @@ describe("public-status rollup store", () => {
createdAt: "2026-04-21T10:02:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
@@ -312,7 +312,7 @@ describe("public-status rollup store", () => {
createdAt: "2026-04-21T10:02:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
@@ -350,7 +350,7 @@ describe("public-status rollup store", () => {
createdAt: "2026-04-21T10:02:00.000Z",
originalModel: "gpt-4.1",
durationMs: 1200,
- tfftMs: 200,
+ ttftMs: 200,
firstByteMs: 200,
outputTokens: 50,
providerChain: [
diff --git a/tests/unit/public-status/status-page-locale.test.ts b/tests/unit/public-status/status-page-locale.test.ts
index fb0f0fa4a..95a1af5c2 100644
--- a/tests/unit/public-status/status-page-locale.test.ts
+++ b/tests/unit/public-status/status-page-locale.test.ts
@@ -38,7 +38,7 @@ describe("PublicStatusPage locale handling", () => {
generatedAt: "Updated",
history: "History",
availability: "Availability",
- ttfb: "TTFB",
+ ttft: "TTFT",
freshnessWindow: "Snapshot freshness",
fresh: "Fresh",
stale: "Stale",
@@ -66,7 +66,7 @@ describe("PublicStatusPage locale handling", () => {
"statusBadge.failed": "Failed",
"statusBadge.noData": "No data",
"tooltip.availability": "Availability tooltip",
- "tooltip.ttfb": "TTFB tooltip",
+ "tooltip.ttft": "TTFT tooltip",
"tooltip.tps": "TPS tooltip",
"tooltip.historyAriaLabel": "History aria",
};
diff --git a/tests/unit/redis/leaderboard-cache.test.ts b/tests/unit/redis/leaderboard-cache.test.ts
index f3ce0f6c4..66cb34561 100644
--- a/tests/unit/redis/leaderboard-cache.test.ts
+++ b/tests/unit/redis/leaderboard-cache.test.ts
@@ -102,7 +102,7 @@ describe("getLeaderboardWithCache", () => {
true
);
expect(redis.setex).toHaveBeenCalledWith(
- "leaderboard:v2:userCacheHitRate:daily:2026-04-13:tz:UTC:USD:includeModelStats:tags:team-a,vip:groups:group-1",
+ "leaderboard:v3:userCacheHitRate:daily:2026-04-13:tz:UTC:USD:includeModelStats:tags:team-a,vip:groups:group-1",
60,
JSON.stringify(rows)
);
@@ -128,7 +128,7 @@ describe("getLeaderboardWithCache", () => {
await getLeaderboardWithCache("daily", "USD", "userCacheHitRate");
expect(redis.setex).toHaveBeenCalledWith(
- "leaderboard:v2:userCacheHitRate:daily:2026-04-14:tz:Asia/Shanghai:USD",
+ "leaderboard:v3:userCacheHitRate:daily:2026-04-14:tz:Asia/Shanghai:USD",
60,
JSON.stringify(rows)
);
diff --git a/tests/unit/repository/leaderboard-provider-metrics.test.ts b/tests/unit/repository/leaderboard-provider-metrics.test.ts
index ac85f9ae8..275ae7e4b 100644
--- a/tests/unit/repository/leaderboard-provider-metrics.test.ts
+++ b/tests/unit/repository/leaderboard-provider-metrics.test.ts
@@ -53,7 +53,7 @@ vi.mock("@/drizzle/schema", () => ({
successRateOutcome: "successRateOutcome",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
model: "model",
@@ -71,7 +71,7 @@ vi.mock("@/drizzle/schema", () => ({
errorMessage: "errorMessage",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
model: "model",
@@ -125,7 +125,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "5.0",
totalTokens: 500000,
successRate: 0.95,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 50,
},
]),
@@ -156,7 +156,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "5.0",
totalTokens: 500000,
successRate: 0.95,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 50,
},
]),
@@ -181,7 +181,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "0",
totalTokens: 0,
successRate: 0,
- avgTtfbMs: 0,
+ avgTtftMs: 0,
avgTokensPerSecond: 0,
},
]),
@@ -204,7 +204,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "1.0",
totalTokens: 0,
successRate: 0,
- avgTtfbMs: 0,
+ avgTtftMs: 0,
avgTokensPerSecond: 0,
},
]),
@@ -227,7 +227,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "10.0",
totalTokens: 500000,
successRate: 0.95,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 50,
},
{
@@ -237,7 +237,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "2.0",
totalTokens: 100000,
successRate: 0.9,
- avgTtfbMs: 300,
+ avgTtftMs: 300,
avgTokensPerSecond: 40,
},
]),
@@ -260,7 +260,7 @@ describe("Provider Leaderboard Average Cost Metrics", () => {
totalCost: "1.0",
totalTokens: 1000,
successRate: null,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 10,
},
]),
@@ -294,7 +294,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "10.0",
totalTokens: 1000,
successRate: 0.9,
- avgTtfbMs: 200,
+ avgTtftMs: 200,
avgTokensPerSecond: 50,
},
{
@@ -304,7 +304,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "5.0",
totalTokens: 500,
successRate: 0.8,
- avgTtfbMs: 300,
+ avgTtftMs: 300,
avgTokensPerSecond: 40,
},
]),
@@ -316,7 +316,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "6.0",
totalTokens: 600,
successRate: 0.95,
- avgTtfbMs: 120,
+ avgTtftMs: 120,
avgTokensPerSecond: 55,
},
{
@@ -326,7 +326,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "4.0",
totalTokens: 400,
successRate: 0.85,
- avgTtfbMs: 180,
+ avgTtftMs: 180,
avgTokensPerSecond: 45,
},
{
@@ -336,7 +336,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "0.1",
totalTokens: 10,
successRate: 0,
- avgTtfbMs: 0,
+ avgTtftMs: 0,
avgTokensPerSecond: 0,
},
{
@@ -346,7 +346,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "5.0",
totalTokens: 500,
successRate: 0.8,
- avgTtfbMs: 300,
+ avgTtftMs: 300,
avgTokensPerSecond: 40,
},
]),
@@ -382,7 +382,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "1.0",
totalTokens: 100,
successRate: 0.9,
- avgTtfbMs: 100,
+ avgTtftMs: 100,
avgTokensPerSecond: 10,
},
]),
@@ -394,7 +394,7 @@ describe("Provider Leaderboard Model Breakdown", () => {
totalCost: "1.0",
totalTokens: 100,
successRate: 0.9,
- avgTtfbMs: 100,
+ avgTtftMs: 100,
avgTokensPerSecond: 10,
},
]),
@@ -690,7 +690,7 @@ describe("Provider Leaderboard Cache Coefficient", () => {
totalCost,
totalTokens: 1000,
successRate: 0.9,
- avgTtfbMs: 100,
+ avgTtftMs: 100,
avgTokensPerSecond: 10,
});
diff --git a/tests/unit/repository/leaderboard-timezone-parentheses.test.ts b/tests/unit/repository/leaderboard-timezone-parentheses.test.ts
index 7cd5195ec..33a2ebbcd 100644
--- a/tests/unit/repository/leaderboard-timezone-parentheses.test.ts
+++ b/tests/unit/repository/leaderboard-timezone-parentheses.test.ts
@@ -87,7 +87,7 @@ vi.mock("@/drizzle/schema", () => ({
cacheReadInputTokens: "cacheReadInputTokens",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
statusCode: "statusCode",
@@ -108,7 +108,7 @@ vi.mock("@/drizzle/schema", () => ({
errorMessage: "errorMessage",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
statusCode: "statusCode",
diff --git a/tests/unit/repository/leaderboard-tps-basis.test.ts b/tests/unit/repository/leaderboard-tps-basis.test.ts
index 9b2f0167a..2ac545ae7 100644
--- a/tests/unit/repository/leaderboard-tps-basis.test.ts
+++ b/tests/unit/repository/leaderboard-tps-basis.test.ts
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
/**
* 排行榜的两个延迟指标口径不同,必须分开:
- * - 展示用的 avgTtfbMs 走 usage_ledger.ttfb_ms(该列存的是 TFFT)
+ * - 展示用的 avgTtftMs 走 usage_ledger.ttfb_ms(该列存的是 TTFT)
* - avgTokensPerSecond 的分母必须是真 TTFB(first_byte_ms),历史行由 IS NOT NULL 排除
*/
@@ -46,7 +46,7 @@ vi.mock("@/drizzle/schema", () => ({
successRateOutcome: "successRateOutcome",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
model: "model",
@@ -91,10 +91,10 @@ describe("排行榜延迟指标口径", () => {
const tpsSql = JSON.stringify(projection?.avgTokensPerSecond);
expect(tpsSql).toContain("firstByteMs");
- expect(tpsSql).not.toContain("tfftMs");
+ expect(tpsSql).not.toContain("ttftMs");
- const avgLatencySql = JSON.stringify(projection?.avgTtfbMs);
- expect(avgLatencySql).toContain("tfftMs");
+ const avgLatencySql = JSON.stringify(projection?.avgTtftMs);
+ expect(avgLatencySql).toContain("ttftMs");
expect(avgLatencySql).not.toContain("firstByteMs");
});
});
diff --git a/tests/unit/repository/leaderboard-user-model-stats.test.ts b/tests/unit/repository/leaderboard-user-model-stats.test.ts
index 6e75b17e1..ee731b4f7 100644
--- a/tests/unit/repository/leaderboard-user-model-stats.test.ts
+++ b/tests/unit/repository/leaderboard-user-model-stats.test.ts
@@ -83,7 +83,7 @@ vi.mock("@/drizzle/schema", () => ({
successRateOutcome: "successRateOutcome",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
model: "model",
@@ -101,7 +101,7 @@ vi.mock("@/drizzle/schema", () => ({
errorMessage: "errorMessage",
blockedBy: "blockedBy",
createdAt: "createdAt",
- tfftMs: "tfftMs",
+ ttftMs: "ttftMs",
firstByteMs: "firstByteMs",
durationMs: "durationMs",
model: "model",
diff --git a/tests/unit/repository/message-public-readback.test.ts b/tests/unit/repository/message-public-readback.test.ts
index cf3270f5b..939fcee11 100644
--- a/tests/unit/repository/message-public-readback.test.ts
+++ b/tests/unit/repository/message-public-readback.test.ts
@@ -64,7 +64,7 @@ const MESSAGE_ROW = {
...LATEST_ROW,
model: "gpt-4.1",
originalModel: "gpt-4.1-mini",
- tfftMs: 120,
+ ttftMs: 120,
costMultiplier: "1.5",
sessionId: "public-session",
userAgent: "vitest",
@@ -110,7 +110,7 @@ const LEDGER_ROW = {
context1mApplied: false,
swapCacheTtlApplied: true,
durationMs: 1_500,
- tfftMs: 250,
+ ttftMs: 250,
sessionId: "ledger-session",
isReplay: true,
replaySourceRequestId: 901,
diff --git a/tests/unit/repository/message-public-status-rollup.test.ts b/tests/unit/repository/message-public-status-rollup.test.ts
index 70c96b18b..ed26adde2 100644
--- a/tests/unit/repository/message-public-status-rollup.test.ts
+++ b/tests/unit/repository/message-public-status-rollup.test.ts
@@ -440,7 +440,7 @@ describe("repository/message public status rollup hook", () => {
const finalDetails = {
statusCode: 200,
- tfftMs: 200,
+ ttftMs: 200,
outputTokens: 50,
providerChain: [
{
@@ -469,7 +469,7 @@ describe("repository/message public status rollup hook", () => {
originalModel: "gpt-4.1",
model: "gpt-4.1",
outputTokens: 50,
- tfftMs: 200,
+ ttftMs: 200,
}),
})
);
@@ -489,7 +489,7 @@ describe("repository/message public status rollup hook", () => {
await updateMessageRequestDetails(202, {
statusCode: 200,
- tfftMs: 250,
+ ttftMs: 250,
outputTokens: 75,
providerChain: [
{
@@ -513,7 +513,7 @@ describe("repository/message public status rollup hook", () => {
originalModel: "gpt-4.1",
model: "gpt-4.1",
outputTokens: 75,
- tfftMs: 250,
+ ttftMs: 250,
}),
})
);
@@ -577,7 +577,7 @@ describe("repository/message public status rollup hook", () => {
const finalDetails = {
statusCode: 200,
- tfftMs: 300,
+ ttftMs: 300,
outputTokens: 90,
providerChain: [
{
@@ -604,7 +604,7 @@ describe("repository/message public status rollup hook", () => {
createdAt: new Date("2026-04-21T10:04:00.000Z"),
durationMs: 1800,
outputTokens: 90,
- tfftMs: 300,
+ ttftMs: 300,
}),
})
);
@@ -661,7 +661,7 @@ describe("repository/message public status rollup hook", () => {
const finalDetails = {
statusCode: 200,
- tfftMs: 300,
+ ttftMs: 300,
outputTokens: 90,
providerChain: [
{
@@ -750,7 +750,7 @@ describe("repository/message public status rollup hook", () => {
const finalDetails = {
statusCode: 200,
- tfftMs: 320,
+ ttftMs: 320,
outputTokens: 95,
providerChain: [
{
@@ -776,7 +776,7 @@ describe("repository/message public status rollup hook", () => {
createdAt: new Date("2026-04-21T10:06:00.000Z"),
durationMs: 1900,
outputTokens: 95,
- tfftMs: 320,
+ ttftMs: 320,
}),
})
);
diff --git a/tests/unit/repository/message-session-readback.test.ts b/tests/unit/repository/message-session-readback.test.ts
index d6b7c8e99..d290852bb 100644
--- a/tests/unit/repository/message-session-readback.test.ts
+++ b/tests/unit/repository/message-session-readback.test.ts
@@ -106,7 +106,7 @@ const LEDGER_ROW = {
context1mApplied: true,
swapCacheTtlApplied: false,
durationMs: 1_200,
- tfftMs: 200,
+ ttftMs: 200,
sessionId: "ledger-session-readback",
isReplay: true,
replaySourceRequestId: 911,
diff --git a/tests/unit/repository/message-terminal-public-status-seam.test.ts b/tests/unit/repository/message-terminal-public-status-seam.test.ts
index 33748d342..bd6c5d8fb 100644
--- a/tests/unit/repository/message-terminal-public-status-seam.test.ts
+++ b/tests/unit/repository/message-terminal-public-status-seam.test.ts
@@ -303,7 +303,7 @@ describe("message terminal public-status public seam", () => {
statusCode: 502,
inputTokens: 31,
outputTokens: 3,
- tfftMs: 900,
+ ttftMs: 900,
providerChain: [
{
id: 11,
@@ -321,7 +321,7 @@ describe("message terminal public-status public seam", () => {
durationMs: 1_500,
statusCode: 200,
outputTokens: 96,
- tfftMs: 300,
+ ttftMs: 300,
providerChain: [
{
id: 22,
@@ -337,7 +337,7 @@ describe("message terminal public-status public seam", () => {
const row: TerminalRow & {
inputTokens: number | null;
outputTokens: number | null;
- tfftMs: number | null;
+ ttftMs: number | null;
providerChain: unknown;
providerId: number | null;
} = {
@@ -349,7 +349,7 @@ describe("message terminal public-status public seam", () => {
statusCode: null,
inputTokens: null,
outputTokens: null,
- tfftMs: null,
+ ttftMs: null,
providerChain: null,
providerId: null,
};
@@ -385,7 +385,7 @@ describe("message terminal public-status public seam", () => {
row.statusCode = Number(readCaseValue("status_code"));
row.inputTokens = Number(readCaseValue("input_tokens"));
row.outputTokens = Number(readCaseValue("output_tokens"));
- row.tfftMs = Number(readCaseValue("ttfb_ms"));
+ row.ttftMs = Number(readCaseValue("ttfb_ms"));
row.providerChain = JSON.parse(String(readCaseValue("provider_chain")));
row.providerId = Number(readCaseValue("provider_id"));
return [{ id }];
@@ -525,7 +525,7 @@ describe("message terminal public-status public seam", () => {
statusCode: oldFailureDetails.statusCode,
inputTokens: oldFailureDetails.inputTokens,
outputTokens: oldFailureDetails.outputTokens,
- tfftMs: oldFailureDetails.tfftMs,
+ ttftMs: oldFailureDetails.ttftMs,
providerChain: oldFailureDetails.providerChain,
providerId: oldFailureDetails.providerId,
});
diff --git a/tests/unit/repository/message-terminal-write-apis.test.ts b/tests/unit/repository/message-terminal-write-apis.test.ts
index 6dfd4bc27..c64836901 100644
--- a/tests/unit/repository/message-terminal-write-apis.test.ts
+++ b/tests/unit/repository/message-terminal-write-apis.test.ts
@@ -245,7 +245,7 @@ describe("message terminal write APIs", () => {
const details = {
inputTokens: 101,
outputTokens: 23,
- tfftMs: null,
+ ttftMs: null,
cacheCreationInputTokens: 7,
cacheReadInputTokens: 8,
cacheCreation5mInputTokens: 3,
diff --git a/tests/unit/repository/message-usage-logs-query.test.ts b/tests/unit/repository/message-usage-logs-query.test.ts
index fe945e5a4..c37e165a2 100644
--- a/tests/unit/repository/message-usage-logs-query.test.ts
+++ b/tests/unit/repository/message-usage-logs-query.test.ts
@@ -154,7 +154,7 @@ describe("message repository findUsageLogs", () => {
context1mApplied: true,
swapCacheTtlApplied: false,
durationMs: 250,
- tfftMs: 40,
+ ttftMs: 40,
sessionId: "session-ledger",
createdAt,
},
diff --git a/tests/unit/repository/message-write-buffer.test.ts b/tests/unit/repository/message-write-buffer.test.ts
index a3c0fefb8..6dd9ca48e 100644
--- a/tests/unit/repository/message-write-buffer.test.ts
+++ b/tests/unit/repository/message-write-buffer.test.ts
@@ -154,7 +154,7 @@ describe("message_request 异步批量写入", () => {
} = await import("@/repository/message-write-buffer");
enqueueMessageRequestUpdate(42, { durationMs: 100 });
- enqueueMessageRequestUpdate(42, { statusCode: 200, tfftMs: 10 });
+ enqueueMessageRequestUpdate(42, { statusCode: 200, ttftMs: 10 });
await flushMessageRequestWriteBuffer();
await stopMessageRequestWriteBuffer();
@@ -1438,7 +1438,7 @@ describe("message_request 异步批量写入", () => {
outcome: "success" as const,
statusCode: 200,
durationMs: 100,
- ttfbMs: 50,
+ ttftMs: 50,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
diff --git a/tests/unit/repository/usage-logs-actual-response-model.test.ts b/tests/unit/repository/usage-logs-actual-response-model.test.ts
index c73bc7452..d3606856d 100644
--- a/tests/unit/repository/usage-logs-actual-response-model.test.ts
+++ b/tests/unit/repository/usage-logs-actual-response-model.test.ts
@@ -51,7 +51,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => {
groupCostMultiplier: null,
costBreakdown: null,
durationMs: 500,
- tfftMs: 100,
+ ttftMs: 100,
errorMessage: null,
providerChain: null,
blockedBy: null,
@@ -124,7 +124,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => {
costMultiplier: null,
groupCostMultiplier: null,
durationMs: 400,
- tfftMs: 80,
+ ttftMs: 80,
clientIp: null,
context1mApplied: false,
swapCacheTtlApplied: false,
@@ -184,7 +184,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => {
costMultiplier: null,
groupCostMultiplier: null,
durationMs: 0,
- tfftMs: 0,
+ ttftMs: 0,
clientIp: null,
context1mApplied: false,
swapCacheTtlApplied: false,
diff --git a/tests/unit/repository/usage-logs-replay-projection.test.ts b/tests/unit/repository/usage-logs-replay-projection.test.ts
index 5e2093649..88b72d4ad 100644
--- a/tests/unit/repository/usage-logs-replay-projection.test.ts
+++ b/tests/unit/repository/usage-logs-replay-projection.test.ts
@@ -55,7 +55,7 @@ function makeReplayRow(overrides: Record = {}) {
costBreakdown: null,
hedgeLosers: null,
durationMs: null,
- tfftMs: null,
+ ttftMs: null,
firstByteMs: null,
errorMessage: null,
providerChain: null,
diff --git a/tests/unit/repository/usage-logs-sessionid-filter.test.ts b/tests/unit/repository/usage-logs-sessionid-filter.test.ts
index 59b87856a..8ced8b065 100644
--- a/tests/unit/repository/usage-logs-sessionid-filter.test.ts
+++ b/tests/unit/repository/usage-logs-sessionid-filter.test.ts
@@ -143,7 +143,7 @@ describe("Usage logs sessionId filter", () => {
costUsd: "0.01",
costMultiplier: null,
durationMs: 10,
- tfftMs: 5,
+ ttftMs: 5,
errorMessage: null,
providerChain: null,
blockedBy: null,
@@ -177,7 +177,7 @@ describe("Usage logs sessionId filter", () => {
costUsd: "0.01",
costMultiplier: null,
durationMs: 10,
- tfftMs: 5,
+ ttftMs: 5,
errorMessage: null,
providerChain: null,
blockedBy: null,
diff --git a/tests/unit/types/routing-trace.test.ts b/tests/unit/types/routing-trace.test.ts
index 3c5456452..a934c3dc8 100644
--- a/tests/unit/types/routing-trace.test.ts
+++ b/tests/unit/types/routing-trace.test.ts
@@ -38,7 +38,7 @@ describe("normalizeRoutingTrace", () => {
outcome: "success",
statusCode: 200,
durationMs: 5_000,
- ttfbMs: 1_000,
+ ttftMs: 1_000,
attemptsPerRequest: 2,
maxActiveAttempts: 2,
rounds: 1,
@@ -83,6 +83,36 @@ describe("normalizeRoutingTrace", () => {
expect(trace?.summary).not.toHaveProperty("rawErrorBody");
});
+ it("读取旧版 summary.ttfbMs 并归一化为 ttftMs", () => {
+ const trace = normalizeRoutingTrace({
+ version: 1,
+ mode: "single_upstream",
+ startedAt: 1,
+ updatedAt: 2,
+ discoveryEnabled: false,
+ eligible: false,
+ summary: {
+ outcome: "success",
+ statusCode: 200,
+ durationMs: 5_000,
+ ttfbMs: 1_000,
+ attemptsPerRequest: 1,
+ maxActiveAttempts: 1,
+ rounds: 1,
+ providerMs: 3_000,
+ fallbackPromotions: 0,
+ cancelFailures: 0,
+ winnerOrigin: "normal",
+ winnerProviderId: 7,
+ winnerRound: 1,
+ },
+ events: [],
+ });
+
+ expect(trace?.summary).toMatchObject({ ttftMs: 1_000 });
+ expect(trace?.summary).not.toHaveProperty("ttfbMs");
+ });
+
it("限制事件数量并标记 truncated", () => {
const events = Array.from({ length: ROUTING_TRACE_MAX_EVENTS + 1 }, (_, index) => ({
type: "round_started",
diff --git a/tests/unit/usage-logs/export-csv.test.ts b/tests/unit/usage-logs/export-csv.test.ts
index 6c5dccfb3..54a85c86b 100644
--- a/tests/unit/usage-logs/export-csv.test.ts
+++ b/tests/unit/usage-logs/export-csv.test.ts
@@ -30,7 +30,7 @@ function makeLog(overrides: Partial = {}): UsageLogRow {
groupCostMultiplier: null,
costBreakdown: null,
durationMs: 123,
- ttfbMs: null,
+ ttftMs: null,
errorMessage: null,
providerChain: null,
blockedBy: null,
diff --git a/tests/unit/usage-logs/export-summary.test.ts b/tests/unit/usage-logs/export-summary.test.ts
index b0ac2309a..f396cabf0 100644
--- a/tests/unit/usage-logs/export-summary.test.ts
+++ b/tests/unit/usage-logs/export-summary.test.ts
@@ -29,7 +29,7 @@ function makeLog(overrides: Partial = {}): UsageLogRow {
groupCostMultiplier: null,
costBreakdown: null,
durationMs: 100,
- ttfbMs: null,
+ ttftMs: null,
errorMessage: null,
providerChain: null,
blockedBy: null,
diff --git a/tests/unit/usage-logs/export-xlsx.test.ts b/tests/unit/usage-logs/export-xlsx.test.ts
index a2178a69e..3bd0d9e6d 100644
--- a/tests/unit/usage-logs/export-xlsx.test.ts
+++ b/tests/unit/usage-logs/export-xlsx.test.ts
@@ -30,7 +30,7 @@ function makeLog(overrides: Partial = {}): UsageLogRow {
groupCostMultiplier: null,
costBreakdown: null,
durationMs: 123,
- ttfbMs: null,
+ ttftMs: null,
errorMessage: null,
providerChain: null,
blockedBy: null,