mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
Merge upstream/main into feat/grok-complete-integration
冲突集中在 chat completions / messages 两条 Responses 转发路径: upstream 给 OpenAIForwardResult 增加了 UpstreamResponseModel 与 UpstreamResponseModelConflict(配套 beginUpstreamResponseModelObservation 观测器),本分支在同样位置把返回值改成了具名变量以便挂 Grok 原生搜索计数。 两侧不互斥,合并结果同时保留上游的响应模型观测字段与 Grok SearchCount 逻辑。 frontend/pnpm-lock.yaml 取 upstream 版本:package.json 与 upstream 完全一致, 本地差异只是 pnpm install 的重解析噪音。
This commit is contained in:
@@ -139,6 +139,11 @@
|
||||
<Select v-model="filters.billing_mode" :options="billingModeOptions" @change="emitChange" />
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'usage'" class="w-full sm:w-auto sm:min-w-[220px]">
|
||||
<label class="input-label">{{ t('admin.usage.upstreamModelAudit') }}</label>
|
||||
<Select v-model="filters.upstream_model_mismatch" :options="upstreamModelMismatchOptions" @change="emitChange" />
|
||||
</div>
|
||||
|
||||
<!-- Error Phase Filter (errors only) -->
|
||||
<div v-if="mode === 'errors'" class="w-full sm:w-auto sm:min-w-[180px]">
|
||||
<label class="input-label">{{ t('admin.ops.errorLog.type') }}</label>
|
||||
@@ -307,6 +312,12 @@ const billingModeOptions = ref<SelectOption[]>([
|
||||
{ value: 'video', label: t('admin.usage.billingModeVideo') }
|
||||
])
|
||||
|
||||
const upstreamModelMismatchOptions = ref<SelectOption[]>([
|
||||
{ value: null, label: t('admin.usage.allUpstreamModelAudit') },
|
||||
{ value: true, label: t('admin.usage.upstreamModelMismatchOnly') },
|
||||
{ value: false, label: t('admin.usage.upstreamModelMatchedOnly') }
|
||||
])
|
||||
|
||||
const emitChange = () => emit('change')
|
||||
|
||||
const clearPendingUserSearch = () => {
|
||||
|
||||
@@ -53,23 +53,41 @@
|
||||
</template>
|
||||
|
||||
<template #cell-model="{ row }">
|
||||
<div v-if="row.model_mapping_chain && row.model_mapping_chain.includes('→')" class="space-y-0.5 text-xs">
|
||||
<div v-for="(step, i) in row.model_mapping_chain.split('→')" :key="i"
|
||||
class="break-all"
|
||||
:class="i === 0 ? 'font-medium text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400'"
|
||||
:style="i > 0 ? `padding-left: ${i * 0.75}rem` : ''">
|
||||
<span v-if="i > 0" class="mr-0.5">↳</span>{{ step }}
|
||||
<div class="space-y-0.5 text-xs">
|
||||
<div v-if="row.model_mapping_chain && row.model_mapping_chain.includes('→')" class="space-y-0.5">
|
||||
<div v-for="(step, i) in row.model_mapping_chain.split('→')" :key="i"
|
||||
class="break-all"
|
||||
:class="i === 0 ? 'font-medium text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400'"
|
||||
:style="i > 0 ? `padding-left: ${i * 0.75}rem` : ''">
|
||||
<span v-if="i > 0" class="mr-0.5">↳</span>{{ step }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="row.upstream_model && row.upstream_model !== row.model" class="space-y-0.5">
|
||||
<div class="break-all font-medium text-gray-900 dark:text-white">
|
||||
{{ row.model }}
|
||||
</div>
|
||||
<div class="break-all text-gray-500 dark:text-gray-400">
|
||||
<span class="mr-0.5">↳</span>{{ row.upstream_model }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="font-medium text-gray-900 dark:text-white">{{ row.model }}</span>
|
||||
<div
|
||||
v-if="row.upstream_model_mismatch === true && row.upstream_response_model"
|
||||
class="break-all pl-3 text-[11px]"
|
||||
:class="isLikelyModelVariant(row) ? 'text-amber-600 dark:text-amber-400' : 'text-orange-600 dark:text-orange-400'"
|
||||
:title="modelAuditTitle(row)"
|
||||
>
|
||||
<span class="mr-1">↳ {{ t('usage.upstreamResponseModel') }}:</span>{{ row.upstream_response_model }}
|
||||
<span
|
||||
class="ml-1 inline-flex rounded px-1 py-px text-[10px] font-medium ring-1 ring-inset"
|
||||
:class="isLikelyModelVariant(row)
|
||||
? 'bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-500/30'
|
||||
: 'bg-orange-50 text-orange-700 ring-orange-200 dark:bg-orange-500/10 dark:text-orange-300 dark:ring-orange-500/30'"
|
||||
>
|
||||
{{ isLikelyModelVariant(row) ? t('usage.modelVariant') : t('usage.modelMismatch') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="row.upstream_model && row.upstream_model !== row.model" class="space-y-0.5 text-xs">
|
||||
<div class="break-all font-medium text-gray-900 dark:text-white">
|
||||
{{ row.model }}
|
||||
</div>
|
||||
<div class="break-all text-gray-500 dark:text-gray-400">
|
||||
<span class="mr-0.5">↳</span>{{ row.upstream_model }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="font-medium text-gray-900 dark:text-white">{{ row.model }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-reasoning_effort="{ row }">
|
||||
@@ -548,6 +566,27 @@ const ipGeoBatchLoading = ref(false)
|
||||
|
||||
const showIpGeoToolbar = computed(() => props.columns.some((col) => col.key === 'ip_address'))
|
||||
|
||||
const sentUpstreamModel = (row: AdminUsageLog): string => row.upstream_model?.trim() || row.model?.trim() || ''
|
||||
|
||||
const normalizeModelVariant = (model: string): string => model
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/-latest$/, '')
|
||||
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
||||
.replace(/-\d{8}$/, '')
|
||||
|
||||
const isLikelyModelVariant = (row: AdminUsageLog): boolean => {
|
||||
const sent = sentUpstreamModel(row)
|
||||
const response = row.upstream_response_model?.trim() || ''
|
||||
return sent !== '' && response !== '' && normalizeModelVariant(sent) === normalizeModelVariant(response)
|
||||
}
|
||||
|
||||
const modelAuditTitle = (row: AdminUsageLog): string => [
|
||||
`${t('usage.requestedModel')}: ${row.model || '-'}`,
|
||||
`${t('usage.sentUpstreamModel')}: ${sentUpstreamModel(row) || '-'}`,
|
||||
`${t('usage.upstreamResponseModel')}: ${row.upstream_response_model || '-'}`,
|
||||
].join('\n')
|
||||
|
||||
const currentPageIps = computed(() =>
|
||||
Array.from(new Set(props.data.map((row) => row.ip_address).filter((ip): ip is string => Boolean(ip))))
|
||||
)
|
||||
|
||||
@@ -28,6 +28,10 @@ const messages: Record<string, string> = {
|
||||
'admin.usage.billingModeToken': 'Token',
|
||||
'admin.usage.billingModePerRequest': 'Per Request',
|
||||
'admin.usage.billingModeImage': 'Image',
|
||||
'admin.usage.upstreamModelAudit': 'Upstream model audit',
|
||||
'admin.usage.allUpstreamModelAudit': 'All response model states',
|
||||
'admin.usage.upstreamModelMismatchOnly': 'Mismatched only',
|
||||
'admin.usage.upstreamModelMatchedOnly': 'Matched only',
|
||||
'admin.usage.group': 'Group',
|
||||
'admin.usage.allGroups': 'All Groups',
|
||||
'common.refresh': 'Refresh',
|
||||
@@ -75,6 +79,7 @@ const defaultFilters = () => ({
|
||||
request_type: null,
|
||||
billing_type: null,
|
||||
billing_mode: null,
|
||||
upstream_model_mismatch: null,
|
||||
group_id: null,
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
|
||||
@@ -51,6 +51,11 @@ const messages: Record<string, string> = {
|
||||
'admin.usage.billingModeToken': 'Token',
|
||||
'admin.usage.billingModePerRequest': 'Per request',
|
||||
'admin.usage.billingModeImage': 'Image',
|
||||
'usage.requestedModel': 'Requested',
|
||||
'usage.sentUpstreamModel': 'Sent upstream',
|
||||
'usage.upstreamResponseModel': 'Upstream response',
|
||||
'usage.modelVariant': 'Possible version variant',
|
||||
'usage.modelMismatch': 'Different model',
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
@@ -240,6 +245,48 @@ describe('admin UsageTable tooltip', () => {
|
||||
expect(text).toContain('claude-sonnet-4-20250514')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'possible version variant',
|
||||
responseModel: 'gpt-5.5-2026-08-01',
|
||||
expectedBadge: 'Possible version variant',
|
||||
},
|
||||
{
|
||||
name: 'different upstream model',
|
||||
responseModel: 'gpt-5.4',
|
||||
expectedBadge: 'Different model',
|
||||
},
|
||||
])('shows a compact upstream response audit marker for $name', ({ responseModel, expectedBadge }) => {
|
||||
const wrapper = mount(UsageTable, {
|
||||
props: {
|
||||
data: [{
|
||||
request_id: `req-${responseModel}`,
|
||||
model: 'gpt-5.6-sol',
|
||||
upstream_model: 'gpt-5.5',
|
||||
model_mapping_chain: 'gpt-5.6-sol→gpt-5.5',
|
||||
upstream_response_model: responseModel,
|
||||
upstream_model_mismatch: true,
|
||||
}],
|
||||
loading: false,
|
||||
columns: [],
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
EmptyState: true,
|
||||
Icon: true,
|
||||
Teleport: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('gpt-5.6-sol')
|
||||
expect(text).toContain('gpt-5.5')
|
||||
expect(text).toContain(responseModel)
|
||||
expect(text).toContain(expectedBadge)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'defaulted row',
|
||||
|
||||
@@ -59,13 +59,22 @@
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="m in sortedModels"
|
||||
:key="m.name"
|
||||
:key="`${m.platform}:${m.name}`"
|
||||
class="border-b border-gray-100 transition-colors last:border-b-0 hover:bg-gray-50/70 dark:border-dark-800 dark:hover:bg-dark-800/50"
|
||||
>
|
||||
<!-- 模型名 + 非 token 计费模式徽章 -->
|
||||
<td class="border-r border-gray-100 py-2.5 pl-5 pr-4 align-middle dark:border-dark-700/60">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ m.name }}</span>
|
||||
<span
|
||||
v-if="platform && m.platform !== platform"
|
||||
:class="[
|
||||
'inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium',
|
||||
platformBadgeLightClass(m.platform)
|
||||
]"
|
||||
>
|
||||
{{ platformLabel(m.platform) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="billingMode(m) !== BILLING_MODE_TOKEN"
|
||||
class="rounded-md bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:bg-dark-700/70 dark:text-dark-300"
|
||||
@@ -203,7 +212,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatScaled } from '@/utils/pricing'
|
||||
import { platformAccentColor } from '@/utils/platformColors'
|
||||
import { platformAccentColor, platformBadgeLightClass, platformLabel } from '@/utils/platformColors'
|
||||
import {
|
||||
BILLING_MODE_TOKEN,
|
||||
BILLING_MODE_IMAGE,
|
||||
|
||||
@@ -383,4 +383,25 @@ describe('PlazaModelPricingTable', () => {
|
||||
// 旧 bug:image_output_price × 0.1 = 0.000003 被当按次价
|
||||
expect(text).not.toContain('$0.000003')
|
||||
})
|
||||
|
||||
it('Composite 分组中相同模型名按具体平台分别展示徽章', () => {
|
||||
const anthropic = tokenModel({ name: 'shared-model', platform: 'anthropic' })
|
||||
const openai = tokenModel({ name: 'shared-model', platform: 'openai' })
|
||||
const wrapper = mount(PlazaModelPricingTable, {
|
||||
props: {
|
||||
models: [anthropic, openai],
|
||||
platform: 'composite',
|
||||
rateMultiplier: 1
|
||||
}
|
||||
})
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows.map((row) => row.find('td').text())).toEqual([
|
||||
'shared-modelAnthropic',
|
||||
'shared-modelOpenAI'
|
||||
])
|
||||
expect(wrapper.text()).toContain('Anthropic')
|
||||
expect(wrapper.text()).toContain('OpenAI')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user