mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
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:
@@ -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
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user