feat(health): expose ollama provider chain
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* AIModelStatus — AI 模型狀態 2×2 網格
|
||||
* AIModelStatus — AI provider route health grid
|
||||
* Sprint 5R S9: 設計稿 L531-545
|
||||
* @created 2026-04-09 Claude Opus 4.6 Asia/Taipei
|
||||
*/
|
||||
@@ -13,33 +13,66 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
interface ModelInfo {
|
||||
name: string
|
||||
tag: string
|
||||
healthy: boolean
|
||||
role: 'primary' | 'backup' | 'local' | 'agent' | 'provider'
|
||||
status: 'up' | 'down' | 'degraded' | 'unknown'
|
||||
latencyMs?: number | null
|
||||
}
|
||||
|
||||
interface HealthComponent {
|
||||
status?: 'up' | 'down' | 'degraded'
|
||||
latency_ms?: number | null
|
||||
}
|
||||
|
||||
interface HealthResponse {
|
||||
components?: Record<string, HealthComponent>
|
||||
ollama_route_order?: string[]
|
||||
}
|
||||
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
ollama_gcp_a: 'GCP-A',
|
||||
ollama_gcp_b: 'GCP-B',
|
||||
ollama_local: '111',
|
||||
openclaw: 'OpenClaw',
|
||||
}
|
||||
|
||||
const PROVIDER_ROLES: Record<string, ModelInfo['role']> = {
|
||||
ollama_gcp_a: 'primary',
|
||||
ollama_gcp_b: 'backup',
|
||||
ollama_local: 'local',
|
||||
openclaw: 'agent',
|
||||
}
|
||||
|
||||
function statusColor(status: ModelInfo['status']) {
|
||||
if (status === 'up') return '#22C55E'
|
||||
if (status === 'degraded') return '#F59E0B'
|
||||
if (status === 'down') return '#cc2200'
|
||||
return '#87867f'
|
||||
}
|
||||
|
||||
export function AIModelStatus() {
|
||||
const t = useTranslations('dashboard')
|
||||
const [models, setModels] = useState<ModelInfo[]>([
|
||||
{ name: 'OpenClaw Nemo', tag: 'local', healthy: false },
|
||||
{ name: 'Ollama gemma3', tag: 'local', healthy: false },
|
||||
{ name: 'Gemini Pro', tag: 'cloud', healthy: false },
|
||||
{ name: 'NVIDIA NIM', tag: 'cloud', healthy: false },
|
||||
{ name: 'GCP-A', role: 'primary', status: 'unknown' },
|
||||
{ name: 'GCP-B', role: 'backup', status: 'unknown' },
|
||||
{ name: '111', role: 'local', status: 'unknown' },
|
||||
{ name: 'OpenClaw', role: 'agent', status: 'unknown' },
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/api/v1/health`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
.then((d: HealthResponse | null) => {
|
||||
if (!d?.components) return
|
||||
setModels(prev => prev.map(m => {
|
||||
if (m.name.includes('OpenClaw') && d.components.openclaw) return { ...m, healthy: d.components.openclaw.status === 'up' }
|
||||
if (m.name.includes('Ollama') && d.components.ollama) return { ...m, healthy: d.components.ollama.status === 'up' }
|
||||
// 2026-04-09 Claude Sonnet 4.6: 移除假數據 — /api/v1/health 無 gemini/nvidia component
|
||||
// cloud 模型狀態未知,保持 false,不顯示假綠燈
|
||||
if (m.name.includes('Gemini') && d.components.gemini) return { ...m, healthy: d.components.gemini.status === 'up' }
|
||||
if (m.name.includes('NVIDIA') && d.components.nvidia) return { ...m, healthy: d.components.nvidia.status === 'up' }
|
||||
return m
|
||||
}))
|
||||
const routeOrder = d.ollama_route_order?.length
|
||||
? d.ollama_route_order
|
||||
: ['ollama_gcp_a', 'ollama_gcp_b', 'ollama_local']
|
||||
const providerKeys = [...routeOrder, 'openclaw']
|
||||
setModels(providerKeys.map(key => ({
|
||||
name: PROVIDER_LABELS[key] ?? key,
|
||||
role: PROVIDER_ROLES[key] ?? 'provider',
|
||||
status: d.components?.[key]?.status ?? 'unknown',
|
||||
latencyMs: d.components?.[key]?.latency_ms,
|
||||
})))
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
@@ -62,9 +95,13 @@ export function AIModelStatus() {
|
||||
border: '0.5px solid #e0ddd4', borderRadius: 6, padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: m.healthy ? '#22C55E' : '#cc2200', flexShrink: 0 }} />
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: statusColor(m.status), flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 500, color: '#141413' }}>{m.name}</span>
|
||||
<span style={{ fontSize: 10, color: '#87867f', marginLeft: 'auto' }}>{m.tag}</span>
|
||||
<span style={{ fontSize: 10, color: '#87867f', marginLeft: 'auto' }}>
|
||||
{typeof m.latencyMs === 'number'
|
||||
? `${Math.round(m.latencyMs)}ms`
|
||||
: t(`aiModelRoles.${m.role}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user