:last-child {
min-width: 0;
}
+.modern-log-speed {
+ font-family: var(--modern-font-mono);
+ font-size: var(--modern-font-size-small);
+ font-variant-numeric: tabular-nums;
+}
.modern-log-cell-value.is-model {
font-family: var(--modern-font-mono);
font-size: var(--modern-font-size-small);
diff --git a/web/src/frontends/modern/features/logs/LogValue.vue b/web/src/frontends/modern/features/logs/LogValue.vue
index 2f78c030..ae9602b4 100644
--- a/web/src/frontends/modern/features/logs/LogValue.vue
+++ b/web/src/frontends/modern/features/logs/LogValue.vue
@@ -22,9 +22,11 @@ import {
logHasUsage,
logMoney,
logNumber,
+ logOutputRate,
logStatusTone,
logTime,
} from './log-display'
+import LogCredentialValue from './LogCredentialValue.vue'
const props = defineProps<{
row: LogEntry
@@ -44,6 +46,11 @@ const channel = computed(() =>
function valueName(value: string | null | undefined): string {
return !value ? '—' : te('logs.values.' + value) ? t('logs.values.' + value) : value
}
+const affinityReason = computed(() => {
+ if (!props.row.affinity_hit) return ''
+ const key = 'logs.affinityKinds.' + props.row.affinity_kind
+ return te(key) ? t(key) : t('logs.affinityKinds.other')
+})
const tokenValue = computed(() => {
const row = props.row
if (props.column === 'cache_write_tokens') return logCacheWrites(row)
@@ -176,6 +183,14 @@ const hint = computed(() => {
:value="row.request_id"
:label="t('logs.copyRequest')"
/>
+
{
v-else-if="column === 'protocol' || column === 'upstream_protocol'"
:protocol="row[column]"
/>
+
+ {{ display }}
+
+
+ {{ display }}
+
+ {{
+ logOutputRate(row, locale)
+ }}
+
+
{
.modern-log-deleted {
color: var(--modern-muted);
}
+.modern-log-value-stack {
+ display: inline-grid;
+ min-width: 0;
+ max-width: 100%;
+ gap: var(--modern-space-0-5);
+}
.modern-log-channel {
display: flex;
align-items: center;
diff --git a/web/src/frontends/modern/features/logs/log-columns.ts b/web/src/frontends/modern/features/logs/log-columns.ts
index 65e5101c..251a1324 100644
--- a/web/src/frontends/modern/features/logs/log-columns.ts
+++ b/web/src/frontends/modern/features/logs/log-columns.ts
@@ -198,7 +198,7 @@ export function useLogColumns(admin: boolean) {
['route_mode', 'affinity_hit', 62],
['status', 'status_code', 76],
['error_code', 'error_summary', 200],
- ['duration_ms', 'first_response_ms', 72],
+ ['duration_ms', 'first_response_ms', 132],
['input_tokens', 'output_tokens', 68],
['cache_read_tokens', 'cache_hit_rate', 68],
['cache_write_tokens', 'total_tokens', 68],
diff --git a/web/src/frontends/modern/features/logs/log-display.ts b/web/src/frontends/modern/features/logs/log-display.ts
index f99b2ed5..d2cbc051 100644
--- a/web/src/frontends/modern/features/logs/log-display.ts
+++ b/web/src/frontends/modern/features/logs/log-display.ts
@@ -9,11 +9,7 @@ export const logStatusTone: Record = {
canceled: 'neutral',
}
export function logModelMismatch(row: LogEntry): boolean {
- return (
- row.model_consistency === 'mismatch' &&
- Boolean(row.upstream_model) &&
- Boolean(row.upstream_reported_model)
- )
+ return row.model_consistency === 'mismatch'
}
export function logCanMergeError(row: LogEntry): boolean {
// 中断或失败仍可能产生实际消耗;真实的零用量/零费用也不能当成缺失。
@@ -65,6 +61,21 @@ export function logDuration(ms: number | null, locale: string): string {
}).format(ms < 1000 ? ms : ms / 1000) + (ms < 1000 ? ' ms' : ' s')
)
}
+export function logOutputRate(row: LogEntry, locale: string): string {
+ if (
+ !row.stream ||
+ !logHasUsage(row) ||
+ row.first_response_ms === null ||
+ row.duration_ms <= row.first_response_ms
+ )
+ return '—'
+ const output = Number(row.output_tokens)
+ if (!Number.isSafeInteger(output) || output <= 0) return '—'
+ const rate = output / ((row.duration_ms - row.first_response_ms) / 1000)
+ return Number.isFinite(rate)
+ ? numberFormatter(locale, { maximumFractionDigits: 1 }).format(rate) + ' t/s'
+ : '—'
+}
export function logTime(ms: number, locale: string, full = false): string {
return dateFormatter(locale, {
...(full ? { year: 'numeric' as const } : {}),
diff --git a/web/src/frontends/modern/features/logs/log-filters.ts b/web/src/frontends/modern/features/logs/log-filters.ts
index 9f49784a..1b10cd87 100644
--- a/web/src/frontends/modern/features/logs/log-filters.ts
+++ b/web/src/frontends/modern/features/logs/log-filters.ts
@@ -4,6 +4,7 @@ import {
internalLogFilters,
logCursorPattern,
logFilterNames,
+ logOperations,
logRequestPattern,
logStatuses,
type LogFilterName,
@@ -29,6 +30,7 @@ export interface LogFilterDefinition {
export const logFilterOptions: Partial> = {
status: logStatuses,
protocol: accessProtocols,
+ operation: logOperations,
stream: ['true', 'false'],
cache_present: ['true', 'false'],
usage_state: ['complete', 'partial', 'missing', 'not_applicable'],
@@ -52,6 +54,7 @@ export const logFilterOptions: Partial>
export const advancedLogFilters: readonly LogFilterDefinition[] = [
{ key: 'request_id', section: 'request', kind: 'text' },
{ key: 'protocol', section: 'request', kind: 'select', values: accessProtocols },
+ { key: 'operation', section: 'request', kind: 'select', values: logOperations },
{ key: 'stream', section: 'request', kind: 'select', values: logFilterOptions.stream },
{
key: 'retry_state',
@@ -62,6 +65,7 @@ export const advancedLogFilters: readonly LogFilterDefinition[] = [
},
{ key: 'retry_count_min', section: 'routing', kind: 'number', admin: true },
{ key: 'retry_count_max', section: 'routing', kind: 'number', admin: true },
+ { key: 'upstream_model', section: 'routing', kind: 'text', admin: true },
{ key: 'final_status_code', section: 'result', kind: 'number' },
{
key: 'model_consistency',
diff --git a/web/src/frontends/modern/i18n/locales/logs.ts b/web/src/frontends/modern/i18n/locales/logs.ts
index 1bbb90ae..dc22f51d 100644
--- a/web/src/frontends/modern/i18n/locales/logs.ts
+++ b/web/src/frontends/modern/i18n/locales/logs.ts
@@ -9,6 +9,16 @@ export const zhCN = {
modelUpstream: '上游模型',
},
returnedModelMismatch: '返回模型不一致',
+ returnedModelUnknown: '返回模型未知',
+ returnedModelUnknownHint: '请求上游模型:{requested}\n上游未返回模型标识,无法确认模型是否一致。',
+ modelNotObserved: '未观测到',
+ outputRate: '输出速度',
+ affinityKinds: {
+ prompt_prefix: '提示词前缀',
+ prompt_cache_key: '缓存键',
+ response_continuity: '响应连续性',
+ other: '亲和命中',
+ },
standaloneSearchHint: 'Codex 独立搜索请求',
returnedModelMismatchHint: '请求上游模型:{requested}\n上游返回模型:{returned}',
columnGroups: {
@@ -58,6 +68,7 @@ export const zhCN = {
selectGroupFirst: '先选择分组',
copyRequest: '复制请求 ID',
copyError: '复制错误代码',
+ copyCredential: '复制凭据',
copyRedactedLog: '复制脱敏日志',
redactedCopySuccess: '已复制脱敏副本,账号、密钥等标识已匿名化,常见敏感内容已处理。',
redactedCopyFailed: '脱敏日志复制失败,请重试。',
@@ -147,6 +158,7 @@ export const zhCN = {
error_summary: '错误详情',
},
filters: {
+ operation: '操作',
group_id: '分组',
channel_id: '渠道',
credential_id: '账号',
@@ -264,6 +276,7 @@ export const zhCN = {
responses_input_tokens: '输入 Token 统计',
count_tokens: 'Token 统计',
responses_passthrough: '响应透传',
+ web_search: '独立搜索',
images_generate: '图片生成',
images_edit: '图片编辑',
embeddings_create: '向量生成',
@@ -291,6 +304,17 @@ export const enUS: typeof zhCN = {
modelUpstream: 'Upstream model',
},
returnedModelMismatch: 'Returned model mismatch',
+ returnedModelUnknown: 'Returned model unknown',
+ returnedModelUnknownHint:
+ 'Requested upstream model: {requested}\nThe upstream did not report a model identifier, so consistency cannot be verified.',
+ modelNotObserved: 'Not observed',
+ outputRate: 'Output speed',
+ affinityKinds: {
+ prompt_prefix: 'Prompt prefix',
+ prompt_cache_key: 'Cache key',
+ response_continuity: 'Response continuity',
+ other: 'Affinity match',
+ },
standaloneSearchHint: 'Codex standalone search request',
returnedModelMismatchHint:
'Requested upstream model: {requested}\nReported upstream model: {returned}',
@@ -342,6 +366,7 @@ export const enUS: typeof zhCN = {
selectGroupFirst: 'Select a group first',
copyRequest: 'Copy request ID',
copyError: 'Copy error code',
+ copyCredential: 'Copy credential',
copyRedactedLog: 'Copy sanitized log',
redactedCopySuccess:
'Sanitized log copied. Account and key identities are anonymized; common sensitive content has been redacted.',
@@ -433,6 +458,7 @@ export const enUS: typeof zhCN = {
},
filters: {
group_id: 'Group',
+ operation: 'Operation',
channel_id: 'Channel',
credential_id: 'Account',
access_key_id: 'Access key',
@@ -549,6 +575,7 @@ export const enUS: typeof zhCN = {
responses_input_tokens: 'Input token count',
count_tokens: 'Count tokens',
responses_passthrough: 'Response passthrough',
+ web_search: 'Standalone search',
images_generate: 'Generate image',
images_edit: 'Edit image',
embeddings_create: 'Create embedding',
@@ -576,6 +603,17 @@ export const jaJP: typeof zhCN = {
modelUpstream: '上流モデル',
},
returnedModelMismatch: '返却モデルが不一致',
+ returnedModelUnknown: '返却モデルが不明',
+ returnedModelUnknownHint:
+ '要求した上流モデル:{requested}\n上流からモデル識別子が返されなかったため、一致を確認できません。',
+ modelNotObserved: '未観測',
+ outputRate: '出力速度',
+ affinityKinds: {
+ prompt_prefix: 'プロンプト接頭辞',
+ prompt_cache_key: 'キャッシュキー',
+ response_continuity: '応答の連続性',
+ other: 'アフィニティ一致',
+ },
standaloneSearchHint: 'Codex の独立検索リクエスト',
returnedModelMismatchHint: '要求した上流モデル:{requested}\n上流が返却したモデル:{returned}',
columnGroups: {
@@ -625,6 +663,7 @@ export const jaJP: typeof zhCN = {
selectGroupFirst: '先にグループを選択',
copyRequest: 'リクエスト ID をコピー',
copyError: 'エラーコードをコピー',
+ copyCredential: '認証情報をコピー',
copyRedactedLog: '匿名化ログをコピー',
redactedCopySuccess:
'匿名化ログをコピーしました。アカウントとキーの識別情報を匿名化し、一般的な機密情報を処理しました。',
@@ -716,6 +755,7 @@ export const jaJP: typeof zhCN = {
},
filters: {
group_id: 'グループ',
+ operation: '操作',
channel_id: 'チャネル',
credential_id: 'アカウント',
access_key_id: 'アクセスキー',
@@ -832,6 +872,7 @@ export const jaJP: typeof zhCN = {
responses_input_tokens: '入力 Token 数',
count_tokens: 'Token 計数',
responses_passthrough: '応答パススルー',
+ web_search: '独立検索',
images_generate: '画像生成',
images_edit: '画像編集',
embeddings_create: '埋め込み生成',