feat: update gateway pool and usage flows

This commit is contained in:
fawney19
2026-05-12 21:05:11 +08:00
parent 38012c62ff
commit d1a47c068e
18 changed files with 582 additions and 328 deletions

View File

@@ -662,19 +662,6 @@ const formatNumber = (num: number): string => {
return num.toLocaleString('zh-CN')
}
// 计算最终状态:优先检查进行中状态,再使用外部状态码
const computedFinalStatus = computed(() => {
const hasPending = trace.value?.candidates?.some(
c => c.status === 'pending' || c.status === 'streaming'
)
return resolveTimelineFinalStatus({
hasPendingCandidates: hasPending,
statusCode: props.overrideStatusCode,
requestStatus: props.requestStatus ?? usageData.value?.status,
traceFinalStatus: trace.value?.final_status,
})
})
// 获取最终状态标签
const getFinalStatusLabel = (status: string) => {
const labels: Record<string, string> = {
@@ -806,11 +793,25 @@ const STATUS_PRIORITY: Record<string, number> = {
}
const isParticipatedCandidate = (candidate: CandidateRecord): boolean => {
if (candidate.status === 'available' || candidate.status === 'unused') return false
if (candidate.status === 'pending' && !candidate.started_at) return false
return true
return TIMELINE_STATUS.includes(candidate.status)
}
const isLiveCandidate = (candidate: CandidateRecord): boolean => {
if (candidate.status === 'streaming') return true
return candidate.status === 'pending' && Boolean(candidate.started_at)
}
// 计算最终状态:优先检查真正已启动的进行中状态,再使用外部状态码
const computedFinalStatus = computed(() => {
const hasPending = trace.value?.candidates?.some(isLiveCandidate)
return resolveTimelineFinalStatus({
hasPendingCandidates: hasPending,
statusCode: props.overrideStatusCode,
requestStatus: props.requestStatus ?? usageData.value?.status,
traceFinalStatus: trace.value?.final_status,
})
})
const compareBySchedulingOrder = (a: CandidateRecord, b: CandidateRecord): number => {
if (a.candidate_index !== b.candidate_index) {
return a.candidate_index - b.candidate_index
@@ -1695,10 +1696,7 @@ const propsRequestIsActive = computed(() => {
})
const traceHasActiveCandidate = computed(() => {
return rawTimeline.value.some((candidate) => {
const status = getDisplayStatus(candidate)
return status === 'pending' || status === 'streaming'
})
return rawTimeline.value.some(isLiveCandidate)
})
const traceFinalIsTerminal = computed(() => {
@@ -1751,15 +1749,12 @@ watch(groupedTimeline, (newGroups) => {
}
// 查找正在进行的组
const activeIdx = newGroups.findIndex(g => g.primaryStatus === 'pending' || g.primaryStatus === 'streaming')
const activeIdx = newGroups.findIndex(g => g.allAttempts.some(isLiveCandidate))
if (activeIdx >= 0) {
selectedGroupIndex.value = activeIdx
// 选中正在进行的尝试,而非最后一个
const group = newGroups[activeIdx]
const attemptIdx = group.allAttempts.findIndex(a => {
const status = getDisplayStatus(a)
return status === 'pending' || status === 'streaming'
})
const attemptIdx = group.allAttempts.findIndex(isLiveCandidate)
selectedAttemptIndex.value = attemptIdx >= 0 ? attemptIdx : group.allAttempts.length - 1
return
}
@@ -1853,8 +1848,8 @@ const formatDuration = (startStr: string, endStr: string): string => {
// 获取状态标签
const getStatusLabel = (status: string) => {
const labels: Record<string, string> = {
available: '未执行',
unused: '未执行',
available: '可用未尝试',
unused: '未使用',
pending: '进行中',
streaming: '传输中',
stream_interrupted: '流中断',

View File

@@ -180,7 +180,7 @@ describe('HorizontalRequestTimeline', () => {
])
})
it('orders visible candidates by scheduling index and hides unstarted lazy candidates', async () => {
it('orders visible candidates by scheduling index and includes unattempted candidates', async () => {
const trace = buildTrace([
buildCandidate({
id: 'cand-success',
@@ -244,7 +244,17 @@ describe('HorizontalRequestTimeline', () => {
const labels = [...root.querySelectorAll<HTMLElement>('.node-label')]
.map(label => label.textContent?.trim())
expect(labels).toEqual(['Provider Skipped', 'Provider Failed', 'Provider Success'])
expect(labels).toEqual([
'Provider Available',
'Provider Skipped',
'Provider Pending',
'Provider Failed',
'Provider Success',
])
const nodeDots = [...root.querySelectorAll<HTMLElement>('.node-dot')]
expect(nodeDots[0].classList.contains('status-available')).toBe(true)
expect(nodeDots[2].classList.contains('status-pending')).toBe(true)
})
it('uses candidate terminal status for node colors instead of overriding with HTTP code', async () => {

View File

@@ -24,7 +24,7 @@ function buildCandidate(
}
describe('poolTrace', () => {
it('keeps only pool nodes that actually participated in scheduling audit fallback', () => {
it('keeps pool audit nodes even when they were not attempted', () => {
const attempts = buildPoolAttemptCandidatesFromAudit([], [
{
candidate_index: 0,
@@ -68,11 +68,13 @@ describe('poolTrace', () => {
},
], 'req-1')
expect(attempts).toHaveLength(2)
expect(attempts).toHaveLength(3)
expect(attempts[0].key_id).toBe('key-success')
expect(attempts[0].status).toBe('success')
expect(attempts[1].key_id).toBe('key-skipped')
expect(attempts[1].status).toBe('skipped')
expect(attempts[2].key_id).toBe('key-available')
expect(attempts[2].status).toBe('available')
})
it('preserves real trace attempts even when audit status is non-standard', () => {

View File

@@ -12,11 +12,6 @@ export const TIMELINE_STATUS: CandidateRecord['status'][] = [
'stream_interrupted',
]
const POOL_HIDDEN_STATUS = new Set<CandidateRecord['status']>([
'available',
'unused',
])
const PROVIDER_TYPE_LIKE_NAMES = new Set<string>([
'codex',
'kiro',
@@ -40,9 +35,7 @@ export const makeAttemptKey = (candidateIndex: number, retryIndex: number): stri
}
export const isPoolParticipatedCandidate = (candidate: CandidateRecord): boolean => {
if (POOL_HIDDEN_STATUS.has(candidate.status)) return false
if (candidate.status === 'pending' && !candidate.started_at) return false
return true
return TIMELINE_STATUS.includes(candidate.status)
}
export const isAttemptedCandidate = (