fix(web): show ai route fallback evidence
This commit is contained in:
@@ -253,7 +253,19 @@
|
|||||||
"humanGap": "Human gap",
|
"humanGap": "Human gap",
|
||||||
"humanGapDetail": "{gate} missing {count}",
|
"humanGapDetail": "{gate} missing {count}",
|
||||||
"modelRoute": "Model route",
|
"modelRoute": "Model route",
|
||||||
"routeDetail": "{model}; fallback {fallback}",
|
"routeDetail": "{model}; current {selected}; {primary}={primaryStatus}; fallback {fallback}",
|
||||||
|
"routeReasonSeparator": "; ",
|
||||||
|
"routeReason": "Reason: {reason}",
|
||||||
|
"routeErrorDetail": "Route check failed: {error}",
|
||||||
|
"routeNoFallback": "none",
|
||||||
|
"routeHealth": {
|
||||||
|
"healthy": "healthy",
|
||||||
|
"slow": "slow",
|
||||||
|
"degraded": "degraded",
|
||||||
|
"offline": "offline",
|
||||||
|
"not_checked": "standby",
|
||||||
|
"unknown": "unknown"
|
||||||
|
},
|
||||||
"topGap": "Largest current gap: {gate}, {count} items."
|
"topGap": "Largest current gap: {gate}, {count} items."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -254,7 +254,19 @@
|
|||||||
"humanGap": "人工缺口",
|
"humanGap": "人工缺口",
|
||||||
"humanGapDetail": "{gate} 缺 {count} 筆",
|
"humanGapDetail": "{gate} 缺 {count} 筆",
|
||||||
"modelRoute": "模型路由",
|
"modelRoute": "模型路由",
|
||||||
"routeDetail": "{model};備援 {fallback}",
|
"routeDetail": "{model};目前 {selected};{primary}={primaryStatus};備援 {fallback}",
|
||||||
|
"routeReasonSeparator": ";",
|
||||||
|
"routeReason": "原因:{reason}",
|
||||||
|
"routeErrorDetail": "路由檢查失敗:{error}",
|
||||||
|
"routeNoFallback": "無",
|
||||||
|
"routeHealth": {
|
||||||
|
"healthy": "健康",
|
||||||
|
"slow": "偏慢",
|
||||||
|
"degraded": "降級",
|
||||||
|
"offline": "離線",
|
||||||
|
"not_checked": "待命",
|
||||||
|
"unknown": "未知"
|
||||||
|
},
|
||||||
"topGap": "目前最大缺口:{gate},共 {count} 筆。"
|
"topGap": "目前最大缺口:{gate},共 {count} 筆。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,12 +79,27 @@ interface RunsResponse {
|
|||||||
items?: RunSummary[]
|
items?: RunSummary[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AiRoutePolicyItem {
|
||||||
|
provider_name: string
|
||||||
|
role?: string | null
|
||||||
|
runtime?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiRouteHealthItem {
|
||||||
|
status?: string | null
|
||||||
|
latency_ms?: number | null
|
||||||
|
reason?: string | null
|
||||||
|
checked?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface AiRouteStatusResponse {
|
interface AiRouteStatusResponse {
|
||||||
|
policy_order?: AiRoutePolicyItem[]
|
||||||
selected_provider?: string | null
|
selected_provider?: string | null
|
||||||
selected_model?: string | null
|
selected_model?: string | null
|
||||||
fallback_chain?: Array<{
|
fallback_chain?: AiRoutePolicyItem[]
|
||||||
provider_name: string
|
route_reason?: string | null
|
||||||
}>
|
route_error?: string | null
|
||||||
|
health?: Record<string, AiRouteHealthItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EvidenceSnapshot {
|
interface EvidenceSnapshot {
|
||||||
@@ -137,6 +152,42 @@ function gateLabelKey(gate?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeHealthLabelKey(status?: string | null) {
|
||||||
|
if (
|
||||||
|
status === 'healthy' ||
|
||||||
|
status === 'slow' ||
|
||||||
|
status === 'degraded' ||
|
||||||
|
status === 'offline' ||
|
||||||
|
status === 'not_checked'
|
||||||
|
) {
|
||||||
|
return `routeHealth.${status}`
|
||||||
|
}
|
||||||
|
return 'routeHealth.unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerDisplayName(provider?: string | null) {
|
||||||
|
switch (provider) {
|
||||||
|
case 'ollama_gcp_a':
|
||||||
|
return 'GCP-A'
|
||||||
|
case 'ollama_gcp_b':
|
||||||
|
return 'GCP-B'
|
||||||
|
case 'ollama_local':
|
||||||
|
return '111'
|
||||||
|
case 'gemini':
|
||||||
|
return 'Gemini'
|
||||||
|
default:
|
||||||
|
return provider || '--'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeTone(route: AiRouteStatusResponse | null): Tone {
|
||||||
|
if (!route?.selected_provider || route.route_error) return 'warn'
|
||||||
|
const primaryProvider = route.policy_order?.[0]?.provider_name
|
||||||
|
if (primaryProvider && route.selected_provider !== primaryProvider) return 'warn'
|
||||||
|
const primaryStatus = primaryProvider ? route.health?.[primaryProvider]?.status : null
|
||||||
|
return primaryStatus === 'healthy' ? 'good' : 'warn'
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJson<T>(path: string, signal: AbortSignal): Promise<T | null> {
|
async function fetchJson<T>(path: string, signal: AbortSignal): Promise<T | null> {
|
||||||
const response = await fetch(`${API_BASE}${path}`, { signal })
|
const response = await fetch(`${API_BASE}${path}`, { signal })
|
||||||
if (!response.ok) return null
|
if (!response.ok) return null
|
||||||
@@ -254,10 +305,26 @@ export function AutomationEvidenceCard() {
|
|||||||
|
|
||||||
const topGate = quality?.gate_failures?.[0]
|
const topGate = quality?.gate_failures?.[0]
|
||||||
const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair)
|
const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair)
|
||||||
const selectedProvider = snapshot?.route?.selected_provider ?? '--'
|
const route = snapshot?.route ?? null
|
||||||
const fallback = snapshot?.route?.fallback_chain
|
const primaryProvider = route?.policy_order?.[0]?.provider_name ?? null
|
||||||
|
const primaryStatus = primaryProvider ? route?.health?.[primaryProvider]?.status : null
|
||||||
|
const selectedProvider = providerDisplayName(route?.selected_provider)
|
||||||
|
const fallback = route?.fallback_chain
|
||||||
?.map((item) => item.provider_name)
|
?.map((item) => item.provider_name)
|
||||||
|
.map(providerDisplayName)
|
||||||
.join(' -> ')
|
.join(' -> ')
|
||||||
|
const routeSummary = route?.route_error
|
||||||
|
? t('routeErrorDetail', { error: route.route_error })
|
||||||
|
: t('routeDetail', {
|
||||||
|
model: route?.selected_model ?? '--',
|
||||||
|
selected: selectedProvider,
|
||||||
|
primary: providerDisplayName(primaryProvider),
|
||||||
|
primaryStatus: t(routeHealthLabelKey(primaryStatus) as never),
|
||||||
|
fallback: fallback || t('routeNoFallback'),
|
||||||
|
})
|
||||||
|
const routeDetail = route?.route_reason && !route.route_error
|
||||||
|
? `${routeSummary}${t('routeReasonSeparator')}${t('routeReason', { reason: route.route_reason })}`
|
||||||
|
: routeSummary
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quality,
|
quality,
|
||||||
@@ -268,8 +335,10 @@ export function AutomationEvidenceCard() {
|
|||||||
claimReady,
|
claimReady,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
fallback,
|
fallback,
|
||||||
|
routeDetail,
|
||||||
|
routeTone: routeTone(route),
|
||||||
}
|
}
|
||||||
}, [snapshot])
|
}, [snapshot, t])
|
||||||
|
|
||||||
const hasData = Boolean(snapshot?.quality || snapshot?.coverage || snapshot?.recurrence)
|
const hasData = Boolean(snapshot?.quality || snapshot?.coverage || snapshot?.recurrence)
|
||||||
|
|
||||||
@@ -365,11 +434,8 @@ export function AutomationEvidenceCard() {
|
|||||||
<EvidenceMetric
|
<EvidenceMetric
|
||||||
label={t('modelRoute')}
|
label={t('modelRoute')}
|
||||||
value={derived.selectedProvider}
|
value={derived.selectedProvider}
|
||||||
detail={t('routeDetail', {
|
detail={derived.routeDetail}
|
||||||
model: snapshot?.route?.selected_model ?? '--',
|
tone={derived.routeTone}
|
||||||
fallback: derived.fallback || '--',
|
|
||||||
})}
|
|
||||||
tone="neutral"
|
|
||||||
icon={Route}
|
icon={Route}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user