mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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>
435 lines
12 KiB
TypeScript
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')
|
|
}
|