mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +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:
@@ -450,6 +450,7 @@ const loadModelStats = async (source: ModelDistributionSource, force = false) =>
|
||||
request_type: requestType,
|
||||
stream: legacyStream === null ? undefined : legacyStream,
|
||||
billing_type: filters.value.billing_type,
|
||||
upstream_model_mismatch: filters.value.upstream_model_mismatch,
|
||||
}
|
||||
|
||||
const response = await adminAPI.dashboard.getModelStats({ ...baseParams, model_source: source })
|
||||
@@ -499,6 +500,7 @@ const loadChartData = async () => {
|
||||
request_type: requestType,
|
||||
stream: legacyStream === null ? undefined : legacyStream,
|
||||
billing_type: filters.value.billing_type,
|
||||
upstream_model_mismatch: filters.value.upstream_model_mismatch,
|
||||
include_stats: false,
|
||||
include_trend: true,
|
||||
include_model_stats: false,
|
||||
@@ -573,7 +575,7 @@ const exportToExcel = async () => {
|
||||
const XLSX = await import('xlsx')
|
||||
const headers = [
|
||||
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
||||
t('admin.usage.account'), t('usage.model'), t('usage.upstreamModel'), t('usage.reasoningEffort'), t('admin.usage.group'),
|
||||
t('admin.usage.account'), t('usage.requestedModel'), t('usage.sentUpstreamModel'), t('usage.upstreamResponseModel'), t('usage.upstreamModelMismatch'), t('usage.reasoningEffort'), t('admin.usage.group'),
|
||||
t('usage.inboundEndpoint'), t('usage.upstreamEndpoint'),
|
||||
t('usage.type'),
|
||||
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
||||
@@ -593,7 +595,7 @@ const exportToExcel = async () => {
|
||||
if (c.signal.aborted) break; if (p === 1) { total = res.total; exportProgress.total = total }
|
||||
const rows = (res.items || []).map((log: AdminUsageLog) => [
|
||||
log.created_at, log.user?.email || '', log.api_key?.name || '', log.account?.name || '', log.model,
|
||||
log.upstream_model || '', formatReasoningEffort(log.reasoning_effort), log.group?.name || '',
|
||||
log.upstream_model || log.model, log.upstream_response_model || '', log.upstream_model_mismatch == null ? '' : t(log.upstream_model_mismatch ? 'common.yes' : 'common.no'), formatReasoningEffort(log.reasoning_effort), log.group?.name || '',
|
||||
log.inbound_endpoint || '', log.upstream_endpoint || '', getRequestTypeLabel(log),
|
||||
log.input_tokens, log.output_tokens, log.cache_read_tokens, log.cache_creation_tokens,
|
||||
log.input_cost?.toFixed(6) || '0.000000', log.output_cost?.toFixed(6) || '0.000000',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineComponent, ref } from 'vue'
|
||||
|
||||
import UsageView from '../UsageView.vue'
|
||||
|
||||
const { list, getStats, getSnapshotV2, getById, getModelStats, listErrorLogs, routeQuery } = vi.hoisted(() => {
|
||||
const { list, exportList, getStats, getSnapshotV2, getById, getModelStats, listErrorLogs, routeQuery, aoaToSheet, sheetAddAoa, saveAs, xlsxWrite } = vi.hoisted(() => {
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: vi.fn(() => null),
|
||||
setItem: vi.fn(),
|
||||
@@ -13,12 +13,17 @@ const { list, getStats, getSnapshotV2, getById, getModelStats, listErrorLogs, ro
|
||||
|
||||
return {
|
||||
list: vi.fn(),
|
||||
exportList: vi.fn(),
|
||||
getStats: vi.fn(),
|
||||
getSnapshotV2: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
getModelStats: vi.fn(),
|
||||
listErrorLogs: vi.fn(),
|
||||
routeQuery: {} as Record<string, string>,
|
||||
aoaToSheet: vi.fn(() => ({})),
|
||||
sheetAddAoa: vi.fn(),
|
||||
saveAs: vi.fn(),
|
||||
xlsxWrite: vi.fn(() => new Uint8Array([1, 2, 3])),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,6 +32,12 @@ const messages: Record<string, string> = {
|
||||
'admin.dashboard.day': 'Day',
|
||||
'admin.dashboard.hour': 'Hour',
|
||||
'admin.usage.failedToLoadUser': 'Failed to load user',
|
||||
'usage.requestedModel': 'Requested model',
|
||||
'usage.sentUpstreamModel': 'Sent upstream model',
|
||||
'usage.upstreamResponseModel': 'Upstream response model',
|
||||
'usage.upstreamModelMismatch': 'Upstream model mismatch',
|
||||
'common.yes': 'Yes',
|
||||
'common.no': 'No',
|
||||
}
|
||||
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
@@ -54,10 +65,22 @@ vi.mock('@/api/admin', () => ({
|
||||
|
||||
vi.mock('@/api/admin/usage', () => ({
|
||||
adminUsageAPI: {
|
||||
list: vi.fn(),
|
||||
list: exportList,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('file-saver', () => ({ saveAs }))
|
||||
|
||||
vi.mock('xlsx', () => ({
|
||||
utils: {
|
||||
aoa_to_sheet: aoaToSheet,
|
||||
sheet_add_aoa: sheetAddAoa,
|
||||
book_new: vi.fn(() => ({})),
|
||||
book_append_sheet: vi.fn(),
|
||||
},
|
||||
write: xlsxWrite,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin/ops', () => ({
|
||||
listErrorLogs,
|
||||
}))
|
||||
@@ -539,3 +562,62 @@ describe('admin UsageView ranking tab', () => {
|
||||
expect(list).toHaveBeenCalledWith(expect.objectContaining({ user_id: 5 }), expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin UsageView model audit export', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
list.mockReset().mockResolvedValue({ items: [], total: 0, pages: 0 })
|
||||
exportList.mockReset().mockResolvedValue({
|
||||
items: [{
|
||||
id: 1,
|
||||
created_at: '2026-08-04T00:00:00Z',
|
||||
model: 'gpt-5.6-sol',
|
||||
upstream_model: 'gpt-5.5',
|
||||
upstream_response_model: 'gpt-5.4',
|
||||
upstream_model_mismatch: true,
|
||||
request_type: 'sync',
|
||||
input_tokens: 1,
|
||||
output_tokens: 1,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
duration_ms: 10,
|
||||
}],
|
||||
total: 1,
|
||||
pages: 1,
|
||||
})
|
||||
getStats.mockReset().mockResolvedValue({
|
||||
total_requests: 0, total_input_tokens: 0, total_output_tokens: 0,
|
||||
total_cache_tokens: 0, total_tokens: 0, total_cost: 0, total_actual_cost: 0, average_duration_ms: 0,
|
||||
})
|
||||
getSnapshotV2.mockReset().mockResolvedValue({ trend: [], models: [], groups: [] })
|
||||
getModelStats.mockReset().mockResolvedValue({ models: [] })
|
||||
aoaToSheet.mockClear()
|
||||
sheetAddAoa.mockClear()
|
||||
saveAs.mockClear()
|
||||
xlsxWrite.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('exports requested, sent, response, and mismatch as separate admin columns', async () => {
|
||||
const wrapper = mountRouteFilteredUsageView()
|
||||
vi.advanceTimersByTime(120)
|
||||
await flushPromises()
|
||||
|
||||
await (wrapper.vm as any).exportToExcel()
|
||||
await flushPromises()
|
||||
|
||||
const headers = aoaToSheet.mock.calls[0][0][0]
|
||||
expect(headers.slice(4, 8)).toEqual([
|
||||
'Requested model',
|
||||
'Sent upstream model',
|
||||
'Upstream response model',
|
||||
'Upstream model mismatch',
|
||||
])
|
||||
const row = sheetAddAoa.mock.calls[0][1][0]
|
||||
expect(row.slice(4, 8)).toEqual(['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.4', 'Yes'])
|
||||
expect(saveAs).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
<OpsErrorDetailsModal
|
||||
:show="showErrorDetails"
|
||||
:time-range="timeRange"
|
||||
:custom-start-time="customStartTime"
|
||||
:custom-end-time="customEndTime"
|
||||
:platform="platform"
|
||||
:group-id="groupId"
|
||||
:error-type="errorDetailsType"
|
||||
|
||||
@@ -5,10 +5,13 @@ import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import OpsErrorLogTable from './OpsErrorLogTable.vue'
|
||||
import { opsAPI, type OpsErrorLog } from '@/api/admin/ops'
|
||||
import { buildOpsErrorTimeParams } from '../utils/opsErrorParams'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
timeRange: string
|
||||
customStartTime?: string | null
|
||||
customEndTime?: string | null
|
||||
platform?: string
|
||||
groupId?: number | null
|
||||
errorType: 'request' | 'upstream'
|
||||
@@ -103,11 +106,11 @@ async function fetchErrorLogs() {
|
||||
const params: Record<string, any> = {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
time_range: props.timeRange,
|
||||
view: viewMode.value,
|
||||
sort_by: sortBy.value,
|
||||
sort_order: sortOrder.value
|
||||
}
|
||||
Object.assign(params, buildOpsErrorTimeParams(props.timeRange, props.customStartTime, props.customEndTime))
|
||||
|
||||
const platform = String(props.platform || '').trim()
|
||||
if (platform) params.platform = platform
|
||||
@@ -160,7 +163,7 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.timeRange, props.platform, props.groupId] as const,
|
||||
() => [props.timeRange, props.customStartTime, props.customEndTime, props.platform, props.groupId] as const,
|
||||
() => {
|
||||
if (!props.show) return
|
||||
page.value = 1
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildOpsErrorTimeParams } from '../opsErrorParams'
|
||||
|
||||
describe('buildOpsErrorTimeParams', () => {
|
||||
it('uses explicit timestamps for a complete custom range', () => {
|
||||
expect(buildOpsErrorTimeParams('custom', '2026-08-01T00:00:00Z', '2026-08-02T00:00:00Z')).toEqual({
|
||||
start_time: '2026-08-01T00:00:00Z',
|
||||
end_time: '2026-08-02T00:00:00Z'
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves predefined ranges and falls back for incomplete custom ranges', () => {
|
||||
expect(buildOpsErrorTimeParams('24h')).toEqual({ time_range: '24h' })
|
||||
expect(buildOpsErrorTimeParams('custom', null, null)).toEqual({ time_range: '1h' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
export function buildOpsErrorTimeParams(
|
||||
timeRange: string,
|
||||
customStartTime?: string | null,
|
||||
customEndTime?: string | null
|
||||
): Record<string, string> {
|
||||
if (timeRange === 'custom' && customStartTime && customEndTime) {
|
||||
return { start_time: customStartTime, end_time: customEndTime }
|
||||
}
|
||||
|
||||
return { time_range: timeRange === 'custom' ? '1h' : timeRange }
|
||||
}
|
||||
Reference in New Issue
Block a user