Use header-only usage server timing

This commit is contained in:
Novick Yuan
2026-05-28 14:30:15 +08:00
parent 01c8592ca6
commit 6412294262
14 changed files with 425 additions and 75 deletions
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import {
SERVER_NOW_UNIX_MS_HEADER,
buildServerTimingMetadata,
readServerNowUnixMsFromHeaders,
withServerTiming,
} from '../serverTiming'
describe('serverTiming', () => {
it('reads server time from response headers', () => {
expect(readServerNowUnixMsFromHeaders({
[SERVER_NOW_UNIX_MS_HEADER]: '1779999000123',
})).toBe(1_779_999_000_123)
expect(readServerNowUnixMsFromHeaders({
'X-Aether-Server-Now-Unix-Ms': '1779999000456',
})).toBe(1_779_999_000_456)
})
it('does not fall back to body fields', () => {
const timing = buildServerTimingMetadata({
headers: {},
data: {
server_now_unix_ms: 1_779_999_000_123,
},
}, 1_000, 1_100)
expect(timing).toBeUndefined()
})
it('builds metadata with round trip duration', () => {
const timing = buildServerTimingMetadata({
headers: {
[SERVER_NOW_UNIX_MS_HEADER]: '1050',
},
}, 1_000, 1_125)
expect(timing).toEqual({
server_now_unix_ms: 1_050,
client_send_unix_ms: 1_000,
client_receive_unix_ms: 1_125,
round_trip_ms: 125,
})
})
it('returns the original payload when the header is missing or invalid', () => {
const payload = { records: [] }
expect(withServerTiming({ data: payload, headers: {} }, 1_000)).toBe(payload)
expect(withServerTiming({
data: payload,
headers: { [SERVER_NOW_UNIX_MS_HEADER]: 'not-a-number' },
}, 1_000)).toBe(payload)
expect(withServerTiming({
data: payload,
headers: { [SERVER_NOW_UNIX_MS_HEADER]: '0' },
}, 1_000)).toBe(payload)
expect(withServerTiming({
data: payload,
headers: { [SERVER_NOW_UNIX_MS_HEADER]: '1050.5' },
}, 1_000)).toBe(payload)
})
})
@@ -132,8 +132,10 @@ describe('usageApi contract alignment', () => {
}
if (url === '/api/admin/usage/records') {
return Promise.resolve({
headers: {
'x-aether-server-now-unix-ms': '10050',
},
data: {
server_now_unix_ms: 10_050,
records: [{ id: 'record-3' }],
total: 1,
limit: 25,
@@ -163,6 +165,7 @@ describe('usageApi contract alignment', () => {
server_now_unix_ms: 10_050,
client_send_unix_ms: 1_000,
client_receive_unix_ms: 1_100,
round_trip_ms: 100,
})
})
+2 -3
View File
@@ -148,7 +148,6 @@ export interface ApiFormatSummary {
// 使用统计响应接口
export interface UsageResponse extends ServerTimedPayload {
server_now_unix_ms?: number
total_requests: number
total_input_tokens: number
total_output_tokens: number
@@ -329,7 +328,7 @@ export const meApi = {
}): Promise<UsageResponse> {
const clientSendUnixMs = beginServerTimingSample()
const response = await apiClient.get<UsageResponse>('/api/users/me/usage', { params })
return withServerTiming(response.data, clientSendUnixMs)
return withServerTiming(response, clientSendUnixMs)
},
// 获取活跃请求状态(用于轮询更新)
@@ -368,7 +367,7 @@ export const meApi = {
const params = ids ? { ids } : {}
const clientSendUnixMs = beginServerTimingSample()
const response = await apiClient.get('/api/users/me/usage/active', { params })
return withServerTiming(response.data, clientSendUnixMs)
return withServerTiming(response, clientSendUnixMs)
},
// 获取可用的提供商
+44 -10
View File
@@ -1,7 +1,12 @@
import type { AxiosResponse } from 'axios'
export const SERVER_NOW_UNIX_MS_HEADER = 'x-aether-server-now-unix-ms'
export interface ServerTimingMetadata {
server_now_unix_ms: number
client_send_unix_ms: number
client_receive_unix_ms: number
round_trip_ms: number
}
export interface ServerTimedPayload {
@@ -12,33 +17,62 @@ export function beginServerTimingSample(): number {
return Date.now()
}
export function readServerNowUnixMs(payload: unknown): number | null {
if (!payload || typeof payload !== 'object') return null
const value = (payload as { server_now_unix_ms?: unknown }).server_now_unix_ms
return typeof value === 'number' && Number.isFinite(value) ? value : null
function readHeaderValue(headers: unknown, name: string): unknown {
if (!headers || typeof headers !== 'object') return undefined
const get = (headers as { get?: unknown }).get
if (typeof get === 'function') {
return get.call(headers, name)
}
const lowerName = name.toLowerCase()
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
if (key.toLowerCase() === lowerName) return value
}
return undefined
}
export function readServerNowUnixMsFromHeaders(headers: unknown): number | null {
const value = readHeaderValue(headers, SERVER_NOW_UNIX_MS_HEADER)
const raw = Array.isArray(value) ? value[0] : value
const parsed = typeof raw === 'number'
? raw
: typeof raw === 'string'
? Number(raw.trim())
: Number.NaN
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
}
export function buildServerTimingMetadata(
payload: unknown,
response: Pick<AxiosResponse, 'headers'> | { headers?: unknown } | null | undefined,
clientSendUnixMs: number,
clientReceiveUnixMs = Date.now()
): ServerTimingMetadata | undefined {
const serverNowUnixMs = readServerNowUnixMs(payload)
const serverNowUnixMs = readServerNowUnixMsFromHeaders(response?.headers)
if (serverNowUnixMs == null) return undefined
if (!Number.isFinite(clientSendUnixMs) || !Number.isFinite(clientReceiveUnixMs)) return undefined
if (clientReceiveUnixMs < clientSendUnixMs) return undefined
const roundTripMs = clientReceiveUnixMs - clientSendUnixMs
return {
server_now_unix_ms: serverNowUnixMs,
client_send_unix_ms: clientSendUnixMs,
client_receive_unix_ms: clientReceiveUnixMs,
round_trip_ms: roundTripMs,
}
}
export function withServerTiming<T extends object>(payload: T, clientSendUnixMs: number): T & ServerTimedPayload {
const serverTiming = buildServerTimingMetadata(payload, clientSendUnixMs)
if (!serverTiming) return payload
export function withServerTiming<T extends object>(
response: Pick<AxiosResponse<T>, 'data' | 'headers'>,
clientSendUnixMs: number
): T & ServerTimedPayload {
const serverTiming = buildServerTimingMetadata(response, clientSendUnixMs)
if (!serverTiming) return response.data
return {
...payload,
...response.data,
server_timing: serverTiming,
}
}
+4 -5
View File
@@ -134,7 +134,6 @@ export interface UsageRequestOptions {
type UsageListResponse = ServerTimedPayload & {
records?: unknown
server_now_unix_ms?: unknown
pagination?: {
total?: unknown
limit?: unknown
@@ -379,7 +378,7 @@ export const usageApi = {
const { params, pagination } = buildCurrentUserUsageParams(filters)
const clientSendUnixMs = beginServerTimingSample()
const response = await apiClient.get<UsageListResponse>('/api/users/me/usage', { params })
return normalizeUsageRecordPage(withServerTiming(response.data, clientSendUnixMs), pagination)
return normalizeUsageRecordPage(withServerTiming(response, clientSendUnixMs), pagination)
},
async getUsageStats(filters?: UsageFilters, options?: UsageRequestOptions): Promise<UsageStats> {
@@ -462,7 +461,7 @@ export const usageApi = {
const recordsClientSendUnixMs = beginServerTimingSample()
const recordsRequest = apiClient
.get<UsageListResponse>('/api/admin/usage/records', { params: recordParams })
.then(response => withServerTiming(response.data, recordsClientSendUnixMs))
.then(response => withServerTiming(response, recordsClientSendUnixMs))
const [statsResponse, recordsResponse] = await Promise.all([
statsRequest,
@@ -503,7 +502,7 @@ export const usageApi = {
return dedupedRequest(key, async () => {
const clientSendUnixMs = beginServerTimingSample()
const response = await apiClient.get('/api/admin/usage/records', { params })
return withServerTiming(response.data, clientSendUnixMs)
return withServerTiming(response, clientSendUnixMs)
})
},
@@ -571,7 +570,7 @@ export const usageApi = {
}
const clientSendUnixMs = beginServerTimingSample()
const response = await apiClient.get('/api/admin/usage/active', { params })
return withServerTiming(response.data, clientSendUnixMs)
return withServerTiming(response, clientSendUnixMs)
},
/**