Merge branch 'refs/heads/dev/3.1' into dev/docs

This commit is contained in:
musi 2026-07-31 08:36:48 +08:00
commit 7aa18a0c49
9 changed files with 560 additions and 32 deletions

View file

@ -2280,7 +2280,7 @@ export type AgentAnalysisSubagentRow = {
export type AgentAnalysisTraceRunKind = "agent" | "llm" | "route" | "subagent" | "tool";
export type AgentAnalysisTraceRunStatus = "error" | "success";
export type AgentAnalysisTraceRunStatus = "error" | "partial" | "success";
export type AgentAnalysisTracePayloadPreview = {
kind: "empty" | "json" | "text";
@ -2318,6 +2318,7 @@ export type AgentAnalysisTraceRun = {
cacheReadTokens: number;
cacheWriteTokens: number;
concurrentRequests: number;
costUsd?: number;
depth: number;
durationMs: number;
endedAt: string;

View file

@ -1882,15 +1882,67 @@ function extractSubagentModel(
}
for (const payload of requestPayloads) {
const match = stringifyForSearch(payload).match(/<CCR-SUBAGENT-MODEL>(.*?)<\/CCR-SUBAGENT-MODEL>/s);
if (match?.[1]?.trim()) {
return match[1].trim();
const model = extractPayloadSubagentModel(payload);
if (model) {
return model;
}
}
return undefined;
}
function extractPayloadSubagentModel(payload: unknown): string | undefined {
if (!isRecord(payload)) {
return undefined;
}
const systemModel = extractSubagentModelFromContent(payload.system);
if (systemModel) {
return systemModel;
}
if (!Array.isArray(payload.messages)) {
return undefined;
}
for (const message of payload.messages.slice(0, 2)) {
if (!isRecord(message) || message.role !== "user") {
continue;
}
const model = extractSubagentModelFromContent(message.content);
if (model) {
return model;
}
}
return undefined;
}
function extractSubagentModelFromContent(content: unknown): string | undefined {
if (typeof content === "string") {
return extractSubagentModelFromText(content);
}
if (!Array.isArray(content)) {
return undefined;
}
for (const block of content) {
const text = typeof block === "string"
? block
: isRecord(block) && typeof block.text === "string"
? block.text
: undefined;
const model = text ? extractSubagentModelFromText(text) : undefined;
if (model) {
return model;
}
}
return undefined;
}
function extractSubagentModelFromText(text: string): string | undefined {
const match = text.match(/<CCR-SUBAGENT-MODEL>(.*?)<\/CCR-SUBAGENT-MODEL>/s);
const model = match?.[1]?.trim();
return model && model.toLowerCase() !== "provider/model" ? model : undefined;
}
function parseLogBodyPayloads(body: RequestLogBody | undefined): unknown[] {
if (!body || body.encoding !== "utf8" || !body.text.trim()) {
return [];
@ -2568,6 +2620,7 @@ function buildAgentTrace(requests: AnalyzedAgentRequest[]): AgentAnalysisTrace {
cacheReadTokens: totals.cacheReadTokens,
cacheWriteTokens: totals.cacheWriteTokens,
concurrentRequests: totals.maxConcurrentRequests,
costUsd: totals.costUsd,
depth: 0,
durationMs,
endedAt: isoFromMs(endMs),
@ -2579,7 +2632,11 @@ function buildAgentTrace(requests: AnalyzedAgentRequest[]): AgentAnalysisTrace {
outputTokens: totals.outputTokens,
sessionId,
startedAt: isoFromMs(startMs),
status: totals.errorCount > 0 ? "error" : "success",
status: totals.errorCount === 0
? "success"
: totals.errorCount === totals.requestCount
? "error"
: "partial",
totalTokens: totals.totalTokens
}
];
@ -2704,6 +2761,7 @@ function requestTraceRun({
cacheReadTokens: request.cacheReadTokens,
cacheWriteTokens: request.cacheWriteTokens,
concurrentRequests: request.concurrentRequests,
costUsd: request.costUsd,
depth,
durationMs: request.durationMs,
endedAt: isoFromMs(request.endedAtMs),

View file

@ -126,6 +126,11 @@ test("RequestLogStore applies persisted custom model pricing to raw trace usage
);
record.model = "custom-model";
record.providerName = "custom-provider";
record.requestHeaders = {
"content-type": "application/json",
"user-agent": "openai-codex test",
"x-codex-session-id": "custom-pricing-session"
};
record.pricing = {
inputUsdPerMillionTokens: 2,
outputUsdPerMillionTokens: 8
@ -141,6 +146,19 @@ test("RequestLogStore applies persisted custom model pricing to raw trace usage
let page = await store.list({ pageSize: 10 });
let detail = await store.getDetail({ id: page.items[0].id });
assert.equal(detail.costUsd, 6);
let analysis = await store.analyze({
range: "30d",
sessionAgent: "codex",
sessionId: "custom-pricing-session"
});
assert.equal(
analysis.selectedSession?.trace.runs.find((run) => run.id === analysis.selectedSession?.trace.rootRunId)?.costUsd,
6
);
assert.equal(
analysis.selectedSession?.trace.runs.find((run) => run.kind === "llm")?.costUsd,
6
);
assert.equal(await store.updateFromRawTrace({
model: "custom-model",
@ -151,6 +169,15 @@ test("RequestLogStore applies persisted custom model pricing to raw trace usage
page = await store.list({ pageSize: 10 });
detail = await store.getDetail({ id: page.items[0].id });
assert.equal(detail.costUsd, 12);
analysis = await store.analyze({
range: "30d",
sessionAgent: "codex",
sessionId: "custom-pricing-session"
});
assert.equal(
analysis.selectedSession?.trace.runs.find((run) => run.kind === "llm")?.costUsd,
12
);
} finally {
await store.close();
rmSync(dir, { force: true, recursive: true });

View file

@ -987,6 +987,10 @@ test("RequestLogStore analyzes agent sessions and exposes trace payloads", async
assert.equal(selected.selectedSession?.trace.toolRunCount, 1);
assert.equal(selected.selectedSession?.trace.llmRunCount, 1);
assert.equal(selected.selectedSession?.trace.runs.some((run) => run.toolName === "read_file"), true);
assert.equal(
selected.selectedSession?.trace.runs.find((run) => run.id === selected.selectedSession?.trace.rootRunId)?.status,
"success"
);
const inputPayload = await store.getTracePayload({
callId: "call-read",
@ -1010,6 +1014,189 @@ test("RequestLogStore analyzes agent sessions and exposes trace payloads", async
}
});
test("RequestLogStore ignores subagent markers in tool definitions and placeholder text", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-subagent-detection-test-"));
try {
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
const baseTime = Date.now() - 5000;
async function recordClaudeRequest({ body, offsetMs, sessionId }) {
const startedAtMs = baseTime + offsetMs;
await store.record({
completedAt: new Date(startedAtMs + 100).toISOString(),
durationMs: 100,
method: "POST",
path: "/v1/messages",
providerName: "test-provider",
requestBody: Buffer.from(JSON.stringify({
...body,
model: "claude-test"
}), "utf8"),
requestHeaders: {
"content-type": "application/json",
"user-agent": "claude-cli test",
"x-ccr-route-reason": "default",
"x-ccr-routed-model": "test-provider/claude-test",
"x-claude-code-session-id": sessionId
},
requestId: `${sessionId}-request`,
responseBodyText: JSON.stringify({ model: "claude-test" }),
responseHeaders: { "content-type": "application/json" },
startedAt: new Date(startedAtMs).toISOString(),
statusCode: 200,
url: "http://127.0.0.1:3456/v1/messages"
});
}
await recordClaudeRequest({
body: {
messages: [{ content: "normal main-agent request", role: "user" }],
tools: [{
description: "Use <CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL> when spawning an agent.",
input_schema: {
properties: {
prompt: {
description: "Start with <CCR-SUBAGENT-MODEL>Provider/model</CCR-SUBAGENT-MODEL>.",
type: "string"
}
},
type: "object"
},
name: "Agent"
}]
},
offsetMs: 0,
sessionId: "tool-description-session"
});
await recordClaudeRequest({
body: {
messages: [{ content: "normal request", role: "user" }],
system: "Example: <CCR-SUBAGENT-MODEL>Provider/model</CCR-SUBAGENT-MODEL>"
},
offsetMs: 1000,
sessionId: "placeholder-session"
});
await recordClaudeRequest({
body: {
messages: [{
content: "<CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL>\nInspect the repository.",
role: "user"
}]
},
offsetMs: 2000,
sessionId: "real-subagent-session"
});
const toolDescription = await store.analyze({
range: "30d",
sessionAgent: "claude-code",
sessionId: "tool-description-session"
});
assert.equal(toolDescription.selectedSession?.trace.subagentRunCount, 0);
assert.equal(toolDescription.selectedSession?.subagents.length, 0);
const placeholder = await store.analyze({
range: "30d",
sessionAgent: "claude-code",
sessionId: "placeholder-session"
});
assert.equal(placeholder.selectedSession?.trace.subagentRunCount, 0);
assert.equal(placeholder.selectedSession?.subagents.length, 0);
const realSubagent = await store.analyze({
range: "30d",
sessionAgent: "claude-code",
sessionId: "real-subagent-session"
});
assert.equal(realSubagent.selectedSession?.trace.subagentRunCount, 1);
assert.equal(realSubagent.selectedSession?.subagents[0]?.model, "Provider/claude-opus");
} finally {
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore distinguishes partial session failures from failed sessions", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-agent-status-test-"));
try {
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
const baseTime = Date.now() - 5000;
async function recordAgentRequest({ offsetMs, requestId, sessionId, statusCode }) {
const startedAtMs = baseTime + offsetMs;
await store.record({
completedAt: new Date(startedAtMs + 100).toISOString(),
durationMs: 100,
...(statusCode >= 400 ? { error: "upstream request failed" } : {}),
method: "POST",
path: "/v1/chat/completions",
providerName: "test-provider",
providerProtocol: "openai_chat_completions",
requestBody: Buffer.from(JSON.stringify({
messages: [{ content: "continue task", role: "user" }],
model: "gpt-test",
session_id: sessionId
}), "utf8"),
requestHeaders: {
"content-type": "application/json",
"user-agent": "openai-codex test",
"x-codex-session-id": sessionId
},
requestId,
responseBodyText: JSON.stringify({ model: "gpt-test" }),
responseHeaders: { "content-type": "application/json" },
startedAt: new Date(startedAtMs).toISOString(),
statusCode,
url: "http://127.0.0.1:3456/v1/chat/completions"
});
}
await recordAgentRequest({
offsetMs: 0,
requestId: "mixed-failed",
sessionId: "session-mixed",
statusCode: 502
});
await recordAgentRequest({
offsetMs: 1000,
requestId: "mixed-recovered",
sessionId: "session-mixed",
statusCode: 200
});
await recordAgentRequest({
offsetMs: 2000,
requestId: "failed-only",
sessionId: "session-failed",
statusCode: 502
});
const mixed = await store.analyze({
range: "30d",
sessionAgent: "codex",
sessionId: "session-mixed"
});
const mixedTrace = mixed.selectedSession?.trace;
assert.equal(
mixedTrace?.runs.find((run) => run.id === mixedTrace.rootRunId)?.status,
"partial"
);
assert.equal(mixedTrace?.runs.some((run) => run.kind === "llm" && run.status === "error"), true);
assert.equal(mixedTrace?.runs.some((run) => run.kind === "llm" && run.status === "success"), true);
const failed = await store.analyze({
range: "30d",
sessionAgent: "codex",
sessionId: "session-failed"
});
const failedTrace = failed.selectedSession?.trace;
assert.equal(
failedTrace?.runs.find((run) => run.id === failedTrace.rootRunId)?.status,
"error"
);
} finally {
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore agent analysis cache ratio denominator includes cache tokens when total tokens omit cache", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-cache-ratio-test-"));
try {

View file

@ -3517,7 +3517,7 @@ function AgentSessionDetailCard({
<AnalysisEmptyState label={t("No session requests")} />
) : (
<div className={cn("max-h-[260px]", agentListFrameClassName)}>
<table className={cn("min-w-[980px]", agentListTableClassName)}>
<table className={cn("min-w-[1060px]", agentListTableClassName)}>
<thead className={agentListHeadClassName}>
<tr>
<th className="px-3 py-2 font-semibold">{t("Time")}</th>
@ -3525,7 +3525,8 @@ function AgentSessionDetailCard({
<th className="px-3 py-2 font-semibold">{t("Route")}</th>
<th className="px-3 py-2 font-semibold">{t("Model")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Tools")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Tokens")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Token")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Cost")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Duration")}</th>
</tr>
</thead>
@ -3538,6 +3539,7 @@ function AgentSessionDetailCard({
<td className="max-w-[300px] px-3 py-2" title={`${request.provider}/${request.model}`}>{request.provider}/{request.model}</td>
<td className="px-3 py-2 text-right" title={request.tools.join(", ")}>{formatCompactNumber(request.toolCallCount)}</td>
<td className="px-3 py-2 text-right">{formatCompactNumber(request.totalTokens)}</td>
<td className="px-3 py-2 text-right">{formatUsdCost(request.costUsd ?? 0)}</td>
<td className="px-3 py-2 text-right">{formatDuration(request.durationMs)}</td>
</tr>
))}
@ -3557,19 +3559,22 @@ function AgentSessionDetailCard({
const agentListSurfaceClassName = "rounded-md border border-border/70 bg-card/70 shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
const agentListFrameClassName = cn("overflow-auto", agentListSurfaceClassName);
const agentListTableClassName = "w-full border-collapse text-left text-[11px]";
const agentListHeadClassName = "sticky top-0 z-10 border-b border-border/70 bg-muted/80 text-muted-foreground backdrop-blur";
const agentListHeadClassName = "sticky top-0 z-10 border-b border-border/70 bg-muted/80 text-muted-foreground backdrop-blur [&_th]:min-w-[64px] [&_th]:whitespace-nowrap";
const agentListBodyClassName = "divide-y divide-border/50";
function agentListRowClassName({
danger,
selected
selected,
warning
}: {
danger?: boolean;
selected?: boolean;
warning?: boolean;
} = {}) {
return cn(
"bg-card/40 transition-colors hover:bg-muted/30",
danger && "bg-rose-500/5 hover:bg-rose-500/10",
warning && "bg-amber-500/5 hover:bg-amber-500/10",
selected && "bg-teal-500/10 shadow-[inset_2px_0_0_rgba(20,184,166,0.7)] hover:bg-teal-500/15"
);
}
@ -3598,22 +3603,26 @@ function AgentTracePanel({ trace }: { trace: AgentTraceDetail }) {
<AnalysisEmptyState label={t("No trace runs")} />
) : (
<div className={cn("max-h-[420px]", agentListFrameClassName)}>
<table className={cn("min-w-[1180px]", agentListTableClassName)}>
<table className={cn("min-w-[1260px]", agentListTableClassName)}>
<thead className={agentListHeadClassName}>
<tr>
<th className="px-3 py-2 font-semibold">{t("Run")}</th>
<th className="px-3 py-2 font-semibold">{t("Timeline")}</th>
<th className="px-3 py-2 font-semibold">{t("Status")}</th>
<th className="px-3 py-2 font-semibold">{t("Target")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Tokens")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Token")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Cache")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Cost")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Concurrency")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Duration")}</th>
</tr>
</thead>
<tbody className={agentListBodyClassName}>
{trace.runs.map((run) => (
<tr className={agentListRowClassName({ danger: run.status === "error" })} key={run.id}>
<tr className={agentListRowClassName({
danger: run.status === "error",
warning: run.status === "partial"
})} key={run.id}>
<td className="max-w-[360px] px-3 py-2">
<div className="flex min-w-0 items-center gap-2" style={{ paddingLeft: `${Math.min(run.depth, 8) * 16}px` }}>
<span className={cn("h-2 w-2 shrink-0 rounded-full", traceRunDotClass(run))} />
@ -3636,8 +3645,8 @@ function AgentTracePanel({ trace }: { trace: AgentTraceDetail }) {
</div>
</td>
<td className="px-3 py-2">
<Badge className={cn("border", run.status === "error" ? "border-rose-200 bg-rose-50 text-rose-700" : "border-emerald-200 bg-emerald-50 text-emerald-700")} variant="outline">
{t(run.status === "error" ? "Error" : "Success")}
<Badge className={cn("border", traceRunStatusBadgeClass(run.status))} variant="outline">
{t(traceRunStatusLabel(run.status))}
</Badge>
</td>
<td className="max-w-[260px] px-3 py-2" title={traceRunTarget(run)}>
@ -3645,6 +3654,7 @@ function AgentTracePanel({ trace }: { trace: AgentTraceDetail }) {
</td>
<td className="px-3 py-2 text-right">{run.totalTokens > 0 ? formatCompactNumber(run.totalTokens) : "-"}</td>
<td className="px-3 py-2 text-right">{run.cacheReadTokens + run.cacheWriteTokens > 0 ? formatCompactNumber(run.cacheReadTokens + run.cacheWriteTokens) : "-"}</td>
<td className="px-3 py-2 text-right">{run.costUsd !== undefined ? formatUsdCost(run.costUsd) : "-"}</td>
<td className="px-3 py-2 text-right">{formatCompactNumber(run.concurrentRequests)}</td>
<td className="px-3 py-2 text-right">{formatDuration(run.durationMs)}</td>
</tr>
@ -4000,6 +4010,7 @@ function traceRunBarStyle(run: AgentAnalysisTraceRun, traceDurationMs: number):
function traceRunDotClass(run: AgentAnalysisTraceRun): string {
if (run.status === "error") return "bg-rose-500";
if (run.status === "partial") return "bg-amber-500";
if (run.kind === "agent") return "bg-teal-500";
if (run.kind === "route") return "bg-cyan-500";
if (run.kind === "subagent") return "bg-amber-500";
@ -4009,6 +4020,7 @@ function traceRunDotClass(run: AgentAnalysisTraceRun): string {
function traceRunBarClass(run: AgentAnalysisTraceRun): string {
if (run.status === "error") return "bg-rose-500";
if (run.status === "partial") return "bg-amber-500";
if (run.kind === "agent") return "bg-teal-500";
if (run.kind === "route") return "bg-cyan-500";
if (run.kind === "subagent") return "bg-amber-500";
@ -4016,6 +4028,18 @@ function traceRunBarClass(run: AgentAnalysisTraceRun): string {
return "bg-blue-500";
}
function traceRunStatusBadgeClass(status: AgentAnalysisTraceRun["status"]): string {
if (status === "error") return "border-rose-200 bg-rose-50 text-rose-700";
if (status === "partial") return "border-amber-200 bg-amber-50 text-amber-700";
return "border-emerald-200 bg-emerald-50 text-emerald-700";
}
function traceRunStatusLabel(status: AgentAnalysisTraceRun["status"]): string {
if (status === "error") return "Error";
if (status === "partial") return "Partial failure";
return "Success";
}
function formatRouteReason(value: string | undefined): string {
const trimmed = value?.trim();
if (!trimmed) {
@ -4041,7 +4065,7 @@ function AgentSessionsCard({
<AnalysisEmptyState label={t("No session activity")} />
) : (
<div className={cn("h-full", agentListFrameClassName)}>
<table className={cn("min-w-[1260px]", agentListTableClassName)}>
<table className={cn("min-w-[1420px]", agentListTableClassName)}>
<thead className={agentListHeadClassName}>
<tr>
<th className="px-3 py-2 font-semibold">{t("Session")}</th>
@ -4054,6 +4078,8 @@ function AgentSessionsCard({
<th className="px-3 py-2 text-right font-semibold">{t("Tools")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Subagents")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Errors")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Cache rate")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Cost")}</th>
<th className="px-3 py-2 font-semibold">{t("Models")}</th>
<th className="px-3 py-2 font-semibold">{t("Providers")}</th>
<th className="px-3 py-2 font-semibold">{t("UA")}</th>
@ -4077,6 +4103,8 @@ function AgentSessionsCard({
<td className="px-3 py-2 text-right">{formatCompactNumber(session.toolCallCount)}</td>
<td className="px-3 py-2 text-right">{formatCompactNumber(session.subagentCallCount)}</td>
<td className="px-3 py-2 text-right">{formatCompactNumber(session.errorCount)}</td>
<td className="px-3 py-2 text-right">{formatPercent(session.cacheRatio)}</td>
<td className="px-3 py-2 text-right font-semibold">{formatUsdCost(session.costUsd)}</td>
<td className="max-w-[240px] px-3 py-2" title={session.models.join(", ")}>{session.models.join(", ") || "-"}</td>
<td className="max-w-[220px] px-3 py-2" title={session.providers.join(", ")}>{session.providers.join(", ") || "-"}</td>
<td className="max-w-[220px] px-3 py-2 font-mono" title={session.userAgent}>{compactUserAgent(session.userAgent)}</td>
@ -4143,7 +4171,7 @@ function UsageAnalysisCard({
{visibleColumns.map((column) => (
<th className="px-3 py-2 font-semibold" key={column.key}>{column.label}</th>
))}
<th className="px-3 py-2 text-right font-semibold">{t("Tokens")}</th>
<th className="px-3 py-2 text-right font-semibold">{t("Token")}</th>
{showCost ? <th className="px-3 py-2 text-right font-semibold">{t("Cost")}</th> : null}
<th className="px-3 py-2 text-right font-semibold">{t("Requests")}</th>
{showTokenBreakdown ? <th className="px-3 py-2 text-right font-semibold">{t("Input")}</th> : null}

View file

@ -782,7 +782,7 @@ function logTableColumnLabel(columnId: LogTableColumnId, t: (value: string) => s
case "credential":
return t("Credential");
case "tokens":
return t("令牌");
return t("Token");
case "duration":
return t("持续时间");
}
@ -849,7 +849,7 @@ function LogMobileCard({
</div>
</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-[11px]">
<LogCompactMetric label={t("令牌")} value={tokenSummary} />
<LogCompactMetric label={t("Token")} value={tokenSummary} />
<LogCompactMetric label={t("持续时间")} value={formatDuration(item.durationMs)} />
{hasCredentialInfo ? <LogCompactMetric label={t("Credential")} value={logCredentialCellLabel(item)} /> : null}
<LogCompactMetric label={t("Provider")} value={item.provider || "-"} />

View file

@ -227,6 +227,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Stream": "Stream",
"Streaming": "Streaming",
"Non-streaming": "Non-streaming",
"Token": "Token",
"令牌": "Tokens",
"成本": "Cost",
"持续时间": "Duration",
@ -880,7 +881,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Cache": "缓存",
"Cache rate": "缓存率",
"Cache ratio": "缓存率",
"Cache tokens": "缓存令牌",
"Cache tokens": "缓存 Token",
"Cache write": "缓存写入",
"Cancel": "取消",
"Channel": "频道",
@ -1045,6 +1046,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Model overrides are optional; empty fields keep Claude Code defaults.": "模型设置是可选项;留空会保留 Claude Code 默认设置。",
"Display name": "显示名称",
"Double click to copy": "双击复制",
"Duration": "持续时间",
"Edit": "编辑",
"Edit bot": "编辑 Bot",
"Edit API Key": "编辑 API 密钥",
@ -1120,7 +1122,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Idle seconds": "空闲秒数",
"Idle seconds must be between 30 and 86400.": "空闲秒数必须在 30 到 86400 之间。",
"Input": "输入",
"Input tokens": "输入令牌",
"Input tokens": "输入 Token",
"Integration ID": "集成 ID",
"Install": "安装",
"Install and restart": "安装并重启",
@ -1208,7 +1210,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Open profiles": "查看配置档案",
"Open providers": "查看供应商",
"Output": "输出",
"Output tokens": "输出令牌",
"Output tokens": "输出 Token",
"Observability": "可观测",
"Off": "关闭",
"On failure": "失败时",
@ -1300,6 +1302,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Profile stopping is only available in the Electron app.": "配置档案停止功能仅在 Electron App 中可用。",
"Profile ready": "配置档案已就绪",
"Password": "密码",
"Partial failure": "部分失败",
"Recent Errors": "最近错误",
"Recent Requests": "最近请求",
"Refresh": "刷新",
@ -1528,7 +1531,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"AI Fuel Cockpit": "AI 燃料仪表",
"Token Calendar Poster": "Token 日历海报",
"Spend Receipt": "消费小票",
"tokens routed through CCR": "通过 CCR 路由的令牌",
"tokens routed through CCR": "通过 CCR 路由的 Token",
"Top model": "最高频模型",
"Top provider": "最高频供应商",
"No provider activity": "暂无供应商活动",
@ -1569,7 +1572,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Subagent": "子代理",
"Subagent Routing": "Subagent 路由",
"Subagent calls": "Subagent 调用",
"Subagents": "Subagent",
"Subagents": "子代理",
"Success": "成功",
"Success rate": "成功率",
"System proxy": "系统代理",
@ -1581,15 +1584,16 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Thread": "线程",
"Thread ID": "线程 ID",
"Thinking": "思考",
"Token Mix": "令牌构成",
"Token": "Token",
"Token Mix": "Token 构成",
"Total": "总计",
"Total tokens": "总令牌",
"Total tokens": "总 Token",
"Username": "用户名",
"Today": "今天",
"Token threshold": "令牌阈值",
"Token threshold": "Token 阈值",
"Truncated": "已截断",
"Tokens": "令牌",
"tokens": "令牌",
"Tokens": "Token",
"tokens": "Token",
"day": "天",
"days": "天",
"Tool": "工具",
@ -1812,10 +1816,10 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"5h quota": "5 小时额度",
"Granted balance": "赠送余额",
"Individual limit": "个人额度",
"Lifetime tokens": "累计令牌",
"Lifetime tokens": "累计 Token",
"Manual resets": "主动重置次数",
"Monthly budget": "月度预算",
"Peak daily tokens": "日峰值令牌",
"Peak daily tokens": "日峰值 Token",
"Primary quota": "主额度",
"Secondary quota": "副额度",
"Topped-up balance": "充值余额",
@ -1844,7 +1848,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Copy API key": "复制 API 密钥",
"Model settings": "模型设置",
"Context, pricing, reasoning, web search, and image": "上下文、价格、推理、网页搜索与图片",
"Context window (tokens)": "上下文窗口(令牌",
"Context window (tokens)": "上下文窗口(Token",
"Custom pricing": "自定义价格",
"Pricing": "价格",
"Preset": "预设",

View file

@ -2,9 +2,11 @@ import assert from "node:assert/strict";
import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { RequestLogPage } from "@ccr/core/contracts/app.ts";
import type { AgentAnalysisSessionRow, AgentAnalysisTraceRun, RequestLogEntry, RequestLogPage } from "@ccr/core/contracts/app.ts";
import { AgentAnalysisView } from "@ccr/ui/pages/home/components/dashboard.tsx";
import { LogsView } from "@ccr/ui/pages/home/components/network-logs.tsx";
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
import { createEmptyAgentAnalysis } from "@ccr/ui/pages/home/shared/usage.ts";
const emptyLogPage: RequestLogPage = {
generatedAt: "2026-07-23T00:00:00.000Z",
@ -20,6 +22,39 @@ const emptyLogPage: RequestLogPage = {
totalPages: 1
};
const sampleRequestLogEntry: RequestLogEntry = {
cacheReadTokens: 0,
cacheWriteTokens: 0,
client: "claude-code",
costUsd: 0.01,
createdAt: "2026-07-23T00:00:00.000Z",
credentialChain: [],
credentialSaturated: false,
durationMs: 120,
id: 1,
inputTokens: 100,
isStream: true,
method: "POST",
model: "claude-sonnet-4",
ok: true,
outputTokens: 50,
path: "/v1/messages",
provider: "anthropic",
reasoningTokens: 0,
requestBody: { encoding: "utf8", sizeBytes: 2, text: "{}", truncated: false },
requestHeaders: {},
requestId: "req-token-copy",
routeAttemptCount: 1,
routeHopCount: 1,
routeTraceTruncated: false,
retryAttempts: [],
responseBody: { encoding: "utf8", sizeBytes: 2, text: "{}", truncated: false },
responseHeaders: {},
statusCode: 200,
totalTokens: 150,
url: "/v1/messages"
};
test("LogsView keeps disabled request logs discoverable with an enable action", () => {
const html = renderToStaticMarkup(
<AppI18nContext.Provider value={appCopy.zh}>
@ -59,3 +94,162 @@ test("LogsView explains filtered empty results and translates page sizes", () =>
assert.match(html, /25 \/ page/);
assert.doesNotMatch(html, /\/ 页/);
});
test("LogsView keeps Chinese token column copy as Token", () => {
const html = renderToStaticMarkup(
<AppI18nContext.Provider value={appCopy.zh}>
<LogsView
error=""
filter={{ page: 1, pageSize: 25, status: "all" }}
loading={false}
page={{ ...emptyLogPage, items: [sampleRequestLogEntry], total: 1 }}
refreshLogs={() => undefined}
updateFilter={() => undefined}
/>
</AppI18nContext.Provider>
);
assert.match(html, /Token/);
assert.doesNotMatch(html, /令牌/);
});
test("AgentAnalysisView keeps session headings horizontal and shows cache rate and cost", () => {
const session: AgentAnalysisSessionRow = {
agent: "claude-code",
avgDurationMs: 420,
cacheRatio: 0.375,
cacheReadTokens: 300,
cacheTokens: 300,
cacheWriteTokens: 100,
client: "claude-code",
costUsd: 1.25,
durationMs: 900,
errorCount: 0,
id: "session-cache-cost",
inputTokens: 500,
lastSeenAt: "2026-07-23T00:01:00.000Z",
maxConcurrentRequests: 1,
maxDurationMs: 500,
models: ["claude-sonnet-4"],
outputTokens: 100,
p50DurationMs: 420,
p95DurationMs: 500,
p99DurationMs: 500,
providers: ["anthropic"],
requestCount: 2,
sessionCount: 1,
startedAt: "2026-07-23T00:00:00.000Z",
subagentCallCount: 0,
successRate: 1,
toolCallCount: 1,
topTools: [{ count: 1, name: "Read" }],
totalTokens: 1000
};
const snapshot = {
...createEmptyAgentAnalysis("24h"),
scannedRequestCount: 2,
selectedSession: {
endpoints: [],
errors: [],
models: [],
requests: [{
agent: session.agent,
cacheReadTokens: 300,
cacheWriteTokens: 100,
client: session.client,
concurrentRequests: 1,
costUsd: 0.25,
createdAt: session.startedAt,
durationMs: 420,
id: 42,
inputTokens: 500,
method: "POST",
model: "claude-sonnet-4",
ok: true,
outputTokens: 100,
path: "/v1/messages",
provider: "anthropic",
requestId: "request-with-cost",
routeReason: "default",
sessionId: session.id,
statusCode: 200,
toolCallCount: 1,
tools: ["Read"],
totalTokens: 1000
}],
routes: [],
session,
statusCodes: [],
subagents: [],
tools: [],
totals: session,
trace: {
agent: session.agent,
durationMs: session.durationMs,
endedAt: session.lastSeenAt,
errorCount: 1,
id: `${session.agent}:${session.id}`,
llmRunCount: 0,
maxDepth: 0,
rootRunId: `agent:${session.agent}:${session.id}`,
runCount: 1,
runs: [{
agent: session.agent,
cacheReadTokens: session.cacheReadTokens,
cacheWriteTokens: session.cacheWriteTokens,
concurrentRequests: session.maxConcurrentRequests,
costUsd: 0.75,
depth: 0,
durationMs: session.durationMs,
endedAt: session.lastSeenAt,
id: `agent:${session.agent}:${session.id}`,
inputTokens: session.inputTokens,
kind: "agent",
name: "Claude Code session",
offsetMs: 0,
outputTokens: session.outputTokens,
sessionId: session.id,
startedAt: session.startedAt,
status: "partial",
totalTokens: session.totalTokens
} satisfies AgentAnalysisTraceRun],
sessionId: session.id,
startedAt: session.startedAt,
subagentRunCount: 0,
toolRunCount: 0
}
},
sessions: [session]
};
const html = renderToStaticMarkup(
<AppI18nContext.Provider value={appCopy.zh}>
<AgentAnalysisView
agentFilter="all"
error=""
loading={false}
range="24h"
refreshAnalysis={() => undefined}
setAgentFilter={() => undefined}
setRange={() => undefined}
setSelectedSession={() => undefined}
snapshot={snapshot}
/>
</AppI18nContext.Provider>
);
assert.match(html, /持续时间/);
assert.match(html, /子代理/);
assert.match(html, /缓存率/);
assert.match(html, /38%/);
assert.match(html, /成本/);
assert.match(html, /Token/);
assert.doesNotMatch(html, /令牌/);
assert.match(html, /\$1\.25/);
assert.match(html, /\$0\.25/);
assert.match(html, /\$0\.75/);
assert.match(html, /部分失败/);
assert.match(html, /border-amber-200/);
assert.match(html, /min-w-\[64px\]/);
assert.match(html, /whitespace-nowrap/);
});

View file

@ -3,6 +3,7 @@ import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { formatCodexResetCardExpiry, formatCodexResetCardNumber, OverviewView } from "@ccr/ui/pages/home/components/dashboard.tsx";
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
import { parseStatusBucketDate } from "@ccr/ui/pages/home/shared/controls.tsx";
import { providerAccountMeterDetailValidityProgress } from "@ccr/ui/pages/home/shared/provider-accounts.ts";
import type { OverviewWidgetConfig, ProviderAccountSnapshot } from "@ccr/core/contracts/app.ts";
@ -74,6 +75,34 @@ test("OverviewView renders every overview widget type", () => {
assert.match(html, /Spend Receipt/);
});
test("OverviewView keeps Chinese token copy as Token", () => {
const html = renderToStaticMarkup(
<AppI18nContext.Provider value={appCopy.zh}>
<OverviewView
overviewWidgets={[
{ enabled: true, id: "metric-total", metric: "total-tokens", size: "1:1", type: "metric", variant: "card" },
{ enabled: true, id: "trend", size: "3:2", type: "usage-trend", variant: "composed" },
{ enabled: true, id: "activity", size: "4:2", type: "token-activity", variant: "heatmap" },
{ enabled: true, id: "token-mix", size: "2:2", type: "token-mix", variant: "donut" },
{ enabled: true, id: "share-usage", size: "1:4", type: "share-usage-wrapped", variant: "card" },
{ enabled: true, id: "share-receipt", size: "1:4", type: "share-spend-receipt", variant: "card" }
]}
providerAccounts={accountSnapshots()}
refreshProviderAccounts={() => undefined}
setUsageRange={() => undefined}
usageRange="30d"
usageStats={usageStats("30d")}
onWidgetsChange={() => undefined}
/>
</AppI18nContext.Provider>
);
assert.match(html, /Token/);
assert.match(html, /Token 构成/);
assert.match(html, /总 Token/);
assert.doesNotMatch(html, /令牌/);
});
test("overview status dates accept ISO usage buckets", () => {
assert.equal(parseStatusBucketDate("2026-06-20T00:00:00.000Z")?.toISOString(), "2026-06-20T00:00:00.000Z");
});