mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 14:10:19 +08:00
Use header-only usage server timing
This commit is contained in:
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
// 获取可用的提供商
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user