feat(health): add model and provider health monitoring

- Rename the original health monitor to endpoint health monitor
- Add tab navigation for endpoint, model, and provider health views
- Add model health monitor cards with availability, latency, first-byte latency, and 60-point history
- Add admin-only provider health monitor with collapsible active-provider sections
- Show per-provider model health cards after expanding a provider
- Add backend model health and provider health monitor payload builders
- Add admin endpoint for provider health monitoring
- Add provider-scoped usage breakdown filtering for per-provider model statistics
- Add frontend API types and request helpers for model/provider health data
- Add demo mock data for model and provider health monitoring
- Fix model health timeline time-unit handling so request history segments render correctly

Verification:
- cargo fmt
- npm run type-check
- npm run build
- cargo test -p aether-gateway health_models
- cargo test -p aether-gateway health_providers
- cargo test -p aether-gateway gateway_exposes_frontdoor_manifest_without_proxying_upstream
This commit is contained in:
AAEE86
2026-05-28 12:00:20 +08:00
parent 14ad6e9b75
commit 4ce056fe45
24 changed files with 1914 additions and 17 deletions
+196
View File
@@ -139,6 +139,20 @@ function generateHealthEvents(
return events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
}
function generateHealthTimeline(
healthyRate: number,
warningRate: number,
segments = 60
) {
return Array.from({ length: segments }, () => {
const rand = Math.random()
if (rand < healthyRate) return 'healthy'
if (rand < healthyRate + warningRate) return 'warning'
if (rand < 0.96) return 'unknown'
return 'unhealthy'
})
}
// Mock 端点健康数据
// 注意:success_rate 使用 0-1 之间的小数,前端会乘以 100 显示为百分比
// 事件的成功/失败/跳过比例必须与 success_rate 保持一致
@@ -246,6 +260,154 @@ const MOCK_ENDPOINT_STATUS = {
]
}
const MOCK_MODEL_STATUS = {
generated_at: new Date().toISOString(),
models: [
{
model: 'gpt-5.5',
display_name: 'gpt-5.5',
total_attempts: 2021,
success_count: 2000,
failed_count: 21,
success_rate: 0.9896,
avg_latency_ms: 1736,
avg_first_byte_ms: 176,
provider_count: 3,
last_event_at: new Date().toISOString(),
events: generateHealthEvents(60, 0.989, 0.008, 0.003, 1600, 460),
timeline: generateHealthTimeline(0.9, 0.05),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString()
},
{
model: 'claude-sonnet-4-5-20250929',
display_name: 'Claude Sonnet 4.5',
total_attempts: 1684,
success_count: 1642,
failed_count: 42,
success_rate: 0.9751,
avg_latency_ms: 1280,
avg_first_byte_ms: 221,
provider_count: 2,
last_event_at: new Date().toISOString(),
events: generateHealthEvents(60, 0.975, 0.02, 0.005, 1200, 520),
timeline: generateHealthTimeline(0.84, 0.09),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString()
},
{
model: 'gemini-3-pro-preview',
display_name: 'Gemini 3 Pro Preview',
total_attempts: 932,
success_count: 887,
failed_count: 45,
success_rate: 0.9517,
avg_latency_ms: 940,
avg_first_byte_ms: 184,
provider_count: 2,
last_event_at: new Date().toISOString(),
events: generateHealthEvents(55, 0.952, 0.04, 0.008, 860, 300),
timeline: generateHealthTimeline(0.78, 0.14),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString()
},
{
model: 'gpt-5.1-codex-mini',
display_name: 'gpt-5.1-codex-mini',
total_attempts: 418,
success_count: 349,
failed_count: 69,
success_rate: 0.835,
avg_latency_ms: 2310,
avg_first_byte_ms: 420,
provider_count: 1,
last_event_at: new Date().toISOString(),
events: generateHealthEvents(45, 0.835, 0.145, 0.02, 2200, 780),
timeline: generateHealthTimeline(0.58, 0.24),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString()
}
]
}
const MOCK_PROVIDER_HEALTH_STATUS = {
generated_at: new Date().toISOString(),
providers: [
{
provider_id: 'provider-001',
provider_name: 'OpenAI Official',
provider_type: 'codex',
is_active: true,
total_attempts: 2021,
success_count: 2000,
failed_count: 21,
success_rate: 0.9896,
avg_latency_ms: 1736,
avg_first_byte_ms: 176,
model_count: 2,
last_event_at: new Date().toISOString(),
timeline: generateHealthTimeline(0.9, 0.05),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString(),
models: [MOCK_MODEL_STATUS.models[0], MOCK_MODEL_STATUS.models[3]]
},
{
provider_id: 'provider-002',
provider_name: 'Anthropic Official',
provider_type: 'claude_code',
is_active: true,
total_attempts: 1684,
success_count: 1642,
failed_count: 42,
success_rate: 0.9751,
avg_latency_ms: 1280,
avg_first_byte_ms: 221,
model_count: 1,
last_event_at: new Date().toISOString(),
timeline: generateHealthTimeline(0.84, 0.09),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString(),
models: [MOCK_MODEL_STATUS.models[1]]
},
{
provider_id: 'provider-003',
provider_name: 'Google AI',
provider_type: 'gemini_cli',
is_active: true,
total_attempts: 932,
success_count: 887,
failed_count: 45,
success_rate: 0.9517,
avg_latency_ms: 940,
avg_first_byte_ms: 184,
model_count: 1,
last_event_at: new Date().toISOString(),
timeline: generateHealthTimeline(0.78, 0.14),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString(),
models: [MOCK_MODEL_STATUS.models[2]]
},
{
provider_id: 'provider-004',
provider_name: 'AWS Bedrock',
provider_type: 'custom',
is_active: true,
total_attempts: 0,
success_count: 0,
failed_count: 0,
success_rate: 1,
avg_latency_ms: null,
avg_first_byte_ms: null,
model_count: 0,
last_event_at: null,
timeline: Array.from({ length: 60 }, () => 'unknown'),
time_range_start: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
time_range_end: new Date().toISOString(),
models: []
}
]
}
// 生成活跃热力图数据(最近365天)
function generateActivityHeatmap() {
const days: Array<{
@@ -1026,6 +1188,18 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
return createMockResponse(MOCK_ENDPOINT_STATUS)
},
'GET /api/admin/endpoints/health/models': async () => {
await delay()
requireAdmin()
return createMockResponse(MOCK_MODEL_STATUS)
},
'GET /api/admin/endpoints/health/providers': async () => {
await delay()
requireAdmin()
return createMockResponse(MOCK_PROVIDER_HEALTH_STATUS)
},
'GET /api/admin/endpoints/keys': async () => {
await delay()
requireAdmin()
@@ -1387,6 +1561,28 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
events: f.events.slice(0, 10)
}))
})
},
'GET /api/public/health/models': async () => {
await delay()
return createMockResponse({
generated_at: new Date().toISOString(),
models: MOCK_MODEL_STATUS.models.map(model => ({
model: model.model,
display_name: model.display_name,
total_attempts: model.total_attempts,
success_count: model.success_count,
failed_count: model.failed_count,
success_rate: model.success_rate,
avg_latency_ms: model.avg_latency_ms,
avg_first_byte_ms: model.avg_first_byte_ms,
last_event_at: model.last_event_at,
events: model.events.slice(0, 10),
timeline: model.timeline,
time_range_start: model.time_range_start,
time_range_end: model.time_range_end
}))
})
}
}