Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,12 @@ export const de: Record<TKey, string> = {
"logs.col.error": "Fehler",
"logs.col.upstreamReason": "Upstream-Grund",
"logs.col.duration": "Dauer",
"logs.modelTooltip.model": "Modell",
"logs.modelTooltip.resolvedModel": "aufgelöstes Modell",
"logs.modelTooltip.requestedTier": "angeforderte Stufe",
"logs.modelTooltip.configuredTier": "konfigurierte Stufe",
"logs.modelTooltip.responseTier": "Antwortstufe",
"logs.modelTooltip.supportsTier": "Stufenunterstützung",
"logs.tokens.reported": "gemeldet",
"logs.tokens.unreported": "nicht gemeldet",
"logs.tokens.unsupported": "nicht unterstützt",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,12 @@ export const en = {
"logs.col.error": "Error",
"logs.col.upstreamReason": "Upstream reason",
"logs.col.duration": "Duration",
"logs.modelTooltip.model": "model",
"logs.modelTooltip.resolvedModel": "resolved model",
"logs.modelTooltip.requestedTier": "requested tier",
"logs.modelTooltip.configuredTier": "configured tier",
"logs.modelTooltip.responseTier": "response tier",
"logs.modelTooltip.supportsTier": "tier support",
"logs.tokens.reported": "reported",
"logs.tokens.unreported": "unreported",
"logs.tokens.unsupported": "unsupported",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,12 @@ export const fr: Record<TKey, string> = {
"logs.col.error": "Erreur",
"logs.col.upstreamReason": "Motif en amont",
"logs.col.duration": "Durée",
"logs.modelTooltip.model": "modèle",
"logs.modelTooltip.resolvedModel": "modèle résolu",
"logs.modelTooltip.requestedTier": "niveau demandé",
"logs.modelTooltip.configuredTier": "niveau configuré",
"logs.modelTooltip.responseTier": "niveau de réponse",
"logs.modelTooltip.supportsTier": "prise en charge du niveau",
"logs.tokens.reported": "communiqués",
"logs.tokens.unreported": "non communiqués",
"logs.tokens.unsupported": "non pris en charge",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,12 @@ export const ja: Record<TKey, string> = {
"logs.col.error": "エラー",
"logs.col.upstreamReason": "上流の理由",
"logs.col.duration": "所要時間",
"logs.modelTooltip.model": "モデル",
"logs.modelTooltip.resolvedModel": "解決後モデル",
"logs.modelTooltip.requestedTier": "要求ティア",
"logs.modelTooltip.configuredTier": "設定ティア",
"logs.modelTooltip.responseTier": "応答ティア",
"logs.modelTooltip.supportsTier": "ティア対応",
"logs.tokens.reported": "報告済み",
"logs.tokens.unreported": "未報告",
"logs.tokens.unsupported": "非対応",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,12 @@ export const ko: Record<TKey, string> = {
"logs.col.error": "오류",
"logs.col.upstreamReason": "업스트림 원인",
"logs.col.duration": "소요 시간",
"logs.modelTooltip.model": "모델",
"logs.modelTooltip.resolvedModel": "해석된 모델",
"logs.modelTooltip.requestedTier": "요청 티어",
"logs.modelTooltip.configuredTier": "설정 티어",
"logs.modelTooltip.responseTier": "응답 티어",
"logs.modelTooltip.supportsTier": "티어 지원",
"logs.tokens.reported": "측정됨",
"logs.tokens.unreported": "미보고",
"logs.tokens.unsupported": "미지원",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,12 @@ export const ru: Record<TKey, string> = {
"logs.col.error": "Ошибка",
"logs.col.upstreamReason": "Причина от провайдера",
"logs.col.duration": "Длительность",
"logs.modelTooltip.model": "модель",
"logs.modelTooltip.resolvedModel": "разрешённая модель",
"logs.modelTooltip.requestedTier": "запрошенный уровень",
"logs.modelTooltip.configuredTier": "настроенный уровень",
"logs.modelTooltip.responseTier": "уровень ответа",
"logs.modelTooltip.supportsTier": "поддержка уровня",
"logs.tokens.reported": "сообщено",
"logs.tokens.unreported": "не сообщено",
"logs.tokens.unsupported": "не поддерживается",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,12 @@ export const tr: Record<TKey, string> = {
"logs.col.error": "Hata",
"logs.col.upstreamReason": "Yukarı akış nedeni",
"logs.col.duration": "Süre",
"logs.modelTooltip.model": "model",
"logs.modelTooltip.resolvedModel": "çözümlenen model",
"logs.modelTooltip.requestedTier": "istenen katman",
"logs.modelTooltip.configuredTier": "yapılandırılan katman",
"logs.modelTooltip.responseTier": "yanıt katmanı",
"logs.modelTooltip.supportsTier": "katman desteği",
"logs.tokens.reported": "bildirilen",
"logs.tokens.unreported": "bildirilmeyen",
"logs.tokens.unsupported": "desteklenmeyen",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ export const zhTW: Record<TKey, string> = {
"logs.col.error": "錯誤",
"logs.col.upstreamReason": "上游原因",
"logs.col.duration": "耗時",
"logs.modelTooltip.model": "模型",
"logs.modelTooltip.resolvedModel": "解析後模型",
"logs.modelTooltip.requestedTier": "請求層級",
"logs.modelTooltip.configuredTier": "設定層級",
"logs.modelTooltip.responseTier": "回應層級",
"logs.modelTooltip.supportsTier": "支援層級",
"logs.tokens.reported": "已上報",
"logs.tokens.unreported": "未上報",
"logs.tokens.unsupported": "不支援",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,12 @@ export const zh: Record<TKey, string> = {
"logs.col.error": "错误",
"logs.col.upstreamReason": "上游原因",
"logs.col.duration": "耗时",
"logs.modelTooltip.model": "模型",
"logs.modelTooltip.resolvedModel": "解析后模型",
"logs.modelTooltip.requestedTier": "请求层级",
"logs.modelTooltip.configuredTier": "配置层级",
"logs.modelTooltip.responseTier": "响应层级",
"logs.modelTooltip.supportsTier": "支持层级",
"logs.tokens.reported": "已上报",
"logs.tokens.unreported": "未上报",
"logs.tokens.unsupported": "不支持",
Expand Down
53 changes: 3 additions & 50 deletions gui/src/pages/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import Debug from "./Debug";

import type { LogsTab } from "./logs-tab-keydown";
import { logsTabKeyDown, readTabFromHash, selectLogsTab } from "./logs-tab-keydown";
import { modelTitle } from "./logs-model-title";
import { speedLabel } from "./logs-speed-label";
import { cacheSplit, isCursorUsageProvider, tokensTitle } from "./logs-token-title";
import type { LogSurface, LogSurfaceFilter } from "./logs-surface-filter";
import { logMatchesSurface } from "./logs-surface-filter";
import {
Expand Down Expand Up @@ -177,30 +179,6 @@ function validCachedLogs(cached: LogEntry[] | null): LogEntry[] | null {
return cached;
}

function isCursorUsageProvider(provider: string): boolean {
return provider === "cursor" || provider.startsWith("cursor-");
}

function tokensTitle(log: LogEntry, t: TFn): string | undefined {
if (!log.usage) return undefined;
const split = cacheSplit(log);
const parts = [
`${t("logs.tokens.input")}=${log.usage.inputTokens}`,
`${t("logs.tokens.output")}=${log.usage.outputTokens}`,
];
if (split.read !== undefined) parts.push(`${t("logs.tokens.cacheRead")}=${split.read}`);
if (split.write !== undefined) parts.push(`${t("logs.tokens.cacheWrite")}=${split.write}`);
if (typeof log.usage.contextTotalTokens === "number") {
parts.push(`${t("logs.tokens.contextTotal")}=${log.usage.contextTotalTokens}`);
}
if (typeof log.usage.reasoningOutputTokens === "number") parts.push(`${t("logs.tokens.reasoning")}=${log.usage.reasoningOutputTokens}`);
if (log.usageStatus === "estimated") parts.push(t("logs.tokens.estimatedNote"));
if (log.usageStatus === "estimated" && split.read === undefined && split.write === undefined) {
parts.push(t(isCursorUsageProvider(log.provider) ? "logs.tokens.noCacheCursorNote" : "logs.tokens.noCacheNote"));
}
return parts.join(" \xC2\xB7 ");
}

function displayTokenTotal(log: LogEntry): number | undefined {
if (!log.usage) return typeof log.totalTokens === "number" ? log.totalTokens : undefined;
// inputTokens is inclusive of cache read/write (canonical convention, devlog 070);
Expand All @@ -227,19 +205,6 @@ function displayContextTokenTotal(log: LogEntry): number | undefined {
return Math.max(base ?? 0, contextTotal) || undefined;
}

/** Cache read/write split; recovers reads from legacy rows that stored read+write combined. */
function cacheSplit(log: LogEntry): { read?: number; write?: number } {
const u = log.usage;
if (!u) return {};
const write = typeof u.cacheCreationInputTokens === "number" ? u.cacheCreationInputTokens : undefined;
const read = typeof u.cacheReadInputTokens === "number"
? u.cacheReadInputTokens
: typeof u.cachedInputTokens === "number" && write !== undefined
? Math.max(0, u.cachedInputTokens - write)
: u.cachedInputTokens;
return { read, write };
}

interface ReasoningLogFields {
requestedEffort?: string;
effectiveEffort?: string;
Expand Down Expand Up @@ -376,18 +341,6 @@ function formatLogDateTime(ts: number, localeTag?: string, timeZone?: string): s
return `${date} ${time}`;
}

function modelTitle(log: LogEntry): string {
const details = [
`model=${log.model}`,
log.resolvedModel ? `resolved=${log.resolvedModel}` : undefined,
log.requestedServiceTier ? `requestedTier=${log.requestedServiceTier}` : undefined,
log.configuredServiceTier ? `configuredTier=${log.configuredServiceTier}` : undefined,
log.responseServiceTier ? `responseTier=${log.responseServiceTier}` : undefined,
log.modelSupportsServiceTier !== undefined ? `supportsTier=${log.modelSupportsServiceTier}` : undefined,
].filter(Boolean);
return details.join(" \xC2\xB7 ");
}

function summarizeFilteredLogs(entries: LogEntry[]): {
requests: number;
totalTokens: number;
Expand Down Expand Up @@ -780,7 +733,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
<td className="num mono log-col-cost">
{formatEstimatedUsd(log.displayMetrics?.cost, localeTag)}
</td>
<td className="mono log-col-model" title={modelTitle(log)}>
<td className="mono log-col-model" title={modelTitle(log, t)}>
<span className="logs-model-cell">
<span>{modelLabel(log.resolvedModel ?? log.model)}</span>
{(log.surface === "claude" || log.surface === "claude-desktop") && (
Expand Down
24 changes: 24 additions & 0 deletions gui/src/pages/logs-model-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { TFn } from "../i18n/shared";

export interface ModelTitleEntry {
model: string;
resolvedModel?: string;
requestedServiceTier?: string;
configuredServiceTier?: string;
responseServiceTier?: string;
modelSupportsServiceTier?: boolean;
}

export function modelTitle(log: ModelTitleEntry, t: TFn): string {
const details = [
`${t("logs.modelTooltip.model")}=${log.model}`,
log.resolvedModel ? `${t("logs.modelTooltip.resolvedModel")}=${log.resolvedModel}` : undefined,
log.requestedServiceTier ? `${t("logs.modelTooltip.requestedTier")}=${log.requestedServiceTier}` : undefined,
log.configuredServiceTier ? `${t("logs.modelTooltip.configuredTier")}=${log.configuredServiceTier}` : undefined,
log.responseServiceTier ? `${t("logs.modelTooltip.responseTier")}=${log.responseServiceTier}` : undefined,
log.modelSupportsServiceTier !== undefined
? `${t("logs.modelTooltip.supportsTier")}=${log.modelSupportsServiceTier}`
: undefined,
].filter(Boolean);
return details.join(" \u00B7 ");
}
56 changes: 56 additions & 0 deletions gui/src/pages/logs-token-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { TFn } from "../i18n/shared";

interface TokenUsageBreakdown {
inputTokens: number;
outputTokens: number;
contextTotalTokens?: number;
cachedInputTokens?: number;
cacheReadInputTokens?: number;
cacheCreationInputTokens?: number;
reasoningOutputTokens?: number;
}

export interface TokenTitleEntry {
provider: string;
usageStatus?: string;
usage?: TokenUsageBreakdown;
}

export function isCursorUsageProvider(provider: string): boolean {
return provider === "cursor" || provider.startsWith("cursor-");
}

/** Cache read/write split; recovers reads from legacy rows that stored read+write combined. */
export function cacheSplit(log: TokenTitleEntry): { read?: number; write?: number } {
const u = log.usage;
if (!u) return {};
const write = typeof u.cacheCreationInputTokens === "number" ? u.cacheCreationInputTokens : undefined;
const read = typeof u.cacheReadInputTokens === "number"
? u.cacheReadInputTokens
: typeof u.cachedInputTokens === "number" && write !== undefined
? Math.max(0, u.cachedInputTokens - write)
: u.cachedInputTokens;
return { read, write };
}

export function tokensTitle(log: TokenTitleEntry, t: TFn): string | undefined {
if (!log.usage) return undefined;
const split = cacheSplit(log);
const parts = [
`${t("logs.tokens.input")}=${log.usage.inputTokens}`,
`${t("logs.tokens.output")}=${log.usage.outputTokens}`,
];
if (split.read !== undefined) parts.push(`${t("logs.tokens.cacheRead")}=${split.read}`);
if (split.write !== undefined) parts.push(`${t("logs.tokens.cacheWrite")}=${split.write}`);
if (typeof log.usage.contextTotalTokens === "number") {
parts.push(`${t("logs.tokens.contextTotal")}=${log.usage.contextTotalTokens}`);
}
if (typeof log.usage.reasoningOutputTokens === "number") {
parts.push(`${t("logs.tokens.reasoning")}=${log.usage.reasoningOutputTokens}`);
}
if (log.usageStatus === "estimated") parts.push(t("logs.tokens.estimatedNote"));
if (log.usageStatus === "estimated" && split.read === undefined && split.write === undefined) {
parts.push(t(isCursorUsageProvider(log.provider) ? "logs.tokens.noCacheCursorNote" : "logs.tokens.noCacheNote"));
}
return parts.join(" \u00B7 ");
}
37 changes: 37 additions & 0 deletions gui/tests/logs-model-title.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from "bun:test";
import type { TFn, TKey } from "../src/i18n/shared";
import { modelTitle, type ModelTitleEntry } from "../src/pages/logs-model-title";

const labels: Partial<Record<TKey, string>> = {
"logs.modelTooltip.model": "模型",
"logs.modelTooltip.resolvedModel": "解析后模型",
"logs.modelTooltip.requestedTier": "请求层级",
"logs.modelTooltip.configuredTier": "配置层级",
"logs.modelTooltip.responseTier": "响应层级",
"logs.modelTooltip.supportsTier": "支持层级",
};

const t: TFn = key => labels[key] ?? key;

function entry(fields: Partial<ModelTitleEntry> = {}): ModelTitleEntry {
return {
model: "gpt-5.6-sol",
...fields,
};
}

test("model diagnostics localize every label and use one Unicode middle dot between fields", () => {
expect(modelTitle(entry({
resolvedModel: "gpt-5.6-sol",
requestedServiceTier: "priority",
configuredServiceTier: "fast",
responseServiceTier: "default",
modelSupportsServiceTier: true,
}), t)).toBe(
"模型=gpt-5.6-sol · 解析后模型=gpt-5.6-sol · 请求层级=priority · 配置层级=fast · 响应层级=default · 支持层级=true",
);
});

test("model diagnostics do not include an extra Latin capital A with circumflex", () => {
expect(modelTitle(entry({ resolvedModel: "gpt-5.6-sol" }), t)).not.toContain("\u00C2");
});
35 changes: 35 additions & 0 deletions gui/tests/logs-token-title.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import type { TFn } from "../src/i18n/shared";
import { tokensTitle } from "../src/pages/logs-token-title";

const labels: Record<string, string> = {
"logs.tokens.input": "输入",
"logs.tokens.output": "输出",
"logs.tokens.cacheRead": "缓存命中 (c)",
"logs.tokens.cacheWrite": "缓存写入 (w)",
"logs.tokens.reasoning": "推理",
};

const t = ((key: string) => labels[key] ?? key) as TFn;

test("token diagnostics use one Unicode middle dot between localized fields", () => {
expect(tokensTitle({
provider: "openai",
usage: {
inputTokens: 59_375,
outputTokens: 553,
cacheReadInputTokens: 54_272,
cacheCreationInputTokens: 0,
reasoningOutputTokens: 237,
},
}, t)).toBe(
"输入=59375 · 输出=553 · 缓存命中 (c)=54272 · 缓存写入 (w)=0 · 推理=237",
);
});

test("token diagnostics do not include an extra Latin capital A with circumflex", () => {
expect(tokensTitle({
provider: "openai",
usage: { inputTokens: 1, outputTokens: 2 },
}, t)).not.toContain("\u00C2");
});
Loading
Loading