mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
Fix usage records filtering and pool trace display
This commit is contained in:
@@ -22,6 +22,19 @@
|
||||
class="h-8 w-full text-xs border-border/60 pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-usage-hide-unknown-toggle="mobile"
|
||||
class="h-8 w-8 shrink-0 md:hidden"
|
||||
:class="hideUnknownRecords ? 'text-primary' : ''"
|
||||
:title="hideUnknownRecords ? '显示 unknown 请求' : '隐藏 unknown 请求'"
|
||||
aria-label="隐藏 unknown 模型或提供商的请求"
|
||||
:aria-pressed="hideUnknownRecords"
|
||||
@click="$emit('update:hideUnknownRecords', !hideUnknownRecords)"
|
||||
>
|
||||
<EyeOff class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -178,6 +191,19 @@
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 自动刷新按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-usage-hide-unknown-toggle="desktop"
|
||||
class="hidden h-8 w-8 shrink-0 md:inline-flex"
|
||||
:class="hideUnknownRecords ? 'text-primary' : ''"
|
||||
:title="hideUnknownRecords ? '显示 unknown 请求' : '隐藏 unknown 请求'"
|
||||
aria-label="隐藏 unknown 模型或提供商的请求"
|
||||
:aria-pressed="hideUnknownRecords"
|
||||
@click="$emit('update:hideUnknownRecords', !hideUnknownRecords)"
|
||||
>
|
||||
<EyeOff class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -1028,7 +1054,7 @@ import {
|
||||
SortableTableHead,
|
||||
TableFilterMenu,
|
||||
} from '@/components/ui'
|
||||
import { RefreshCcw, Search, Shuffle } from 'lucide-vue-next'
|
||||
import { EyeOff, RefreshCcw, Search, Shuffle } from 'lucide-vue-next'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { getCacheCreationTokens, getCacheReadTokens, getEffectiveInputTokens } from '../token-normalization'
|
||||
import {
|
||||
@@ -1114,6 +1140,7 @@ const props = defineProps<{
|
||||
pageSizeOptions: number[]
|
||||
// 自动刷新
|
||||
autoRefresh: boolean
|
||||
hideUnknownRecords: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -1128,6 +1155,7 @@ const emit = defineEmits<{
|
||||
'update:currentPage': [value: number]
|
||||
'update:pageSize': [value: number]
|
||||
'update:autoRefresh': [value: boolean]
|
||||
'update:hideUnknownRecords': [value: boolean]
|
||||
'refresh': []
|
||||
'showDetail': [id: string]
|
||||
'prefetchDetail': [id: string]
|
||||
|
||||
@@ -257,6 +257,46 @@ describe('HorizontalRequestTimeline', () => {
|
||||
expect(nodeDots[2].classList.contains('status-pending')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps successful runtime pool key visible when only pool_key_index is recorded', async () => {
|
||||
const trace = buildTrace([
|
||||
buildCandidate({
|
||||
id: 'pool-skipped',
|
||||
provider_id: 'provider-pool',
|
||||
provider_name: 'CodexFree2',
|
||||
key_id: 'pool-group',
|
||||
key_name: 'CodexFree2',
|
||||
candidate_index: 0,
|
||||
status: 'skipped',
|
||||
started_at: undefined,
|
||||
finished_at: undefined,
|
||||
extra_data: { pool_group_id: 'provider-pool' },
|
||||
}),
|
||||
buildCandidate({
|
||||
id: 'pool-success',
|
||||
provider_id: 'provider-pool',
|
||||
provider_name: 'CodexFree2',
|
||||
key_id: 'key-success',
|
||||
key_name: 'Success Key',
|
||||
candidate_index: 1,
|
||||
status: 'success',
|
||||
extra_data: { pool_key_index: 0 },
|
||||
}),
|
||||
])
|
||||
|
||||
const root = mountTimeline(trace)
|
||||
await nextTick()
|
||||
|
||||
const labels = [...root.querySelectorAll<HTMLElement>('.node-label')]
|
||||
.map(label => label.textContent?.trim())
|
||||
expect(labels).toEqual(['CodexFree2'])
|
||||
expect(root.querySelector<HTMLElement>('.node-dot')?.classList.contains('status-success'))
|
||||
.toBe(true)
|
||||
expect([...root.querySelectorAll<HTMLButtonElement>('.sub-dot')]
|
||||
.map(dot => dot.getAttribute('title'))).toEqual([
|
||||
'#1 · Success Key · 成功',
|
||||
])
|
||||
})
|
||||
|
||||
it('uses candidate terminal status for node colors instead of overriding with HTTP code', async () => {
|
||||
const trace = buildTrace([
|
||||
buildCandidate({
|
||||
|
||||
@@ -81,6 +81,7 @@ vi.mock('lucide-vue-next', async () => {
|
||||
|
||||
return {
|
||||
RefreshCcw: Icon,
|
||||
EyeOff: Icon,
|
||||
Search: Icon,
|
||||
Shuffle: Icon,
|
||||
ChevronDown: Icon,
|
||||
@@ -152,6 +153,7 @@ function mountUsageRecordsTable(records: UsageRecord[], overrides: Record<string
|
||||
totalRecords: records.length,
|
||||
pageSizeOptions: [20, 50],
|
||||
autoRefresh: false,
|
||||
hideUnknownRecords: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -301,6 +303,17 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('Doubao Embedding')
|
||||
})
|
||||
|
||||
it('emits hide unknown toggle changes', () => {
|
||||
const onUpdateHideUnknownRecords = vi.fn()
|
||||
const root = mountUsageRecordsTable([buildRecord()], {
|
||||
'onUpdate:hideUnknownRecords': onUpdateHideUnknownRecords,
|
||||
})
|
||||
|
||||
root.querySelector<HTMLElement>('[data-usage-hide-unknown-toggle="desktop"]')?.click()
|
||||
|
||||
expect(onUpdateHideUnknownRecords).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('shows retry and fallback markers together when both flags are set', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
has_fallback: true,
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface FilterParams {
|
||||
api_format?: string
|
||||
status?: string
|
||||
client_family?: string
|
||||
hideUnknownRecords?: boolean
|
||||
}
|
||||
|
||||
export function useUsageData(options: UseUsageDataOptions) {
|
||||
@@ -361,6 +362,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
if (filters?.client_family) {
|
||||
params.client_family = filters.client_family
|
||||
}
|
||||
if (filters?.hideUnknownRecords) {
|
||||
params.hide_unknown = true
|
||||
}
|
||||
|
||||
const response = await usageApi.getAllUsageRecords(params)
|
||||
if (requestId !== loadRecordsRequestId) {
|
||||
|
||||
@@ -175,6 +175,34 @@ describe('poolTrace', () => {
|
||||
expect(attempts[1].extra_data?.pool_group_id).toBe('provider-1')
|
||||
})
|
||||
|
||||
it('infers pool membership from runtime pool key metadata', () => {
|
||||
const attempts = buildPoolParticipatedCandidates([
|
||||
buildCandidate({
|
||||
id: 'cand-pool-skipped',
|
||||
candidate_index: 0,
|
||||
provider_id: 'provider-1',
|
||||
provider_name: 'CodexFree2',
|
||||
key_id: 'pool-group',
|
||||
key_name: 'CodexFree2',
|
||||
status: 'skipped',
|
||||
extra_data: { pool_group_id: 'provider-1' },
|
||||
}),
|
||||
buildCandidate({
|
||||
id: 'cand-pool-success',
|
||||
candidate_index: 1,
|
||||
provider_id: 'provider-1',
|
||||
provider_name: 'CodexFree2',
|
||||
key_id: 'key-success',
|
||||
key_name: 'Success Key',
|
||||
status: 'success',
|
||||
extra_data: { pool_key_index: 0 },
|
||||
}),
|
||||
], null, 'req-1')
|
||||
|
||||
expect(attempts).toHaveLength(2)
|
||||
expect(attempts.map(item => item.key_id)).toEqual(['pool-group', 'key-success'])
|
||||
})
|
||||
|
||||
it('treats only real execution statuses as attempted', () => {
|
||||
expect(isAttemptedCandidate(buildCandidate({ status: 'success' }))).toBe(true)
|
||||
expect(isAttemptedCandidate(buildCandidate({ status: 'failed' }))).toBe(true)
|
||||
|
||||
@@ -74,14 +74,40 @@ export const parseTimelineStatus = (value: unknown): CandidateRecord['status'] |
|
||||
}
|
||||
|
||||
export const extractPoolGroupId = (
|
||||
candidate: Pick<CandidateRecord, 'extra_data'>,
|
||||
candidate: Pick<CandidateRecord, 'extra_data' | 'provider_id'>,
|
||||
): string | null => {
|
||||
const extra = candidate.extra_data
|
||||
if (!extra || typeof extra !== 'object' || Array.isArray(extra)) return null
|
||||
const value = (extra as Record<string, unknown>).pool_group_id
|
||||
if (typeof value !== 'string') return null
|
||||
const text = value.trim()
|
||||
return text || null
|
||||
const raw = extra as Record<string, unknown>
|
||||
|
||||
for (const key of ['pool_group_id', 'candidate_group_id']) {
|
||||
const value = raw[key]
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim()
|
||||
if (text) return text
|
||||
}
|
||||
}
|
||||
|
||||
const routingTrace = raw.routing_trace
|
||||
if (routingTrace && typeof routingTrace === 'object' && !Array.isArray(routingTrace)) {
|
||||
const poolExpansion = (routingTrace as Record<string, unknown>).pool_expansion
|
||||
if (Array.isArray(poolExpansion)) {
|
||||
for (const item of poolExpansion) {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) continue
|
||||
const value = (item as Record<string, unknown>).pool_group_id
|
||||
if (typeof value !== 'string') continue
|
||||
const text = value.trim()
|
||||
if (text) return text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (raw.pool_key_index !== undefined && raw.pool_key_index !== null) {
|
||||
const providerId = String(candidate.provider_id || '').trim()
|
||||
if (providerId) return providerId
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildPoolParticipatedCandidates(
|
||||
|
||||
Reference in New Issue
Block a user