feat(proxy): record tunnel stability metrics

This commit is contained in:
fawney19
2026-05-08 22:03:17 +08:00
parent 9a84a6ff6c
commit a703acd1fe
81 changed files with 7229 additions and 1110 deletions

View File

@@ -215,31 +215,12 @@
</span>
</div>
<div
v-if="currentAttemptSchedulerInfo"
v-if="currentAttemptRequestPathDisplay"
class="info-item"
>
<span class="info-label">调度顺位</span>
<span class="info-value info-value-stacked">
<code class="format-code">
全局 {{ currentAttemptSchedulerInfo.globalPriorityLabel }}
/ Provider {{ currentAttemptSchedulerInfo.providerPriorityLabel }}
/ Key {{ currentAttemptSchedulerInfo.keyPriorityLabel }}
</code>
<span class="text-xs text-muted-foreground">
{{ currentAttemptSchedulerInfo.hint }}
</span>
</span>
</div>
<div
v-if="currentAttemptRankingInfo"
class="info-item"
>
<span class="info-label">排序原因</span>
<span class="info-value info-value-stacked">
<code class="format-code">{{ currentAttemptRankingInfo.summary }}</code>
<span class="text-xs text-muted-foreground">
{{ currentAttemptRankingInfo.hint }}
</span>
<span class="info-label">请求路径</span>
<span class="info-value">
<code class="format-code request-path-code">{{ currentAttemptRequestPathDisplay }}</code>
</span>
</div>
<div
@@ -269,19 +250,7 @@
<span class="info-value info-value-stacked">
<code class="format-code">{{ currentAttemptKeyFormatsDisplay }}</code>
<span class="text-xs text-muted-foreground">
同一 Key 的不同 endpoint 会分别参与候选与转换判定
</span>
</span>
</div>
<div
v-if="currentAttemptConversionInfo"
class="info-item"
>
<span class="info-label">转换策略</span>
<span class="info-value info-value-stacked">
<code class="format-code">{{ currentAttemptConversionInfo.summary }}</code>
<span class="text-xs text-muted-foreground">
{{ currentAttemptConversionInfo.hint }}
Key 声明的可用 endpoint 格式
</span>
</span>
</div>
@@ -1181,59 +1150,11 @@ const extractStringList = (value: unknown): string[] => {
return []
}
const normalizePriorityNumber = (value: unknown): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return Math.trunc(value)
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return Math.trunc(parsed)
}
}
return null
}
const resolveClientApiFormat = (attempt: CandidateRecord): string => {
const extra = (
attempt.extra_data && typeof attempt.extra_data === 'object' && !Array.isArray(attempt.extra_data)
? attempt.extra_data
: {}
) as Record<string, unknown>
const fromExtra = typeof extra.client_api_format === 'string' ? extra.client_api_format.trim() : ''
if (fromExtra) return fromExtra
if (typeof props.requestApiFormat === 'string' && props.requestApiFormat.trim()) {
return props.requestApiFormat.trim()
}
return ''
}
const resolveProviderApiFormat = (attempt: CandidateRecord): string => {
const extra = (
attempt.extra_data && typeof attempt.extra_data === 'object' && !Array.isArray(attempt.extra_data)
? attempt.extra_data
: {}
) as Record<string, unknown>
const fromExtra = typeof extra.provider_api_format === 'string' ? extra.provider_api_format.trim() : ''
if (fromExtra) return fromExtra
if (typeof attempt.endpoint_name === 'string' && attempt.endpoint_name.trim()) {
return attempt.endpoint_name.trim()
}
return ''
}
const resolveTransportDiagnostics = (attempt: CandidateRecord): Record<string, unknown> | null => {
const extra = extractObject(attempt.extra_data)
return extractObject(extra?.transport_diagnostics)
}
const resolveEndpointFormatAcceptanceConfig = (attempt: CandidateRecord): Record<string, unknown> | null => {
const fromAttempt = extractObject(attempt.endpoint_format_acceptance_config)
if (fromAttempt) return fromAttempt
const transport = resolveTransportDiagnostics(attempt)
return extractObject(transport?.endpoint_format_acceptance_config)
}
const currentAttemptFormatDisplay = computed(() => {
const attempt = currentAttempt.value
if (!attempt) return ''
@@ -1261,120 +1182,52 @@ const currentAttemptFormatDisplay = computed(() => {
return providerText || requestText
})
const currentAttemptSchedulerInfo = computed<{
globalPriorityLabel: string
providerPriorityLabel: string
keyPriorityLabel: string
hint: string
} | null>(() => {
const attempt = currentAttempt.value
if (!attempt) return null
const normalizeQueryString = (value: string): string => {
const trimmed = value.trim()
if (!trimmed) return ''
return trimmed.startsWith('?') ? trimmed.slice(1) : trimmed
}
const clientApiFormat = resolveClientApiFormat(attempt)
const providerApiFormat = resolveProviderApiFormat(attempt)
const providerPriority = normalizePriorityNumber(attempt.provider_priority)
const keyInternalPriority = normalizePriorityNumber(attempt.key_internal_priority)
const resolveRequestPathFromObject = (value: unknown): string => {
const object = extractObject(value)
if (!object) return ''
let globalPriority: number | null = null
const globalPriorityMap = attempt.key_global_priority_by_format
if (globalPriorityMap && typeof globalPriorityMap === 'object' && !Array.isArray(globalPriorityMap) && clientApiFormat) {
const match = Object.entries(globalPriorityMap).find(([format]) => (
normalizeFormatSignature(format) === normalizeFormatSignature(clientApiFormat)
))
globalPriority = match ? normalizePriorityNumber(match[1]) : null
}
const isCrossFormat = Boolean(
clientApiFormat &&
providerApiFormat &&
normalizeFormatSignature(clientApiFormat) !== normalizeFormatSignature(providerApiFormat),
const pathWithQuery = (
readStringField(object, 'request_path_and_query')
|| readStringField(object, 'public_path_and_query')
|| readStringField(object, 'path_and_query')
|| readStringField(object, 'request_uri')
|| readStringField(object, 'public_uri')
)
const keepPriorityOnConversion = attempt.provider_keep_priority_on_conversion === true
if (pathWithQuery) return pathWithQuery
let hint = '顺位展示 Provider / Key 优先级;不同 endpoint 独立参与候选'
if (globalPriority !== null) {
hint = `当前格式 ${formatApiFormat(clientApiFormat)} 先看全局 Key 优先级;不同 endpoint 单独判定`
}
if (isCrossFormat) {
hint = keepPriorityOnConversion
? '跨格式候选已开启保持优先级;同一 Key 的不同 endpoint 独立参与'
: '跨格式候选默认排在同格式候选之后;同一 Key 的不同 endpoint 独立参与'
}
const path = (
readStringField(object, 'request_path')
|| readStringField(object, 'public_path')
|| readStringField(object, 'path')
)
if (!path) return ''
return {
globalPriorityLabel: globalPriority !== null ? String(globalPriority) : '-',
providerPriorityLabel: providerPriority !== null ? String(providerPriority) : '-',
keyPriorityLabel: keyInternalPriority !== null ? String(keyInternalPriority) : '-',
hint,
}
})
const normalizeMetadataText = (value: unknown): string => {
return typeof value === 'string' ? value.trim() : ''
const query = normalizeQueryString(
readStringField(object, 'request_query_string')
|| readStringField(object, 'public_query_string')
|| readStringField(object, 'query_string')
|| readStringField(object, 'query')
|| '',
)
if (!query || path.includes('?')) return path
return `${path}?${query}`
}
const formatRankingModeLabel = (value: string): string => {
const normalized = value.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase()
const labels: Record<string, string> = {
fixed_order: '固定顺序',
cache_affinity: '亲和性优先',
load_balance: '负载均衡',
}
return labels[normalized] || value
}
const formatPriorityModeLabel = (value: string): string => {
const normalized = value.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase()
const labels: Record<string, string> = {
provider: 'Provider 优先级',
global_key: '全局 Key 优先级',
}
return labels[normalized] || value
}
const formatRankingReasonLabel = (value: string): string => {
const labels: Record<string, string> = {
cached_affinity: '缓存亲和性命中',
local_tunnel: '本地隧道优先',
cross_format: '跨格式降级',
}
return labels[value] || value
}
const currentAttemptRankingInfo = computed<{
summary: string
hint: string
} | null>(() => {
const currentAttemptRequestPathDisplay = computed(() => {
const attempt = currentAttempt.value
if (!attempt) return null
const ranking = extractObject(attempt.ranking)
const extra = extractObject(attempt.extra_data)
if (!ranking && !extra) return null
const fromAttempt = resolveRequestPathFromObject(attempt?.extra_data)
if (fromAttempt) return fromAttempt
const rankingMode = normalizeMetadataText(ranking?.mode ?? extra?.ranking_mode)
const priorityMode = normalizeMetadataText(ranking?.priority_mode ?? extra?.priority_mode)
const promotedBy = normalizeMetadataText(ranking?.promoted_by ?? extra?.promoted_by)
const demotedBy = normalizeMetadataText(ranking?.demoted_by ?? extra?.demoted_by)
const rankingIndex = normalizePriorityNumber(ranking?.index ?? extra?.ranking_index)
const prioritySlot = normalizePriorityNumber(ranking?.priority_slot ?? extra?.priority_slot)
if (!rankingMode && !priorityMode && !promotedBy && !demotedBy && rankingIndex === null && prioritySlot === null) {
return null
}
const fromRequestMetadata = resolveRequestPathFromObject(props.requestMetadata)
if (fromRequestMetadata) return fromRequestMetadata
const summaryParts: string[] = []
if (rankingMode) summaryParts.push(formatRankingModeLabel(rankingMode))
if (promotedBy) summaryParts.push(formatRankingReasonLabel(promotedBy))
if (demotedBy) summaryParts.push(formatRankingReasonLabel(demotedBy))
const hintParts: string[] = []
if (rankingIndex !== null) hintParts.push(`排序 #${rankingIndex + 1}`)
if (priorityMode) hintParts.push(formatPriorityModeLabel(priorityMode))
if (prioritySlot !== null) hintParts.push(`槽位 ${prioritySlot}`)
return {
summary: summaryParts.length > 0 ? summaryParts.join(' / ') : '排序元数据',
hint: hintParts.length > 0 ? hintParts.join(' · ') : '候选排序由 scheduler ranking engine 生成',
}
return ''
})
const currentAttemptKeyFormatsDisplay = computed(() => {
@@ -1388,66 +1241,6 @@ const currentAttemptKeyFormatsDisplay = computed(() => {
.map(format => formatApiFormat(format))
.join(' / ')
})
const currentAttemptConversionInfo = computed<{
summary: string
hint: string
} | null>(() => {
const attempt = currentAttempt.value
if (!attempt) return null
const clientApiFormat = resolveClientApiFormat(attempt)
const providerApiFormat = resolveProviderApiFormat(attempt)
if (!clientApiFormat || !providerApiFormat) return null
const isCrossFormat = normalizeFormatSignature(clientApiFormat) !== normalizeFormatSignature(providerApiFormat)
if (!isCrossFormat) {
return {
summary: '同格式直连',
hint: '当前候选直接命中 endpoint 原生格式',
}
}
const transportDiagnostics = resolveTransportDiagnostics(attempt)
const providerEnabled = attempt.provider_enable_format_conversion === true
|| transportDiagnostics?.provider_enable_format_conversion === true
const endpointConfig = resolveEndpointFormatAcceptanceConfig(attempt)
const endpointRuleEnabled = endpointConfig
? endpointConfig.enabled !== false
: false
const acceptFormats = extractStringList(endpointConfig?.accept_formats)
const rejectFormats = extractStringList(endpointConfig?.reject_formats)
const normalizedClientFormat = normalizeFormatSignature(clientApiFormat)
const endpointAcceptsClient = acceptFormats.some(
format => normalizeFormatSignature(format) === normalizedClientFormat,
)
const endpointRejectsClient = rejectFormats.some(
format => normalizeFormatSignature(format) === normalizedClientFormat,
)
let summary = '未开启格式转换'
if (providerEnabled && endpointRuleEnabled) {
summary = 'Provider 总开关 + Endpoint 规则'
} else if (providerEnabled) {
summary = 'Provider 总格式转换'
} else if (endpointRuleEnabled) {
summary = 'Endpoint 独立格式转换'
}
let hint = '同一 Key 的不同 endpoint 会分别判定格式转换'
if (endpointRejectsClient) {
hint = `当前 endpoint 明确拒绝 ${formatApiFormat(clientApiFormat)}`
} else if (endpointAcceptsClient) {
hint = `当前 endpoint 明确接受 ${formatApiFormat(clientApiFormat)}`
} else if (providerEnabled) {
hint = '当前跨格式由 Provider 总开关放行'
} else if (attempt.skip_reason === 'format_conversion_disabled') {
hint = 'Provider 总开关关闭,且当前 endpoint 未单独放行'
}
return { summary, hint }
})
const currentAttemptSkipReasonDisplay = computed(() => {
const attempt = currentAttempt.value
if (!attempt?.skip_reason) return ''
@@ -1870,8 +1663,16 @@ const getStatusColorClass = (status: string) => {
// 展示状态:进行中态优先(包括 started 但未 finished 的中间态),再按 HTTP 状态码兜底
function getDisplayStatus(attempt: CandidateRecord | null | undefined): string {
if (!attempt) return 'available'
const code = attempt.status_code
const isTerminalSuccessCode = typeof code === 'number' && code >= 200 && code < 300
if (attempt.status === 'success') {
if (typeof code === 'number' && !isTerminalSuccessCode) {
return 'failed'
}
return 'success'
}
if (
attempt.status === 'success' ||
attempt.status === 'failed' ||
attempt.status === 'cancelled' ||
attempt.status === 'skipped' ||
@@ -1890,10 +1691,9 @@ function getDisplayStatus(attempt: CandidateRecord | null | undefined): string {
if (isExplicitPending || isImplicitPending) {
return 'pending'
}
const code = attempt.status_code
if (typeof code === 'number') {
if (code >= 200 && code < 300) return 'success'
if (code >= 400) return 'failed'
if (isTerminalSuccessCode) return 'success'
if (code >= 300) return 'failed'
}
return attempt.status
}

View File

@@ -99,12 +99,16 @@ function buildTrace(candidates: CandidateRecord[]): RequestTrace {
}
}
function mountTimeline(traceData: RequestTrace) {
function mountTimeline(
traceData: RequestTrace,
extraProps: Record<string, unknown> = {},
) {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(HorizontalRequestTimeline, {
requestId: traceData.request_id,
traceData,
...extraProps,
})
app.mount(root)
mountedApps.push({ app, root })
@@ -269,4 +273,52 @@ describe('HorizontalRequestTimeline', () => {
expect(nodeDots[0].classList.contains('status-success')).toBe(false)
expect(nodeDots[1].classList.contains('status-success')).toBe(true)
})
it('treats 3xx terminal responses as failed for node display', async () => {
const trace = buildTrace([
buildCandidate({
id: 'cand-redirect',
provider_id: 'provider-redirect',
provider_name: 'Provider Redirect',
key_id: 'key-redirect',
key_name: 'Redirect Key',
candidate_index: 0,
status: 'success',
status_code: 302,
}),
])
const root = mountTimeline(trace)
await nextTick()
const nodeDot = root.querySelector<HTMLElement>('.node-dot')
expect(nodeDot?.classList.contains('status-failed')).toBe(true)
expect(nodeDot?.classList.contains('status-success')).toBe(false)
})
it('shows request path from request metadata', async () => {
const trace = buildTrace([
buildCandidate({
id: 'cand-path',
provider_id: 'provider-path',
provider_name: 'Provider Path',
key_id: 'key-path',
key_name: 'Path Key',
candidate_index: 0,
status: 'failed',
}),
])
const root = mountTimeline(trace, {
requestMetadata: {
request_path: '/v1beta/models/gemini-2.5-pro:generateContent',
request_query_string: 'alt=sse',
},
})
await nextTick()
expect(root.textContent).toContain('请求路径')
const requestPathCode = root.querySelector<HTMLElement>('.request-path-code')
expect(requestPathCode?.textContent).toContain('/v1beta/models/gemini-2.5-pro:generateContent?alt=sse')
})
})

View File

@@ -106,6 +106,14 @@ describe('usage status helpers', () => {
})).toBe('failed')
})
it('downgrades terminal success to failed when status code is 3xx', () => {
expect(resolveTimelineFinalStatus({
traceFinalStatus: 'success',
requestStatus: 'completed',
statusCode: 302,
})).toBe('failed')
})
it('falls back to request lifecycle status when status code and trace are missing', () => {
expect(resolveTimelineFinalStatus({
requestStatus: 'failed',
@@ -189,6 +197,9 @@ describe('usage status helpers', () => {
expect(resolveTimelineFinalStatus({
statusCode: 200,
})).toBe('success')
expect(resolveTimelineFinalStatus({
statusCode: 302,
})).toBe('failed')
expect(resolveTimelineFinalStatus({
statusCode: 503,
})).toBe('failed')

View File

@@ -157,7 +157,7 @@ function hasTerminalSuccessStatusCode(
): boolean {
return typeof record.status_code === 'number' &&
record.status_code >= 200 &&
record.status_code < 400
record.status_code < 300
}
export function isUsageRecordFailed(
@@ -272,18 +272,28 @@ export function resolveTimelineFinalStatus(params: {
requestStatus?: RequestStatusLike
statusCode?: number
}): TimelineFinalStatus {
const hasTerminalSuccessStatusCode = typeof params.statusCode === 'number'
? params.statusCode >= 200 && params.statusCode < 300
: undefined
const requestStatus = mapRequestStatusToTimelineStatus(params.requestStatus)
if (requestStatus === 'success' || requestStatus === 'failed' || requestStatus === 'cancelled') {
if (requestStatus === 'success' && hasTerminalSuccessStatusCode === false) {
return 'failed'
}
return requestStatus
}
const traceStatus = normalizeTimelineFinalStatus(params.traceFinalStatus)
if (traceStatus === 'success' || traceStatus === 'failed' || traceStatus === 'cancelled') {
if (traceStatus === 'success' && hasTerminalSuccessStatusCode === false) {
return 'failed'
}
return traceStatus
}
if (typeof params.statusCode === 'number') {
return params.statusCode >= 200 && params.statusCode < 400 ? 'success' : 'failed'
if (hasTerminalSuccessStatusCode !== undefined) {
return hasTerminalSuccessStatusCode ? 'success' : 'failed'
}
if (params.hasPendingCandidates) {