mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: provider api_formats 可空继承、OpenAI 图片 edit/variation 与用量配额多项补强
- 鉴权: provider_api_keys.api_formats 改为可空,OAuth 托管 key 自动继承 provider endpoints 激活格式,相关 handler/测试同步更新 - 图片 planner: OpenAI 图片路由新增 edit/variation 操作并完善参数校验、响应合并与流式处理 - 用量: user me usage 返回区分 client_requested_stream/upstream_is_stream,前端 usage 列表筛选与展示增强 - 统计: stats_daily_model 新增 cache_creation_ephemeral_5m/1h tokens 字段与回填链路 - 配额/observability: quota repository 新增内存与 SQL 扩展,admin observability usage 字段扩充 - 其它: OAuth 导入/轮询收敛、provider 汇总与 pool admin 读写链路小修、新增 system_config 缓存与 provider template handler Closes #318 Co-authored-by: Entropy.Xu <53283266+Entropy-Xu@users.noreply.github.com>
This commit is contained in:
@@ -58,7 +58,7 @@
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ detail.is_stream ? '流式' : '标准' }}
|
||||
{{ detail ? formatUsageStreamLabel(detail) : '标准' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
@@ -695,6 +695,7 @@ import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { formatShortRequestId } from '@/utils/format'
|
||||
import { log } from '@/utils/logger'
|
||||
import { getEffectiveInputTokens } from '../token-normalization'
|
||||
import { formatUsageStreamLabel } from '../utils/status'
|
||||
|
||||
// 子组件
|
||||
import RequestHeadersContent from './RequestDetailDrawer/RequestHeadersContent.vue'
|
||||
|
||||
@@ -232,19 +232,14 @@
|
||||
>
|
||||
取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="record.is_stream"
|
||||
variant="secondary"
|
||||
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
流式
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
:variant="getUpstreamStream(record) ? 'secondary' : 'outline'"
|
||||
:class="getUpstreamStream(record)
|
||||
? 'whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center'"
|
||||
>
|
||||
标准
|
||||
{{ getStreamModeLabel(record) }}
|
||||
</Badge>
|
||||
<span class="text-muted-foreground/50">|</span>
|
||||
<span>{{ formatDateTime(record.created_at) }}</span>
|
||||
@@ -310,7 +305,7 @@
|
||||
<TableHead class="h-12 font-semibold w-[120px]">
|
||||
API格式
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[70px] text-center">
|
||||
<TableHead class="h-12 font-semibold w-[110px] text-center">
|
||||
类型
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[140px] text-right">
|
||||
@@ -534,19 +529,14 @@
|
||||
>
|
||||
已取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="record.is_stream"
|
||||
variant="secondary"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
流式
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="whitespace-nowrap border-border/60 text-muted-foreground"
|
||||
:variant="getUpstreamStream(record) ? 'secondary' : 'outline'"
|
||||
:class="getUpstreamStream(record)
|
||||
? 'whitespace-nowrap'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground'"
|
||||
>
|
||||
标准
|
||||
{{ getStreamModeLabel(record) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-4 w-[140px]">
|
||||
@@ -678,7 +668,12 @@ import { RefreshCcw, Search } from 'lucide-vue-next'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatDateTime } from '../composables'
|
||||
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
|
||||
import { isUsageRecordFailed, resolveDisplayRequestStatus } from '../utils/status'
|
||||
import {
|
||||
formatUsageStreamLabel,
|
||||
isUsageRecordFailed,
|
||||
isUsageUpstreamStream,
|
||||
resolveDisplayRequestStatus
|
||||
} from '../utils/status'
|
||||
import { useRowClick } from '@/composables/useRowClick'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { DateRangeParams, UsageRecord } from '../types'
|
||||
@@ -764,6 +759,14 @@ function getDisplayStatus(record: UsageRecord) {
|
||||
return resolveDisplayRequestStatus(record)
|
||||
}
|
||||
|
||||
function getStreamModeLabel(record: UsageRecord): string {
|
||||
return formatUsageStreamLabel(record)
|
||||
}
|
||||
|
||||
function getUpstreamStream(record: UsageRecord): boolean {
|
||||
return isUsageUpstreamStream(record)
|
||||
}
|
||||
|
||||
watch(() => props.filterSearch, (value) => {
|
||||
if (value !== localSearch.value) {
|
||||
localSearch.value = value
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import type { UsageRecord, FilterStatusValue } from '../types'
|
||||
import { hasUsageFallback, isUsageRecordFailed } from '../utils/status'
|
||||
import { hasUsageFallback, isUsageRecordFailed, isUsageUpstreamStream } from '../utils/status'
|
||||
|
||||
export interface UseUsageFiltersOptions {
|
||||
/** 所有记录的响应式引用 */
|
||||
@@ -65,11 +65,11 @@ export function useUsageFilters(options: UseUsageFiltersOptions) {
|
||||
if (filterStatus.value !== '__all__') {
|
||||
if (filterStatus.value === 'stream') {
|
||||
records = records.filter(record =>
|
||||
record.is_stream && !isUsageRecordFailed(record)
|
||||
isUsageUpstreamStream(record) && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'standard') {
|
||||
records = records.filter(record =>
|
||||
!record.is_stream && !isUsageRecordFailed(record)
|
||||
!isUsageUpstreamStream(record) && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'active') {
|
||||
records = records.filter(record =>
|
||||
|
||||
@@ -107,6 +107,9 @@ export interface UsageRecord {
|
||||
response_time_ms?: number
|
||||
first_byte_time_ms?: number // 首字时间 (TTFB)
|
||||
is_stream: boolean
|
||||
upstream_is_stream?: boolean
|
||||
client_requested_stream?: boolean
|
||||
client_is_stream?: boolean
|
||||
status_code?: number
|
||||
error_message?: string
|
||||
status?: RequestStatus // 请求状态: pending, streaming, completed, failed
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
formatUsageStreamLabel,
|
||||
hasUsageFallback,
|
||||
isUsageRecordFailed,
|
||||
isUsageRecordSuccessful,
|
||||
@@ -98,6 +99,22 @@ describe('usage status helpers', () => {
|
||||
expect(hasUsageFallback(buildUsageRecord({ has_fallback: undefined }))).toBe(false)
|
||||
})
|
||||
|
||||
it('prefers symmetric stream aliases when present', () => {
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
is_stream: true,
|
||||
upstream_is_stream: true,
|
||||
client_requested_stream: true,
|
||||
client_is_stream: false,
|
||||
}))).toBe('标准 -> 流式')
|
||||
})
|
||||
|
||||
it('falls back to legacy stream fields when symmetric aliases are absent', () => {
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
is_stream: true,
|
||||
client_requested_stream: false,
|
||||
}))).toBe('标准 -> 流式')
|
||||
})
|
||||
|
||||
it('uses status code only as a last fallback for timeline status', () => {
|
||||
expect(resolveTimelineFinalStatus({
|
||||
statusCode: 200,
|
||||
|
||||
@@ -17,6 +17,52 @@ export function hasUsageFallback(
|
||||
return record.has_fallback === true
|
||||
}
|
||||
|
||||
export function resolveUsageStreamModes(
|
||||
record: Pick<
|
||||
UsageRecord,
|
||||
'is_stream' | 'upstream_is_stream' | 'client_requested_stream' | 'client_is_stream'
|
||||
>
|
||||
): { clientRequestedStream: boolean, upstreamStream: boolean } {
|
||||
const upstreamStream = typeof record.upstream_is_stream === 'boolean'
|
||||
? record.upstream_is_stream
|
||||
: record.is_stream
|
||||
|
||||
return {
|
||||
clientRequestedStream: typeof record.client_is_stream === 'boolean'
|
||||
? record.client_is_stream
|
||||
: typeof record.client_requested_stream === 'boolean'
|
||||
? record.client_requested_stream
|
||||
: upstreamStream,
|
||||
upstreamStream
|
||||
}
|
||||
}
|
||||
|
||||
export function isUsageUpstreamStream(
|
||||
record: Pick<
|
||||
UsageRecord,
|
||||
'is_stream' | 'upstream_is_stream' | 'client_requested_stream' | 'client_is_stream'
|
||||
>
|
||||
): boolean {
|
||||
return resolveUsageStreamModes(record).upstreamStream
|
||||
}
|
||||
|
||||
export function formatUsageStreamLabel(
|
||||
record: Pick<
|
||||
UsageRecord,
|
||||
'is_stream' | 'upstream_is_stream' | 'client_requested_stream' | 'client_is_stream'
|
||||
>
|
||||
): string {
|
||||
const { clientRequestedStream, upstreamStream } = resolveUsageStreamModes(record)
|
||||
const clientLabel = clientRequestedStream ? '流式' : '标准'
|
||||
const upstreamLabel = upstreamStream ? '流式' : '标准'
|
||||
|
||||
if (clientRequestedStream === upstreamStream) {
|
||||
return clientLabel
|
||||
}
|
||||
|
||||
return `${clientLabel} -> ${upstreamLabel}`
|
||||
}
|
||||
|
||||
function hasTerminalSuccessStatusCode(
|
||||
record: Pick<UsageRecord, 'status_code'>
|
||||
): boolean {
|
||||
|
||||
Reference in New Issue
Block a user