From ca6738faa24e0449a794b1b295af331d98aae815 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sun, 2 Aug 2026 01:26:09 +0800 Subject: [PATCH 1/2] fix(i18n): rename first-token metrics to TTFT Rename runtime fields, leaderboard keys, public status payload types, and locale keys from the historical TFFT/Ttfb spellings to TTFT/Ttft. Preserve the database ttfb_ms column and Redis rollup metric names as storage compatibility contracts. --- messages/en/dashboard.json | 10 +-- messages/en/settings/statusPage.json | 6 +- messages/ja/dashboard.json | 10 +-- messages/ja/settings/statusPage.json | 6 +- messages/ru/dashboard.json | 10 +-- messages/ru/settings/statusPage.json | 6 +- messages/zh-CN/dashboard.json | 10 +-- messages/zh-CN/settings/statusPage.json | 6 +- messages/zh-TW/dashboard.json | 10 +-- messages/zh-TW/settings/statusPage.json | 6 +- .../_components/leaderboard-view.tsx | 8 +-- .../_components/error-details-dialog.test.tsx | 27 ++++---- .../components/LatencyBreakdownBar.tsx | 20 +++--- .../components/PerformanceTab.tsx | 62 +++++++++---------- .../error-details-dialog/index.tsx | 6 +- .../_components/error-details-dialog/types.ts | 2 +- .../provider-chain-popover.test.tsx | 12 ++-- .../_components/usage-logs-table.test.tsx | 14 ++--- .../logs/_components/usage-logs-table.tsx | 14 ++--- .../virtualized-logs-table.test.tsx | 26 ++++---- .../_components/virtualized-logs-table.tsx | 18 +++--- src/app/[locale]/status/[slug]/page.tsx | 4 +- .../_components/public-status-timeline.tsx | 8 +-- .../status/_components/public-status-view.tsx | 20 +++--- .../_lib/{format-ttfb.ts => format-ttft.ts} | 2 +- .../[locale]/status/_lib/timeline-windows.ts | 6 +- src/app/[locale]/status/page.tsx | 4 +- src/app/v1/_lib/proxy/forwarder.ts | 8 +-- src/app/v1/_lib/proxy/response-handler.ts | 18 +++--- src/app/v1/_lib/proxy/session.ts | 24 +++---- src/app/v1/_lib/proxy/warmup-guard.ts | 2 +- src/drizzle/schema.ts | 12 ++-- src/lib/langfuse/emit-proxy-trace.ts | 2 +- src/lib/langfuse/trace-proxy-request.ts | 14 ++--- src/lib/observability/discovery-metrics.ts | 4 +- src/lib/public-status/aggregation-core.ts | 2 +- src/lib/public-status/aggregation.ts | 28 ++++----- src/lib/public-status/openapi.ts | 10 +-- src/lib/public-status/payload.ts | 4 +- src/lib/public-status/read-store.ts | 4 +- src/lib/public-status/rollup-store.ts | 22 +++---- src/lib/redis/live-chain-store.test.ts | 6 +- src/lib/utils/performance-formatter.test.ts | 10 +-- src/repository/leaderboard.ts | 16 ++--- src/repository/message-write-buffer.ts | 6 +- src/repository/message.ts | 24 +++---- src/repository/usage-logs.ts | 20 +++--- src/types/message.ts | 2 +- src/types/routing-trace.ts | 6 +- .../integration/proxy-hedge-lifecycle.test.ts | 22 +++---- tests/unit/api/leaderboard-route.test.ts | 10 +-- .../api/public-status-api-contract.test.ts | 18 +++--- .../app/status/derive-display-state.test.ts | 2 +- .../app/status/fill-display-timeline.test.ts | 2 +- .../unit/app/status/timeline-windows.test.ts | 20 +++--- ...s-virtualized-special-settings-ui.test.tsx | 2 +- tests/unit/dashboard-logs-warmup-ui.test.tsx | 4 +- ...eaderboard-view-cache-coefficient.test.tsx | 2 +- .../error-details-dialog-warmup-ui.test.tsx | 2 +- tests/unit/langfuse/langfuse-trace.test.ts | 12 ++-- ...nse-handler-abort-listener-cleanup.test.ts | 4 +- ...esponse-handler-client-abort-drain.test.ts | 6 +- ...handler-endpoint-circuit-isolation.test.ts | 4 +- .../response-handler-lease-decrement.test.ts | 4 +- .../proxy/response-handler-non200.test.ts | 4 +- tests/unit/proxy/routing-trace.test.ts | 4 +- ...tfft.test.ts => session-ttfb-ttft.test.ts} | 26 ++++---- .../aggregation-core-tps.test.ts | 6 +- tests/unit/public-status/aggregation.test.ts | 16 ++--- .../public-status/public-status-view.test.tsx | 26 ++++---- tests/unit/public-status/read-store.test.ts | 24 +++---- .../unit/public-status/rebuild-worker.test.ts | 6 +- tests/unit/public-status/rollup-store.test.ts | 10 +-- .../public-status/status-page-locale.test.ts | 4 +- .../leaderboard-provider-metrics.test.ts | 36 +++++------ .../leaderboard-timezone-parentheses.test.ts | 4 +- .../repository/leaderboard-tps-basis.test.ts | 10 +-- .../leaderboard-user-model-stats.test.ts | 4 +- .../message-public-readback.test.ts | 4 +- .../message-public-status-rollup.test.ts | 18 +++--- .../message-session-readback.test.ts | 2 +- ...essage-terminal-public-status-seam.test.ts | 12 ++-- .../message-terminal-write-apis.test.ts | 2 +- .../message-usage-logs-query.test.ts | 2 +- .../repository/message-write-buffer.test.ts | 4 +- .../usage-logs-actual-response-model.test.ts | 6 +- .../usage-logs-replay-projection.test.ts | 2 +- .../usage-logs-sessionid-filter.test.ts | 4 +- tests/unit/types/routing-trace.test.ts | 2 +- tests/unit/usage-logs/export-csv.test.ts | 2 +- tests/unit/usage-logs/export-summary.test.ts | 2 +- tests/unit/usage-logs/export-xlsx.test.ts | 2 +- 92 files changed, 468 insertions(+), 467 deletions(-) rename src/app/[locale]/status/_lib/{format-ttfb.ts => format-ttft.ts} (68%) rename tests/unit/proxy/{session-ttfb-tfft.test.ts => session-ttfb-ttft.test.ts} (76%) 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..9874f70d7 100644 --- a/src/app/[locale]/status/_components/public-status-timeline.tsx +++ b/src/app/[locale]/status/_components/public-status-timeline.tsx @@ -3,11 +3,11 @@ import { useMemo, useState } from "react"; import { cn } from "@/lib/utils"; import type { FilledTimelineCell } from "../_lib/fill-display-timeline"; -import { formatTtfb } from "../_lib/format-ttfb"; +import { formatTtft } from "../_lib/format-ttft"; 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..d383bee2e 100644 --- a/src/app/[locale]/status/_components/public-status-view.tsx +++ b/src/app/[locale]/status/_components/public-status-view.tsx @@ -28,7 +28,7 @@ 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 { formatTtft } from "../_lib/format-ttft"; 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..9ab2e8dba 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 @@ -434,9 +434,9 @@ export async function traceProxyRequest(ctx: TraceContext): Promise { ); // Set TTFB as completionStartTime - if (session.tfftMs != null) { + 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..184880361 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), tps: normalizeNullableNumber(value.tps), sampleCount: value.sampleCount, }, @@ -159,7 +159,7 @@ function sanitizeModelSnapshots(input: unknown): PublicStatusModelSnapshot[] { requestTypeBadge: value.requestTypeBadge, latestState: normalizeTimelineState(value.latestState), availabilityPct: normalizeNullableNumber(value.availabilityPct), - latestTtfbMs: normalizeNullableNumber(value.latestTtfbMs), + latestTtftMs: normalizeNullableNumber(value.latestTtftMs), 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/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..00ae1b2a2 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("TTFB 大于 TTFT 会让 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) || + !(summary.ttftMs === null || finiteNumber(summary.ttftMs) !== undefined) || !(summary.winnerProviderId === null || finiteNumber(summary.winnerProviderId) !== undefined) || !(summary.winnerRound === null || finiteNumber(summary.winnerRound) !== undefined) ) { @@ -246,7 +246,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: summary.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..bea06b858 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 }), + 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..11e07da0d 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: "TTFB", freshnessWindow: "Snapshot freshness", fresh: "Fresh", stale: "Stale", @@ -196,7 +196,7 @@ function buildLabels() { }, tooltip: { availability: "Availability tooltip", - ttfb: "TTFB tooltip", + ttft: "TTFB 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..f4cde6c49 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, }, @@ -522,7 +522,7 @@ describe("readPublicStatusPayload", () => { requestTypeBadge: "openaiCompatible", latestState: "degraded", availabilityPct: 92.5, - latestTtfbMs: 180, + latestTtftMs: 180, latestTps: 3.1, timeline: [ { @@ -530,7 +530,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 +560,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 +627,7 @@ describe("readPublicStatusPayload", () => { requestTypeBadge: "openaiCompatible", latestState: "operational", availabilityPct: 99.5, - latestTtfbMs: 120, + latestTtftMs: 120, latestTps: 4.2, timeline: [ { @@ -635,7 +635,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 +644,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 +653,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 +685,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..82a4be348 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: "TTFB", 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": "TTFB tooltip", "tooltip.tps": "TPS tooltip", "tooltip.historyAriaLabel": "History aria", }; 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..ff4971ace 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, 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, From f6bac1d1afeb989ff4ca807746e2873de6e693e9 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sun, 2 Aug 2026 01:48:31 +0800 Subject: [PATCH 2/2] fix(review): preserve legacy TTFT metric payloads --- .../_components/public-status-timeline.tsx | 2 +- .../status/_components/public-status-view.tsx | 2 +- src/lib/langfuse/trace-proxy-request.ts | 2 +- src/lib/public-status/read-store.ts | 6 +- src/lib/redis/leaderboard-cache.ts | 13 ++-- src/lib/utils/performance-formatter.test.ts | 2 +- src/types/routing-trace.ts | 5 +- .../unit/app/status/timeline-windows.test.ts | 2 +- .../public-status/public-status-view.test.tsx | 4 +- tests/unit/public-status/read-store.test.ts | 76 +++++++++++++++++++ .../public-status/status-page-locale.test.ts | 4 +- tests/unit/redis/leaderboard-cache.test.ts | 4 +- tests/unit/types/routing-trace.test.ts | 30 ++++++++ 13 files changed, 131 insertions(+), 21 deletions(-) diff --git a/src/app/[locale]/status/_components/public-status-timeline.tsx b/src/app/[locale]/status/_components/public-status-timeline.tsx index 9874f70d7..dc60c553b 100644 --- a/src/app/[locale]/status/_components/public-status-timeline.tsx +++ b/src/app/[locale]/status/_components/public-status-timeline.tsx @@ -1,9 +1,9 @@ "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 { formatTtft } from "../_lib/format-ttft"; export interface PublicStatusTimelineLabels { availability: string; diff --git a/src/app/[locale]/status/_components/public-status-view.tsx b/src/app/[locale]/status/_components/public-status-view.tsx index d383bee2e..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 { formatTtft } from "../_lib/format-ttft"; import { clearGroupOrder, loadCollapsedSet, diff --git a/src/lib/langfuse/trace-proxy-request.ts b/src/lib/langfuse/trace-proxy-request.ts index 9ab2e8dba..3fb1aa237 100644 --- a/src/lib/langfuse/trace-proxy-request.ts +++ b/src/lib/langfuse/trace-proxy-request.ts @@ -433,7 +433,7 @@ export async function traceProxyRequest(ctx: TraceContext): Promise { { asType: "generation", startTime: generationStartTime } as { asType: "generation" } ); - // Set TTFB as completionStartTime + // Set TTFT as completionStartTime if (session.ttftMs != null) { generation.update({ completionStartTime: new Date(session.startTime + session.ttftMs), diff --git a/src/lib/public-status/read-store.ts b/src/lib/public-status/read-store.ts index 184880361..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), - ttftMs: normalizeNullableNumber(value.ttftMs), + 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), - latestTtftMs: normalizeNullableNumber(value.latestTtftMs), + latestTtftMs: normalizeNullableNumber( + value.latestTtftMs !== undefined ? value.latestTtftMs : value.latestTtfbMs + ), latestTps: normalizeNullableNumber(value.latestTps), timeline: sanitizeTimelineBuckets(value.timeline), }, 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/utils/performance-formatter.test.ts b/src/lib/utils/performance-formatter.test.ts index 00ae1b2a2..64c3c127f 100644 --- a/src/lib/utils/performance-formatter.test.ts +++ b/src/lib/utils/performance-formatter.test.ts @@ -12,7 +12,7 @@ describe("calculateOutputRate", () => { expect(calculateOutputRate(50, 1000, null)).toBeNull(); }); - it("TTFB 大于 TTFT 会让 TPS 偏高,TTFB 基准才是准确值", () => { + it("TTFT 大于 TTFB 会让 TPS 偏高,TTFB 基准才是准确值", () => { const basedOnTtft = calculateOutputRate(50, 1000, 900); const basedOnTtfb = calculateOutputRate(50, 1000, 200); diff --git a/src/types/routing-trace.ts b/src/types/routing-trace.ts index c8a2b0bd0..665b020f1 100644 --- a/src/types/routing-trace.ts +++ b/src/types/routing-trace.ts @@ -232,11 +232,12 @@ function normalizeRoutingTraceSummary(value: unknown): RoutingTraceSummaryV1 | u "fallbackPromotions", "cancelFailures", ] as const; + const ttftMs = summary.ttftMs !== undefined ? summary.ttftMs : summary.ttfbMs; if ( !outcomes.has(summary.outcome as RoutingTraceRequestOutcome) || !winnerOrigins.has(summary.winnerOrigin as RoutingTraceWinnerOrigin) || numericKeys.some((key) => finiteNumber(summary[key]) === undefined) || - !(summary.ttftMs === null || finiteNumber(summary.ttftMs) !== 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, - ttftMs: summary.ttftMs 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/unit/app/status/timeline-windows.test.ts b/tests/unit/app/status/timeline-windows.test.ts index bea06b858..7730333a8 100644 --- a/tests/unit/app/status/timeline-windows.test.ts +++ b/tests/unit/app/status/timeline-windows.test.ts @@ -81,7 +81,7 @@ describe("computeAvgTtft", () => { expect(result).toBe(200); }); - it("skips buckets with null ttfb", () => { + it("skips buckets with null ttft", () => { const result = computeAvgTtft([ makeBucket(0, { ttftMs: 200, sampleCount: 5 }), makeBucket(1, { ttftMs: null, sampleCount: 5 }), diff --git a/tests/unit/public-status/public-status-view.test.tsx b/tests/unit/public-status/public-status-view.test.tsx index 11e07da0d..83b5851f6 100644 --- a/tests/unit/public-status/public-status-view.test.tsx +++ b/tests/unit/public-status/public-status-view.test.tsx @@ -173,7 +173,7 @@ function buildLabels() { generatedAt: "Updated", history: "History", availability: "Availability", - ttft: "TTFB", + ttft: "TTFT", freshnessWindow: "Snapshot freshness", fresh: "Fresh", stale: "Stale", @@ -196,7 +196,7 @@ function buildLabels() { }, tooltip: { availability: "Availability tooltip", - ttft: "TTFB tooltip", + ttft: "TTFT tooltip", tps: "TPS tooltip", historyAriaLabel: "History aria", }, diff --git a/tests/unit/public-status/read-store.test.ts b/tests/unit/public-status/read-store.test.ts index f4cde6c49..406d828fa 100644 --- a/tests/unit/public-status/read-store.test.ts +++ b/tests/unit/public-status/read-store.test.ts @@ -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({ diff --git a/tests/unit/public-status/status-page-locale.test.ts b/tests/unit/public-status/status-page-locale.test.ts index 82a4be348..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", - ttft: "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.ttft": "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/types/routing-trace.test.ts b/tests/unit/types/routing-trace.test.ts index ff4971ace..a934c3dc8 100644 --- a/tests/unit/types/routing-trace.test.ts +++ b/tests/unit/types/routing-trace.test.ts @@ -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",