Fix/usage transfer filter (#307)

* fix(usage): 恢复 usage 列表中的 fallback 路由信号

* fix(admin): 支持 usage 记录展示和筛选 fallback 转移

* fix(usage): 在用户 usage 记录中暴露 fallback 标记

* fix(usage): 共享 usage 页面支持 fallback 筛选

* fix(usage): 对齐 fallback 筛选相关前端类型

* fix(usage): propagate has_fallback through active polling

---------

Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
RWDai
2026-04-17 13:38:23 +08:00
committed by GitHub
parent 654a41c3b0
commit ff4e853fd3
15 changed files with 298 additions and 49 deletions

View File

@@ -69,6 +69,7 @@ export interface UsageRecordDetail {
cache_creation_price_per_1m?: number
cache_read_price_per_1m?: number
price_per_request?: number // 按次计费价格
has_fallback?: boolean
api_key?: {
id: string
name: string
@@ -293,6 +294,7 @@ export const meApi = {
api_format?: string | null
endpoint_api_format?: string | null
has_format_conversion?: boolean | null
has_fallback?: boolean | null
}>
}> {
const params = ids ? { ids } : {}

View File

@@ -307,6 +307,7 @@ export const usageApi = {
api_format?: string | null
endpoint_api_format?: string | null
has_format_conversion?: boolean | null
has_fallback?: boolean | null
target_model?: string | null
}>
}> {

View File

@@ -401,6 +401,7 @@ export function useUsageData(options: UseUsageDataOptions) {
api_format: existing.api_format || record.api_format,
endpoint_api_format: existing.endpoint_api_format || record.endpoint_api_format,
has_format_conversion: existing.has_format_conversion ?? record.has_format_conversion,
has_fallback: existing.has_fallback === true || record.has_fallback === true,
api_key_name: existing.api_key_name || record.api_key_name,
provider_key_name: existing.provider_key_name || record.provider_key_name,
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,

View File

@@ -1,6 +1,6 @@
import { ref, computed, type Ref } from 'vue'
import type { UsageRecord, FilterStatusValue } from '../types'
import { isUsageRecordFailed } from '../utils/status'
import { hasUsageFallback, isUsageRecordFailed } from '../utils/status'
export interface UseUsageFiltersOptions {
/** 所有记录的响应式引用 */
@@ -79,6 +79,8 @@ export function useUsageFilters(options: UseUsageFiltersOptions) {
records = records.filter(record => isUsageRecordFailed(record))
} else if (filterStatus.value === 'cancelled') {
records = records.filter(record => record.status === 'cancelled')
} else if (filterStatus.value === 'has_fallback') {
records = records.filter(record => hasUsageFallback(record))
}
}

View File

@@ -127,7 +127,14 @@ export interface DateRangeParams {
export type PeriodValue = 'today' | 'yesterday' | 'last7days' | 'last30days' | 'last90days'
// 筛选状态(简化为常用维度)
export type FilterStatusValue = '__all__' | 'stream' | 'standard' | 'active' | 'failed' | 'cancelled'
export type FilterStatusValue =
'__all__' |
'stream' |
'standard' |
'active' |
'failed' |
'cancelled' |
'has_fallback'
// 默认统计状态
export function createDefaultStats(): UsageStatsState {

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
hasUsageFallback,
isUsageRecordFailed,
isUsageRecordSuccessful,
mapRequestStatusToTimelineStatus,
@@ -91,6 +92,12 @@ describe('usage status helpers', () => {
})).toBe('failed')
})
it('uses explicit has_fallback flag for transfer filtering', () => {
expect(hasUsageFallback(buildUsageRecord({ has_fallback: true }))).toBe(true)
expect(hasUsageFallback(buildUsageRecord({ has_fallback: false }))).toBe(false)
expect(hasUsageFallback(buildUsageRecord({ has_fallback: undefined }))).toBe(false)
})
it('uses status code only as a last fallback for timeline status', () => {
expect(resolveTimelineFinalStatus({
statusCode: 200,

View File

@@ -11,6 +11,12 @@ function hasLegacyFailureSignal(
(typeof record.error_message === 'string' && record.error_message.trim().length > 0)
}
export function hasUsageFallback(
record: Pick<UsageRecord, 'has_fallback'>
): boolean {
return record.has_fallback === true
}
function hasTerminalSuccessStatusCode(
record: Pick<UsageRecord, 'status_code'>
): boolean {

View File

@@ -144,7 +144,7 @@ import {
getDateRangeFromPeriod
} from '@/features/usage/composables'
import { reconcileActiveRequestDiscovery } from '@/features/usage/utils/activeRequestDiscovery'
import { isUsageRecordFailed } from '@/features/usage/utils/status'
import { hasUsageFallback, isUsageRecordFailed } from '@/features/usage/utils/status'
import type { DateRangeParams, FilterStatusValue } from '@/features/usage/types'
import type { UserOption } from '@/features/usage/components/UsageRecordsTable.vue'
import { log } from '@/utils/logger'
@@ -309,6 +309,8 @@ const filteredRecords = computed(() => {
records = records.filter(record => isUsageRecordFailed(record))
} else if (filterStatus.value === 'cancelled') {
records = records.filter(record => record.status === 'cancelled')
} else if (filterStatus.value === 'has_fallback') {
records = records.filter(record => hasUsageFallback(record))
}
}
@@ -399,6 +401,9 @@ async function pollActiveRequests() {
if (update.api_format != null) record.api_format = update.api_format
if (update.endpoint_api_format != null) record.endpoint_api_format = update.endpoint_api_format
if (update.has_format_conversion != null) record.has_format_conversion = update.has_format_conversion
if (typeof update.has_fallback === 'boolean') {
record.has_fallback = record.has_fallback === true || update.has_fallback
}
// 模型映射streaming 时已可确定
if ('target_model' in update && (typeof update.target_model === 'string' || update.target_model === null)) {
record.target_model = update.target_model