mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
merge: integrate usage response models with service tier badges
Merge upstream PR #837, preserving its original commits and resolving the UsageModelDisplay layout conflict after #841. Cover response models alongside dynamic service-tier badges in detail tests.
This commit is contained in:
@@ -219,6 +219,7 @@ export interface RequestDetail {
|
||||
has_format_conversion?: boolean | null
|
||||
model: string
|
||||
target_model?: string | null // 映射后的目标模型名
|
||||
response_model?: string | null // 上游响应体实际返回的模型名
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface UsageRecordDetail {
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
actual_service_tier?: string | null
|
||||
response_model?: string | null
|
||||
input_tokens: number
|
||||
effective_input_tokens?: number
|
||||
output_tokens: number
|
||||
@@ -392,6 +393,7 @@ export const meApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
response_model?: string | null
|
||||
request_type?: string | null
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
@@ -438,6 +440,7 @@ export const meApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
response_model?: string | null
|
||||
request_type?: string | null
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface UsageRecord {
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
actual_service_tier?: string | null
|
||||
response_model?: string | null
|
||||
input_tokens: number
|
||||
effective_input_tokens?: number
|
||||
output_tokens: number
|
||||
@@ -618,6 +619,7 @@ export const usageApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
response_model?: string | null
|
||||
request_type?: string | null
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
@@ -685,6 +687,7 @@ export const usageApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
response_model?: string | null
|
||||
request_type?: string | null
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface UsageRecord {
|
||||
model: string
|
||||
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
||||
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
|
||||
response_model?: string | null // 上游响应体实际返回的模型名
|
||||
request_type?: string | null // 由请求语义识别出的操作类型
|
||||
requested_reasoning_effort?: string | null // 用户请求侧 reasoning 级别,用于展示转换关系
|
||||
reasoning_effort?: string | null // 从发送给 Provider 的请求体提取的 reasoning 级别
|
||||
|
||||
@@ -1016,6 +1016,7 @@ const emit = defineEmits<{
|
||||
reasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
actualServiceTier?: string | null
|
||||
responseModel?: string | null
|
||||
imageProgress?: ImageProgress | null
|
||||
errorMessage?: string | null
|
||||
updatedAt?: string | null
|
||||
@@ -1158,6 +1159,7 @@ type HeaderModelTextField =
|
||||
| 'model'
|
||||
| 'target_model'
|
||||
| 'model_version'
|
||||
| 'response_model'
|
||||
| 'request_type'
|
||||
| 'requested_reasoning_effort'
|
||||
| 'reasoning_effort'
|
||||
@@ -1166,6 +1168,7 @@ type HeaderModelTextField =
|
||||
|
||||
const FINAL_PROVIDER_HEADER_FIELDS = new Set<HeaderModelTextField>([
|
||||
'target_model',
|
||||
'response_model',
|
||||
'reasoning_effort',
|
||||
'service_tier',
|
||||
'actual_service_tier',
|
||||
@@ -1252,6 +1255,7 @@ watch(
|
||||
props.summaryRecord?.model,
|
||||
props.summaryRecord?.target_model,
|
||||
props.summaryRecord?.model_version,
|
||||
props.summaryRecord?.response_model,
|
||||
props.summaryRecord?.request_type,
|
||||
props.summaryRecord?.requested_reasoning_effort,
|
||||
props.summaryRecord?.reasoning_effort,
|
||||
@@ -1298,6 +1302,7 @@ function emitDetailRequestState(nextDetail: RequestDetail) {
|
||||
const reasoningEffort = resolveHeaderModelTextField('reasoning_effort', nextDetail)
|
||||
const serviceTier = resolveHeaderModelTextField('service_tier', nextDetail)
|
||||
const actualServiceTier = resolveHeaderModelTextField('actual_service_tier', nextDetail)
|
||||
const responseModel = resolveHeaderModelTextField('response_model', nextDetail)
|
||||
|
||||
emit('requestState', {
|
||||
id,
|
||||
@@ -1334,6 +1339,7 @@ function emitDetailRequestState(nextDetail: RequestDetail) {
|
||||
...(reasoningEffort ? { reasoningEffort } : {}),
|
||||
...(serviceTier ? { serviceTier } : {}),
|
||||
...(actualServiceTier ? { actualServiceTier } : {}),
|
||||
...(responseModel ? { responseModel } : {}),
|
||||
errorMessage: nextDetail.error_message ?? undefined,
|
||||
updatedAt: nextDetail.updated_at ?? undefined,
|
||||
})
|
||||
@@ -1610,7 +1616,7 @@ const headerModelRecord = computed(() => {
|
||||
return {
|
||||
model: resolveHeaderModelTextField('model', currentDetail) ?? '-',
|
||||
target_model: resolveHeaderModelTextField('target_model', currentDetail),
|
||||
model_version: resolveHeaderModelTextField('model_version', currentDetail),
|
||||
response_model: resolveHeaderModelTextField('response_model', currentDetail),
|
||||
request_type: resolveHeaderModelTextField('request_type', currentDetail),
|
||||
requested_reasoning_effort: resolveHeaderModelTextField(
|
||||
'requested_reasoning_effort',
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
:class="modelClass"
|
||||
data-usage-model-source
|
||||
>{{ record.model }}</span>
|
||||
<template v-if="actualModel">
|
||||
<span
|
||||
class="order-last basis-full min-w-0 break-all whitespace-normal text-muted-foreground"
|
||||
data-usage-model-target
|
||||
><span class="mr-1">-></span>{{ actualModel }}</span>
|
||||
<template v-if="hasModelFacts">
|
||||
<div
|
||||
class="order-last basis-full flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-muted-foreground"
|
||||
data-usage-model-facts
|
||||
>
|
||||
<span
|
||||
v-if="mappingModel"
|
||||
class="min-w-0 break-all whitespace-normal"
|
||||
data-usage-model-mapping
|
||||
><span class="mr-1 text-[10px] text-muted-foreground/70">映射模型</span>{{ mappingModel }}</span>
|
||||
<span
|
||||
v-if="responseModel"
|
||||
class="min-w-0 break-all whitespace-normal"
|
||||
data-usage-model-response
|
||||
><span class="mr-1 text-[10px] text-muted-foreground/70">响应模型</span>{{ responseModel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!shouldStackBadges">
|
||||
<Badge
|
||||
@@ -83,7 +94,7 @@ interface ModelBadgePresentation {
|
||||
interface UsageModelDisplayRecord {
|
||||
model: string
|
||||
target_model?: string | null
|
||||
model_version?: string | null
|
||||
response_model?: string | null
|
||||
request_type?: string | null
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
@@ -113,15 +124,20 @@ const props = withDefaults(defineProps<{
|
||||
showReasoningBadge: true,
|
||||
})
|
||||
|
||||
const actualModel = computed(() => {
|
||||
const mappingModel = computed(() => {
|
||||
const targetModel = normalizeText(props.record.target_model)
|
||||
if (targetModel && targetModel !== props.record.model) return targetModel
|
||||
|
||||
const modelVersion = normalizeText(props.record.model_version)
|
||||
if (modelVersion && modelVersion !== props.record.model) return modelVersion
|
||||
if (targetModel && targetModel !== normalizeText(props.record.model)) return targetModel
|
||||
return null
|
||||
})
|
||||
|
||||
const responseModel = computed(() => {
|
||||
const response = normalizeText(props.record.response_model)
|
||||
if (response && response !== normalizeText(props.record.model)) return response
|
||||
return null
|
||||
})
|
||||
|
||||
const hasModelFacts = computed(() => mappingModel.value !== null || responseModel.value !== null)
|
||||
|
||||
const reasoningLabel = computed(() => {
|
||||
const requested = normalizeText(props.record.requested_reasoning_effort)
|
||||
const actual = normalizeText(props.record.reasoning_effort)
|
||||
@@ -191,7 +207,7 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
|
||||
})
|
||||
|
||||
const shouldStackBadges = computed(() => (
|
||||
actualModel.value === null && modelBadges.value.length >= 3
|
||||
!hasModelFacts.value && modelBadges.value.length >= 3
|
||||
))
|
||||
|
||||
function normalizeText(value: string | null | undefined): string | null {
|
||||
|
||||
@@ -1656,20 +1656,6 @@ function getApiFormatTooltip(record: UsageRecord): string {
|
||||
return displayFormat
|
||||
}
|
||||
|
||||
// 获取实际使用的模型(优先 target_model,其次列表接口下发的 model_version)
|
||||
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
||||
function getActualModel(record: UsageRecord): string | null {
|
||||
// 优先显示模型映射
|
||||
if (record.target_model && record.target_model !== record.model) {
|
||||
return record.target_model
|
||||
}
|
||||
// 其次显示 Provider 返回的实际版本(如 Gemini 的 modelVersion)
|
||||
if (record.model_version && record.model_version !== record.model) {
|
||||
return record.model_version
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getReasoningEffort(record: UsageRecord): string | null {
|
||||
const requested = record.requested_reasoning_effort?.trim()
|
||||
const actual = record.reasoning_effort?.trim()
|
||||
@@ -1744,15 +1730,18 @@ function getServiceTierTitle(record: UsageRecord): string {
|
||||
|
||||
// 获取模型列的 tooltip
|
||||
function getModelTooltip(record: UsageRecord): string {
|
||||
const actualModel = getActualModel(record)
|
||||
const reasoningEffort = getReasoningEffort(record)
|
||||
const serviceTierTitle = getServiceTierTitle(record)
|
||||
const tierSuffix = serviceTierTitle ? `\n${serviceTierTitle}` : ''
|
||||
const cyberSuffix = hasCyberPolicyError(record) ? '\nCyber Policy: blocked' : ''
|
||||
const suffix = `${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${tierSuffix}${cyberSuffix}`
|
||||
if (actualModel) {
|
||||
return `${record.model} -> ${actualModel}${suffix}`
|
||||
}
|
||||
return `${record.model}${suffix}`
|
||||
const requestModel = record.model.trim()
|
||||
const mappingModel = record.target_model?.trim()
|
||||
const responseModel = record.response_model?.trim()
|
||||
return [
|
||||
requestModel,
|
||||
mappingModel && mappingModel !== requestModel ? `映射模型: ${mappingModel}` : null,
|
||||
responseModel && responseModel !== requestModel ? `响应模型: ${responseModel}` : null,
|
||||
].filter((line): line is string => Boolean(line)).join('\n') + suffix
|
||||
}
|
||||
</script>
|
||||
|
||||
+14
-12
@@ -313,13 +313,14 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
{ tier: 'priority', label: 'Fast', badgeKey: 'fast' },
|
||||
{ tier: 'ultrafast', label: 'ultrafast', badgeKey: 'service-tier' },
|
||||
{ tier: 'future-tier', label: 'future-tier', badgeKey: 'service-tier' },
|
||||
])('shows mapping, reasoning, $tier, and Cyber together in the model header', async ({ tier, label, badgeKey }) => {
|
||||
])('shows mapping, response model, reasoning, $tier, and Cyber together in the model header', async ({ tier, label, badgeKey }) => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue({
|
||||
...buildEmbeddingDetail(),
|
||||
id: 'usage-cyber-risk-demo',
|
||||
request_id: 'req_usage-cyber-risk-demo',
|
||||
model: 'gpt-5',
|
||||
target_model: 'gpt-5.1',
|
||||
response_model: 'gpt-5.2',
|
||||
requested_reasoning_effort: 'xhigh',
|
||||
reasoning_effort: 'max',
|
||||
request_body: {
|
||||
@@ -382,10 +383,11 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
)
|
||||
const modelRow = modelLayout?.firstElementChild
|
||||
expect(modelRow?.textContent).toContain('gpt-5')
|
||||
expect(modelRow?.textContent).toContain('->')
|
||||
expect(modelRow?.textContent).toContain('gpt-5.1')
|
||||
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('basis-full'))
|
||||
.toBe(true)
|
||||
expect(modelRow?.classList.contains('flex-wrap')).toBe(true)
|
||||
expect(modelRow?.querySelector('[data-usage-model-mapping]')?.textContent)
|
||||
.toContain('gpt-5.1')
|
||||
expect(modelRow?.querySelector('[data-usage-model-response]')?.textContent)
|
||||
.toContain('gpt-5.2')
|
||||
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||||
.toContain('xhigh -> max')
|
||||
expect(modelRow?.querySelector(`[data-request-detail-model-badge="${badgeKey}"]`)?.textContent)
|
||||
@@ -462,7 +464,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||||
.toContain('Cyber')
|
||||
expect(document.body.querySelector('[data-usage-model-target]')?.textContent)
|
||||
expect(document.body.querySelector('[data-usage-model-mapping]')?.textContent)
|
||||
.toContain('gpt-5.1')
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||||
.toContain('xhigh -> max')
|
||||
@@ -573,8 +575,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(apiMocks.getRequestDetail).toHaveBeenCalledTimes(1)
|
||||
expect(document.body.querySelector('[data-usage-model-target]')?.textContent?.trim())
|
||||
.toBe('->gpt-5.1')
|
||||
expect(document.body.querySelector('[data-usage-model-mapping]')?.textContent?.trim())
|
||||
.toBe('映射模型gpt-5.1')
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent?.trim())
|
||||
.toBe('xhigh -> max')
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).toBeNull()
|
||||
@@ -585,7 +587,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses detail model_version when the lightweight summary has null model facts', async () => {
|
||||
it('does not use legacy model_version as a response-model fallback', async () => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue({
|
||||
...buildEmbeddingDetail(),
|
||||
id: 'usage-version-summary',
|
||||
@@ -628,8 +630,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
await nextTick()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body.querySelector('[data-usage-model-target]')?.textContent?.trim())
|
||||
.toBe('->gpt-5.1-2026-07-17')
|
||||
expect(document.body.querySelector('[data-usage-model-response]')).toBeNull()
|
||||
expect(document.body.querySelector('[data-usage-model-mapping]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -689,7 +691,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
expect(document.body.querySelector('[data-usage-model-target]')).toBeNull()
|
||||
expect(document.body.querySelector('[data-usage-model-mapping]')).toBeNull()
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent?.trim())
|
||||
.toBe('xhigh')
|
||||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).toBeNull()
|
||||
|
||||
@@ -461,10 +461,11 @@ describe('UsageRecordsTable', () => {
|
||||
.toBe('会话压缩')
|
||||
})
|
||||
|
||||
it('shows mapping, reasoning, Fast, and Cyber in the model area', () => {
|
||||
it('shows mapping and response models as separately labelled facts', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
model: 'gpt-5',
|
||||
target_model: 'gpt-5.1',
|
||||
response_model: 'gpt-5.2',
|
||||
requested_reasoning_effort: 'xhigh',
|
||||
reasoning_effort: 'max',
|
||||
service_tier: 'priority',
|
||||
@@ -477,6 +478,8 @@ describe('UsageRecordsTable', () => {
|
||||
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
expect(root.textContent).toContain('gpt-5.1')
|
||||
expect(root.textContent).toContain('映射模型')
|
||||
expect(root.textContent).toContain('响应模型')
|
||||
expect(root.textContent).toContain('xhigh -> max')
|
||||
expect(root.textContent).toContain('Fast')
|
||||
const reasoningBadge = root.querySelector<HTMLElement>('[data-usage-model-badge="reasoning"]')
|
||||
@@ -510,15 +513,35 @@ describe('UsageRecordsTable', () => {
|
||||
expect(inlineLayout).not.toBeNull()
|
||||
const modelRow = inlineLayout?.firstElementChild
|
||||
expect(modelRow?.textContent).toContain('gpt-5')
|
||||
expect(modelRow?.textContent).toContain('->')
|
||||
expect(modelRow?.textContent).toContain('gpt-5.1')
|
||||
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('basis-full')).toBe(true)
|
||||
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('order-last')).toBe(true)
|
||||
expect(modelRow?.querySelector('[data-usage-model-mapping]')?.textContent).toContain('映射模型')
|
||||
expect(modelRow?.querySelector('[data-usage-model-response]')?.textContent).toContain('响应模型')
|
||||
expect(modelRow?.querySelector('[data-usage-model-badge="reasoning"]')?.textContent).toContain('xhigh -> max')
|
||||
expect(modelRow?.querySelector('[data-usage-model-badge="fast"]')?.textContent).toContain('Fast')
|
||||
expect(modelRow?.querySelector('[data-usage-model-badge="cyber"]')?.textContent).toContain('Cyber')
|
||||
})
|
||||
|
||||
it('shows a response model without inventing a mapping arrow or using model_version', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
response_model: 'gpt-5.1',
|
||||
model_version: 'legacy-version',
|
||||
target_model: null,
|
||||
})])
|
||||
|
||||
expect(root.querySelector('[data-usage-model-mapping]')).toBeNull()
|
||||
expect(root.querySelector('[data-usage-model-response]')?.textContent).toContain('响应模型')
|
||||
expect(root.textContent).not.toContain('legacy-version')
|
||||
expect(root.textContent).not.toContain('->')
|
||||
})
|
||||
|
||||
it('hides a response model when it matches the request model', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
response_model: 'gpt-5',
|
||||
target_model: null,
|
||||
})])
|
||||
|
||||
expect(root.querySelector('[data-usage-model-response]')).toBeNull()
|
||||
})
|
||||
|
||||
it('stacks three model badges even without a model mapping', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
model: 'gpt-5',
|
||||
|
||||
@@ -716,7 +716,13 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
? (typeof record.actual_service_tier === 'string' && record.actual_service_tier.trim()
|
||||
? record.actual_service_tier
|
||||
: null)
|
||||
: existing.actual_service_tier
|
||||
: existing.actual_service_tier,
|
||||
// 终态列表快照是最终候选的权威事实;空值必须清除旧候选的响应模型。
|
||||
response_model: statusProgressed
|
||||
? (typeof record.response_model === 'string' && record.response_model.trim()
|
||||
? record.response_model
|
||||
: null)
|
||||
: existing.response_model
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -651,6 +651,10 @@ async function pollActiveRequests() {
|
||||
record.actual_service_tier = typeof update.actual_service_tier === 'string' && update.actual_service_tier.trim()
|
||||
? update.actual_service_tier
|
||||
: null
|
||||
// 活跃接口返回的是当前最终候选快照,服务端空值要清除旧响应模型。
|
||||
record.response_model = typeof update.response_model === 'string' && update.response_model.trim()
|
||||
? update.response_model
|
||||
: null
|
||||
// 管理员接口返回额外字段
|
||||
// 只有当返回的 provider 不是 pending/unknown/unknow 时才更新,避免覆盖已有的正确值
|
||||
if ('provider' in update && typeof update.provider === 'string') {
|
||||
@@ -1150,6 +1154,7 @@ function handleDetailRequestState(update: {
|
||||
reasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
actualServiceTier?: string | null
|
||||
responseModel?: string | null
|
||||
imageProgress?: ImageProgress | null
|
||||
errorMessage?: string | null
|
||||
updatedAt?: string | null
|
||||
@@ -1292,6 +1297,11 @@ function handleDetailRequestState(update: {
|
||||
? update.actualServiceTier
|
||||
: null
|
||||
}
|
||||
if ('responseModel' in update) {
|
||||
record.response_model = typeof update.responseModel === 'string'
|
||||
? update.responseModel
|
||||
: null
|
||||
}
|
||||
if ('imageProgress' in update) {
|
||||
const nextProgress = update.imageProgress ?? null
|
||||
if (!sameImageProgress(record.image_progress, nextProgress)) {
|
||||
|
||||
Reference in New Issue
Block a user