Files
codex-proxy-rs/frontend/src/views/usage/constants.ts
T
zyycnandCodex f0ccc981fb feat(proxy): align latest Responses semantics
Preserve Responses Lite and Memgen controls across HTTP and WebSocket transports without rewriting request bodies, and standardize continuation recovery signals.

Track cache-write usage and billing while applying appcast profile updates to new WebSocket generations without breaking exact continuations.

Co-authored-by: Codex <noreply@openai.com>
2026-07-17 00:50:27 +08:00

435 lines
12 KiB
TypeScript

import { formatDuration } from './utils/format'
export const usageRecordColumns = [
{
key: 'accountEmail',
label: '账号',
width: '250px',
ellipsis: true,
},
{
key: 'model',
label: '模型',
width: '160px',
ellipsis: false,
},
{
key: 'reasoningEffort',
label: '推理强度',
width: '98px',
ellipsis: false,
cellClass: 'whitespace-nowrap text-[12px] font-[700] text-(--cp-text-primary)',
},
{
key: 'route',
label: '端点',
width: '185px',
ellipsis: false,
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'recordType',
label: '类型',
width: '78px',
align: 'center' as const,
ellipsis: false,
},
{
key: 'tokenDetails',
label: 'TOKEN',
width: '184px',
align: 'right' as const,
ellipsis: false,
},
{
key: 'billing',
label: '费用',
width: '132px',
align: 'right' as const,
ellipsis: false,
},
{
key: 'latency',
label: '延迟',
width: '156px',
align: 'right' as const,
ellipsis: false,
},
{
key: 'createdAtDisplay',
label: '时间',
width: '190px',
ellipsis: false,
cellClass:
'whitespace-nowrap font-mono text-[12px] font-[650] tabular-nums text-(--cp-text-secondary)',
},
{
key: 'clientIp',
label: 'IP',
width: '240px',
ellipsis: false,
},
{
key: 'userAgent',
label: 'User-Agent',
width: '340px',
ellipsis: false,
cellClass:
'whitespace-normal break-words text-[12px] leading-[1.45] font-[650] text-(--cp-text-secondary)',
},
{
key: 'actions',
label: '操作',
width: '92px',
ellipsis: false,
headerClass: '!px-4',
cellClass: '!px-4',
},
]
export const opsErrorColumns = [
{
key: 'createdAtDisplay',
label: '时间',
width: '190px',
cellClass:
'whitespace-nowrap font-mono text-[12px] font-[650] tabular-nums text-(--cp-text-secondary)',
},
{ key: 'statusCode', label: '状态码', width: '96px', align: 'center' as const },
{
key: 'failureClass',
label: '失败分类',
width: '170px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'kind',
label: '事件',
width: '170px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'route',
label: '端点',
width: '190px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'model',
label: '模型',
width: '180px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'accountId',
label: '账号 ID',
width: '230px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{
key: 'requestId',
label: '请求 ID',
width: '250px',
cellClass: 'font-mono text-[12px] font-[650]',
},
{ key: 'message', label: '消息', minWidth: '300px', flex: 1 },
{
key: 'actions',
label: '操作',
width: '80px',
headerClass: '!px-4',
cellClass: '!px-4',
},
]
export const usageTimeRangeOptions = [
{ label: '今天', value: 'today' },
{ label: '最近 7 天', value: '7d' },
{ label: '最近 30 天', value: '30d' },
]
export function tokenTotal(record: any) {
const tokenDetails = record?.tokenDetails
if (tokenDetails) {
return numberValue(tokenDetails.totalTokens)
}
const usage = record?.metadata?.usage
if (!usage) {
return 0
}
return (
numberValue(usage.totalTokens) ||
numberValue(usage.inputTokens) + numberValue(usage.outputTokens)
)
}
export function formatTokenCount(value: number) {
if (!value) {
return '—'
}
return new Intl.NumberFormat('zh-CN').format(value)
}
export function formatUsageMetric(value: number) {
return new Intl.NumberFormat('zh-CN').format(value || 0)
}
export function formatCompactUsageMetric(value: number) {
const num = Number(value || 0)
if (num >= 1_000_000_000) return `${formatCompact(num / 1_000_000_000)}B`
if (num >= 1_000_000) return `${formatCompact(num / 1_000_000)}M`
if (num >= 1_000) return `${formatCompact(num / 1_000)}K`
return new Intl.NumberFormat('zh-CN').format(num)
}
function formatCompact(value: number) {
const rounded =
value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2)
return rounded.replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1')
}
export function formatLatencyAverage(value: number | null) {
if (value === null || value === undefined) {
return '—'
}
return `${Math.round(value)} ms`
}
export function usageRecordType(record: any) {
if (record?.transport === 'websocket') {
return 'WS'
}
if (record?.metadata?.stream === true || record?.transport === 'http_sse') {
return 'SSE'
}
if (record?.metadata?.stream === false) {
return 'HTTP'
}
return record?.metadata?.apiKind === 'chat' ? 'Chat' : 'HTTP'
}
export function usageRecordTypeClass(record: any) {
const type = usageRecordType(record)
if (type === 'WS') return 'bg-(--cp-info-bg) text-(--cp-info-text)'
if (type === 'SSE') return 'bg-(--cp-success-bg) text-(--cp-success-text)'
if (type === 'Chat') return 'bg-(--cp-warning-bg) text-(--cp-warning-text)'
return 'bg-(--cp-bg-subtle) text-(--cp-text-secondary)'
}
export function usageAccountText(record: any) {
return record?.accountEmail || '—'
}
export function usageClientIp(record: any) {
return record?.clientIp || record?.metadata?.clientIp || '—'
}
export function usageUserAgent(record: any) {
return record?.userAgent || record?.metadata?.userAgent || '—'
}
export function usageReasoningEffort(record: any) {
const reasoningEffort = record?.reasoningEffort || record?.metadata?.reasoningEffort || '—'
if (usageIsSubagent(record)) {
return reasoningEffort
}
return record?.reasoningPreset || record?.metadata?.reasoningPreset || reasoningEffort
}
export function usageIsSubagent(record: any) {
return Boolean(record?.subagentKind)
}
export function usageIsCompact(record: any) {
return record?.compact === true || record?.metadata?.compact === true
}
export function usageModelDisplay(record: any) {
const requestedModel = record?.requestedModel || record?.metadata?.requestedModel || ''
const upstreamModel = record?.upstreamModel || record?.metadata?.upstreamModel || ''
const storedModel = record?.model || ''
const primary = requestedModel || storedModel || upstreamModel || '—'
const secondary =
upstreamModel && upstreamModel !== primary
? upstreamModel
: requestedModel && storedModel && storedModel !== requestedModel
? storedModel
: ''
return { primary, secondary }
}
export function usageTokenDetails(record: any) {
const details = record?.tokenDetails
if (details) {
return details
}
const usage = record?.metadata?.usage || {}
const inputTokens = numberValue(usage.inputTokens)
const outputTokens = numberValue(usage.outputTokens)
const cachedTokens = numberValue(usage.cachedTokens)
const cacheWriteTokens = numberValue(usage.cacheWriteTokens)
const reasoningTokens = numberValue(usage.reasoningTokens)
const totalTokens = numberValue(usage.totalTokens) || inputTokens + outputTokens
return {
inputTokens,
outputTokens,
cachedTokens,
cacheWriteTokens,
reasoningTokens,
totalTokens,
inputTokensDisplay: formatTokenCount(inputTokens),
outputTokensDisplay: formatTokenCount(outputTokens),
cachedTokensDisplay: formatCompactTokenCount(cachedTokens),
cacheWriteTokensDisplay: formatCompactTokenCount(cacheWriteTokens),
reasoningTokensDisplay: formatTokenCount(reasoningTokens),
totalTokensDisplay: formatTokenCount(totalTokens),
}
}
export function usageLatencyDetails(record: any) {
const latencyDetails = record?.latencyDetails || record?.metadata || {}
const firstTokenMs = durationValue(record?.firstTokenLatencyMs ?? latencyDetails.firstTokenMs)
const totalMs = durationValue(record?.latencyMs)
const firstReasoningMs = durationValue(latencyDetails.firstReasoningMs)
const firstTextMs = durationValue(latencyDetails.firstTextMs)
const breakdownItems = []
if (firstTokenMs !== null && totalMs !== null && firstTokenMs <= totalMs) {
breakdownItems.push({ label: '首字等待', value: formatDuration(firstTokenMs) })
if (firstTextMs !== null && firstTextMs >= firstTokenMs && firstTextMs <= totalMs) {
const beforeTextMs = firstTextMs - firstTokenMs
if (beforeTextMs > 0) {
breakdownItems.push({
label: firstReasoningMs === firstTokenMs ? '推理到正文' : '首个输出到正文',
value: formatDuration(beforeTextMs),
})
}
breakdownItems.push({ label: '正文生成', value: formatDuration(totalMs - firstTextMs) })
} else {
breakdownItems.push({
label: '首个输出后完成',
value: formatDuration(totalMs - firstTokenMs),
})
}
}
const transportItems = [
{ label: '传输决策等待', value: durationValue(latencyDetails.transportDecisionWaitMs) },
{ label: 'WebSocket 连接', value: durationValue(latencyDetails.wsConnectMs) },
{ label: '上游响应头', value: durationValue(latencyDetails.upstreamHeadersMs) },
{ label: '首个上游事件', value: durationValue(latencyDetails.firstEventMs) },
{ label: '上游处理', value: durationValue(latencyDetails.openaiProcessingMs) },
]
.filter((item) => item.value !== null)
.map((item) => ({ ...item, value: formatDuration(item.value) }))
return {
firstTokenDisplay: formatDuration(firstTokenMs),
totalDisplay: formatDuration(totalMs),
breakdownItems,
transportItems,
}
}
export function usageBilling(record: any) {
return record?.billing || null
}
export function usageBillingText(record: any) {
return usageBilling(record)?.totalAmountDisplay || '—'
}
function formatCompactTokenCount(value: number) {
if (!value) return '—'
if (value < 1000) return formatTokenCount(value)
if (value < 1_000_000) return `${Number((value / 1000).toFixed(value >= 100_000 ? 0 : 1))}K`
return `${Number((value / 1_000_000).toFixed(value >= 100_000_000 ? 0 : 1))}M`
}
export function visibleRequestText(record: any) {
const body = record?.metadata?.requestBody
if (!body) {
return ''
}
return extractInputText(body) || JSON.stringify(body, null, 2)
}
export function visibleResponseText(record: any) {
const body = record?.metadata?.responseBody
if (!body) {
return ''
}
if (typeof body === 'string') {
return body
}
return body.output_text || extractOutputText(body) || JSON.stringify(body, null, 2)
}
function numberValue(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) ? value : 0
}
function durationValue(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
}
function extractInputText(body: any) {
const input = body?.input
if (typeof input === 'string') {
return input
}
if (!Array.isArray(input)) {
return ''
}
return input
.flatMap((item) => {
if (typeof item?.content === 'string') {
return [item.content]
}
if (!Array.isArray(item?.content)) {
return []
}
return item.content
.filter((part: any) => part?.type === 'input_text' && typeof part.text === 'string')
.map((part: any) => part.text)
})
.filter(Boolean)
.join('\n')
}
function extractOutputText(body: any) {
if (!Array.isArray(body?.output)) {
return ''
}
return body.output
.flatMap((item: any) =>
Array.isArray(item?.content)
? item.content
.filter((part: any) => typeof part?.text === 'string')
.map((part: any) => part.text)
: [],
)
.filter(Boolean)
.join('\n')
}