Add model summaries to request logs

This commit is contained in:
musistudio
2026-07-20 15:57:13 +08:00
parent ecdf2fec0f
commit ea35ed2eaa
9 changed files with 365 additions and 19 deletions
+3
View File
@@ -1935,6 +1935,7 @@ export type RequestLogEntry = {
path: string;
provider: string;
reasoningTokens: number;
requestedModel?: string;
requestBody: RequestLogBody;
requestHeaders: Record<string, string | string[]>;
requestId: string;
@@ -1943,7 +1944,9 @@ export type RequestLogEntry = {
routeTrace?: RequestRouteTrace;
routeTraceTruncated: boolean;
retryAttempts: RequestLogRetryAttempt[];
resolvedModel?: string;
responseBody?: RequestLogBody;
responseModel?: string;
responseHeaders: Record<string, string | string[]>;
statusCode: number;
totalTokens: number;
@@ -7,6 +7,7 @@ import {
markGatewayRequestLogDropped,
recordGatewayRequestLog
} from "@ccr/core/observability/request-log-store";
import { requestLogRequestedModel, requestLogResponseModel } from "@ccr/core/observability/request-log-model";
import { recordGatewayUsageCapture, type UsageCaptureInput } from "@ccr/core/usage/store";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
import { adaptRouteRequestBody, restoreRouteRequestBody } from "@ccr/core/routing/protocol-adapter";
@@ -75,6 +76,7 @@ export class GatewayRequestPipeline {
const method = request.method ?? "GET";
const requestBody = await readRequestBody(request);
const requestedModel = requestLogRequestedModel(requestBody, path);
const startedAt = Date.now();
const startedAtIso = new Date(startedAt).toISOString();
const requestId = randomUUID();
@@ -243,14 +245,17 @@ export class GatewayRequestPipeline {
path,
providerName: resolveProviderLogName(responseHeaders, config, routedModel),
providerProtocol: resolveResponseProviderProtocol(responseHeaders, this.config),
requestedModel,
requestBody: shouldSendBody(method) ? bodyToForward ?? Buffer.alloc(0) : Buffer.alloc(0),
requestHeaders: headers,
requestId,
resolvedModel: routedModel,
routeTrace: routeTrace?.finish({ captureBodyValues: captureBody }),
responseBodyText,
responseBodySizeBytes,
responseBodyTruncated,
responseHeaders,
responseModel: requestLogResponseModel(responseBodyText),
startedAt: startedAtIso,
statusCode,
url: requestUrl
@@ -0,0 +1,68 @@
import { routeModelFromPath } from "@ccr/core/routing/protocol-adapter";
type JsonRecord = Record<string, unknown>;
export function requestLogRequestedModel(body: Buffer | string, path = ""): string | undefined {
const pathModel = normalizeModel(routeModelFromPath(path));
if (pathModel) {
return pathModel;
}
return modelFromPayload(parseJsonBody(typeof body === "string" ? body : body.toString("utf8")));
}
export function requestLogResponseModel(text: string): string | undefined {
const trimmed = text.trim();
if (!trimmed) {
return undefined;
}
const direct = modelFromPayload(parseJsonBody(trimmed));
if (direct) {
return direct;
}
let model: string | undefined;
for (const rawLine of trimmed.split(/\r?\n/)) {
const line = rawLine.trim();
const data = line.startsWith("data:") ? line.slice(5).trim() : line.startsWith("{") ? line : "";
if (!data || data === "[DONE]") {
continue;
}
model = modelFromPayload(parseJsonBody(data)) ?? model;
}
return model;
}
function modelFromPayload(payload: unknown): string | undefined {
if (!isRecord(payload)) {
return undefined;
}
const response = isRecord(payload.response) ? payload.response : undefined;
const message = isRecord(payload.message) ? payload.message : undefined;
return normalizeModel(response?.model) ??
normalizeModel(payload.model) ??
normalizeModel(message?.model) ??
normalizeModel(response?.modelVersion) ??
normalizeModel(payload.modelVersion) ??
normalizeModel(message?.modelVersion);
}
function parseJsonBody(text: string): unknown {
try {
return JSON.parse(text) as unknown;
} catch {
return undefined;
}
}
function isRecord(value: unknown): value is JsonRecord {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function normalizeModel(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim();
return normalized || undefined;
}
@@ -16,6 +16,7 @@ import {
} from "@ccr/core/observability/request-log-runtime";
import { maxRequestLogBodyBytes, rawTraceHardMaxBodyBytes } from "@ccr/core/observability/request-log-limits";
import { compactBase64ImagePayloads } from "@ccr/core/observability/request-log-body";
import { requestLogRequestedModel, requestLogResponseModel } from "@ccr/core/observability/request-log-model";
import { isSensitiveRequestLogHeaderName } from "@ccr/core/observability/sensitive-headers";
import type {
AgentAnalysisAgentRow,
@@ -110,15 +111,18 @@ export type RequestLogRecordInput = {
path: string;
providerName?: string;
providerProtocol?: GatewayProviderProtocol;
requestedModel?: string;
requestBody: Buffer;
requestBodySizeBytes?: number;
requestBodyTruncated?: boolean;
requestHeaders: HeaderRecord;
requestId?: string;
resolvedModel?: string;
routeTrace?: RequestRouteTrace;
responseBodyText?: string;
responseBodySizeBytes?: number;
responseBodyTruncated?: boolean;
responseModel?: string;
responseHeaders?: Headers | HeaderRecord;
startedAt: string;
statusCode: number;
@@ -210,6 +214,7 @@ type StoredRequestLogEntry = {
path: string;
provider: string;
reasoningTokens: number;
requestedModel: string;
requestBody: RequestLogBody;
requestHeaders: Record<string, string | string[]>;
requestId: string;
@@ -218,7 +223,9 @@ type StoredRequestLogEntry = {
routeTrace?: RequestRouteTrace;
routeTraceTruncated: boolean;
retryAttempts: RequestLogRetryAttempt[];
resolvedModel: string;
responseBody?: RequestLogBody;
responseModel: string;
responseHeaders: Record<string, string | string[]>;
statusCode: number;
totalTokens: number;
@@ -504,7 +511,18 @@ export class RequestLogStore {
usageHint: bodyUsage
}) ?? {};
const route = splitRouteSelector(input.fallbackModel);
const requestModel = normalizeFilterValue(input.model) ?? extractModelFromBody(input.requestBody.toString("utf8"));
const bodyModel = requestLogRequestedModel(input.requestBody, input.path);
const requestModel = normalizeFilterValue(input.model) ?? bodyModel;
const requestedModel = normalizeFilterValue(input.requestedModel) ?? bodyModel ?? "";
const resolvedModel = normalizeFilterValue(input.resolvedModel) ??
normalizeFilterValue(input.model) ??
route.model ??
bodyModel ??
normalizeFilterValue(input.fallbackModel) ??
"";
const responseModel = normalizeFilterValue(input.responseModel) ??
requestLogResponseModel(responseBodyText) ??
"";
const provider =
normalizeFilterValue(input.providerName) ??
readResponseHeader(input.responseHeaders, "x-gateway-target-provider-name") ??
@@ -574,6 +592,9 @@ export class RequestLogStore {
credential_chain,
credential_saturated,
model,
requested_model,
resolved_model,
response_model,
route_trace_version,
route_hop_count,
route_attempt_count,
@@ -602,7 +623,7 @@ export class RequestLogStore {
response_body_size_bytes,
response_body_truncated,
error
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
let inserted = false;
@@ -621,6 +642,9 @@ export class RequestLogStore {
credentialInfo.chain.join(","),
credentialInfo.saturated ? 1 : 0,
model,
requestedModel,
resolvedModel,
responseModel,
input.routeTrace?.version ?? 0,
input.routeTrace?.hopCount ?? 0,
input.routeTrace?.attemptCount ?? 0,
@@ -747,6 +771,9 @@ export class RequestLogStore {
const path = normalizeFilterValue(input.path) ?? pathFromUrl(url);
const usagePath = path ?? existingUsageContext.path;
const modelFromTrace = normalizeFilterValue(input.model);
const responseModelFromTrace = rawInput.responseBodyText === undefined
? undefined
: requestLogResponseModel(rawInput.responseBodyText);
const providerFromTrace = normalizeFilterValue(input.provider);
const statusCode = rawStatusCode;
const requestCredentialHeaders = input.requestHeaders ?? {};
@@ -764,6 +791,8 @@ export class RequestLogStore {
pushValue("url", url);
pushValue("provider", providerFromTrace);
pushValue("model", modelFromTrace);
pushValue("resolved_model", modelFromTrace);
pushValue("response_model", responseModelFromTrace);
// The gateway's terminal failure is authoritative, even when it has only
// an HTTP error status and no error string. A final-attempt raw failure may
// still refine a gateway success (for example an SSE error inside HTTP 200).
@@ -917,6 +946,9 @@ export class RequestLogStore {
credential_chain,
credential_saturated,
model,
requested_model,
resolved_model,
response_model,
route_trace_version,
route_hop_count,
route_attempt_count,
@@ -1010,6 +1042,9 @@ export class RequestLogStore {
credential_chain,
credential_saturated,
model,
requested_model,
resolved_model,
response_model,
is_stream,
status_code,
ok,
@@ -1153,6 +1188,9 @@ export class RequestLogStore {
credential_chain TEXT NOT NULL DEFAULT '',
credential_saturated INTEGER NOT NULL DEFAULT 0,
model TEXT NOT NULL DEFAULT 'unknown',
requested_model TEXT NOT NULL DEFAULT '',
resolved_model TEXT NOT NULL DEFAULT '',
response_model TEXT NOT NULL DEFAULT '',
route_trace_version INTEGER NOT NULL DEFAULT 0,
route_hop_count INTEGER NOT NULL DEFAULT 0,
route_attempt_count INTEGER NOT NULL DEFAULT 0,
@@ -3157,12 +3195,47 @@ function parseDateMs(value: string): number {
return Number.isFinite(parsed) ? parsed : 0;
}
function migrateRequestLogModelSummaries(database: SqlDatabase): void {
const rows = queryRows(database, `
SELECT
rowid AS id,
model,
path,
request_body_text,
response_body_text
FROM request_logs
`);
if (rows.length === 0) {
return;
}
const update = database.prepare(`
UPDATE request_logs
SET requested_model = ?, resolved_model = ?, response_model = ?
WHERE rowid = ?
`);
database.transaction(() => {
for (const row of rows) {
const requestedModel = requestLogRequestedModel(
String(row.request_body_text ?? ""),
String(row.path ?? "")
) ?? "";
const resolvedModel = normalizeFilterValue(String(row.model ?? "")) ?? "";
const responseModel = requestLogResponseModel(String(row.response_body_text ?? "")) ?? "";
update.run(requestedModel, resolvedModel, responseModel, normalizeCount(row.id));
}
})();
}
function ensureRequestLogSchema(database: SqlDatabase): void {
const columns = new Set(
queryRows(database, "PRAGMA table_info(request_logs)")
.map((row) => String(row.name ?? ""))
.filter(Boolean)
);
const needsModelSummaryMigration = !columns.has("requested_model") ||
!columns.has("resolved_model") ||
!columns.has("response_model");
const addColumn = (name: string, definition: string) => {
if (!columns.has(name)) {
database.exec(`ALTER TABLE request_logs ADD COLUMN ${name} ${definition}`);
@@ -3184,6 +3257,9 @@ function ensureRequestLogSchema(database: SqlDatabase): void {
addColumn("credential_chain", "TEXT NOT NULL DEFAULT ''");
addColumn("credential_saturated", "INTEGER NOT NULL DEFAULT 0");
addColumn("model", "TEXT NOT NULL DEFAULT 'unknown'");
addColumn("requested_model", "TEXT NOT NULL DEFAULT ''");
addColumn("resolved_model", "TEXT NOT NULL DEFAULT ''");
addColumn("response_model", "TEXT NOT NULL DEFAULT ''");
addColumn("route_trace_version", "INTEGER NOT NULL DEFAULT 0");
addColumn("route_hop_count", "INTEGER NOT NULL DEFAULT 0");
addColumn("route_attempt_count", "INTEGER NOT NULL DEFAULT 0");
@@ -3219,6 +3295,10 @@ function ensureRequestLogSchema(database: SqlDatabase): void {
addColumn("response_body_truncated", "INTEGER NOT NULL DEFAULT 0");
addColumn("error", "TEXT NOT NULL DEFAULT ''");
if (needsModelSummaryMigration) {
migrateRequestLogModelSummaries(database);
}
ensureRequestLogMigrationSchema(database);
migrateGatewayOutcome(database);
migrateGatewayFinalAttempt(database);
@@ -3779,6 +3859,9 @@ function readRequestLogById(database: SqlDatabase, id: number): StoredRequestLog
credential_chain,
credential_saturated,
model,
requested_model,
resolved_model,
response_model,
route_trace_version,
route_hop_count,
route_attempt_count,
@@ -3852,6 +3935,7 @@ function toRequestLogEntry(row: Record<string, SqlValue>): StoredRequestLogEntry
path: normalizeLabel(String(row.path ?? ""), "/"),
provider: normalizeLabel(String(row.provider ?? ""), "unknown"),
reasoningTokens: normalizeCount(row.reasoning_tokens),
requestedModel: normalizeLabel(String(row.requested_model ?? ""), ""),
requestBody,
requestHeaders,
requestId: String(row.request_id ?? ""),
@@ -3859,8 +3943,10 @@ function toRequestLogEntry(row: Record<string, SqlValue>): StoredRequestLogEntry
routeHopCount: normalizeCount(row.route_hop_count),
routeTraceTruncated: normalizeCount(row.route_trace_truncated) === 1,
retryAttempts: parseRequestLogRetryAttempts(responseHeaders, normalizeCount(row.status_code)),
resolvedModel: normalizeLabel(String(row.resolved_model ?? ""), ""),
responseBody,
responseHeaders,
responseModel: normalizeLabel(String(row.response_model ?? ""), ""),
statusCode: normalizeCount(row.status_code),
totalTokens: normalizeCount(row.total_tokens),
url: String(row.url ?? "")
@@ -4671,14 +4757,6 @@ function extractUsageSnapshot(payload: unknown): UsageSnapshot | undefined {
};
}
function extractModelFromBody(text: string): string | undefined {
const parsed = parseJson(text.trim());
if (!isRecord(parsed)) {
return undefined;
}
return asString(parsed.model);
}
function hasUsageNumbers(snapshot: UsageNumbers): boolean {
return [
snapshot.cacheReadTokens,
@@ -443,6 +443,9 @@ test("RequestLogRuntime preserves original response sizes when normal body captu
runtime.enqueueRecord({
...createRecord("metadata-response"),
captureBody: false,
requestedModel: "client-model",
resolvedModel: "resolved-model",
responseModel: "response-model",
responseBodyText: "exists"
});
await runtime.flush({ timeoutMs: 10_000 });
@@ -453,6 +456,9 @@ test("RequestLogRuntime preserves original response sizes when normal body captu
const truncated = await runtime.getDetail({ id: truncatedEntry.id });
const metadata = await runtime.getDetail({ id: metadataEntry.id });
assert.equal(metadataEntry.requestedModel, "client-model");
assert.equal(metadataEntry.resolvedModel, "resolved-model");
assert.equal(metadataEntry.responseModel, "response-model");
assert.equal(truncated.responseBody.sizeBytes, Buffer.byteLength(fullResponse));
assert.equal(Buffer.byteLength(truncated.responseBody.text), 512 * 1024);
assert.equal(truncated.responseBody.truncated, true);
@@ -7,6 +7,7 @@ import test from "node:test";
import { promisify } from "node:util";
import { getHeapStatistics } from "node:v8";
import { createSseErrorDetector, RequestLogStore } from "@ccr/core/observability/request-log-store.ts";
import { requestLogRequestedModel, requestLogResponseModel } from "@ccr/core/observability/request-log-model.ts";
import { resolveStreamRequestLogOutcome } from "@ccr/core/gateway/internal/shared.ts";
import { RequestRouteTraceRecorder } from "@ccr/core/observability/route-trace.ts";
import { createBetterSqliteDatabase } from "@ccr/core/storage/sqlite-native.ts";
@@ -14,6 +15,69 @@ import { createBetterSqliteDatabase } from "@ccr/core/storage/sqlite-native.ts";
const execFileAsync = promisify(execFile);
const isBoundedHeapWorker = process.env.CCR_REQUEST_LOG_BOUNDED_HEAP_WORKER === "1";
test("request log model summaries support routed paths and streamed responses", () => {
assert.equal(
requestLogRequestedModel("{}", "/v1beta/models/gemini-2.5-pro:streamGenerateContent"),
"gemini-2.5-pro"
);
assert.equal(
requestLogResponseModel([
"event: message_start",
"data: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-response\"}}",
"",
"event: message_stop",
"data: {\"type\":\"message_stop\"}"
].join("\n")),
"claude-response"
);
assert.equal(
requestLogResponseModel("data: {\"type\":\"response.created\",\"response\":{\"model\":\"gpt-response\"}}\n\n"),
"gpt-response"
);
});
test("RequestLogStore backfills model summaries when upgrading an existing database", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-model-migration-test-"));
const dbFile = path.join(dir, "request-logs.sqlite");
let store;
try {
const legacy = createBetterSqliteDatabase(dbFile);
try {
legacy.exec(`
CREATE TABLE request_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
model TEXT NOT NULL,
request_body_text TEXT NOT NULL,
response_body_text TEXT NOT NULL
);
`);
legacy.prepare(`
INSERT INTO request_logs (
created_at, method, path, model, request_body_text, response_body_text
) VALUES (?, 'POST', '/v1/messages', 'resolved-model', ?, ?)
`).run(
new Date().toISOString(),
JSON.stringify({ model: "request-model" }),
JSON.stringify({ model: "response-model" })
);
} finally {
legacy.close();
}
store = new RequestLogStore(dbFile);
const page = await store.list({ pageSize: 25 });
assert.equal(page.items[0].requestedModel, "request-model");
assert.equal(page.items[0].resolvedModel, "resolved-model");
assert.equal(page.items[0].responseModel, "response-model");
} finally {
await store?.close();
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore resumes interrupted gateway migrations across bounded batches", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-paged-migration-test-"));
const dbFile = path.join(dir, "request-logs.sqlite");
@@ -232,6 +296,9 @@ test("RequestLogStore keeps list rows lightweight and detail rows complete", asy
const page = await store.list({ pageSize: 25 });
assert.equal(page.items.length, 1);
assert.equal(page.items[0].requestedModel, "request-model");
assert.equal(page.items[0].resolvedModel, "request-model");
assert.equal(page.items[0].responseModel, "response-model");
assert.equal(page.items[0].requestBody.text, "");
assert.equal(page.items[0].responseBody?.text, "");
assert.equal(page.items[0].requestBody.sizeBytes, Buffer.byteLength(body));
@@ -239,6 +306,9 @@ test("RequestLogStore keeps list rows lightweight and detail rows complete", asy
const detail = await store.getDetail({ id: page.items[0].id });
assert.ok(detail);
assert.equal(detail.requestedModel, "request-model");
assert.equal(detail.resolvedModel, "request-model");
assert.equal(detail.responseModel, "response-model");
assert.match(detail.requestBody.text, /request-model/);
assert.match(detail.responseBody?.text ?? "", /response-model/);
} finally {
@@ -1,4 +1,5 @@
import { memo } from "react";
import { createPortal } from "react-dom";
import { Maximize2, Route, X } from "lucide-react";
import type { RequestRouteTrace, RequestRouteTraceChange, RequestRouteTraceHop } from "@ccr/core/contracts/app";
import {
@@ -1102,14 +1103,88 @@ function LogMetric({ label, value }: { label: string; value: string }) {
function LogModelRouteCell({ entry }: { entry: RequestLogEntry }) {
const requestModel = logRequestModel(entry);
const responseModel = logResponseModel(entry);
const title = `${requestModel} -> ${responseModel}`;
return <LogModelTooltip requestModel={requestModel} responseModel={responseModel} />;
}
type LogModelTooltipState = {
left: number;
placement: "above" | "below";
top: number;
width: number;
};
function LogModelTooltip({
requestModel,
responseModel
}: {
requestModel: string;
responseModel: string;
}) {
const triggerRef = useRef<HTMLDivElement>(null);
const [tooltip, setTooltip] = useState<LogModelTooltipState>();
const value = `${requestModel} -> ${responseModel}`;
useEffect(() => {
if (!tooltip) return;
const dismiss = () => setTooltip(undefined);
window.addEventListener("resize", dismiss);
window.addEventListener("scroll", dismiss, true);
return () => {
window.removeEventListener("resize", dismiss);
window.removeEventListener("scroll", dismiss, true);
};
}, [tooltip]);
const showTooltip = () => {
const trigger = triggerRef.current;
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
const margin = 12;
const gap = 6;
const availableWidth = Math.max(0, window.innerWidth - margin * 2);
const width = Math.min(availableWidth, Math.max(160, Math.min(480, value.length * 7 + 24)));
const left = Math.min(
Math.max(margin, rect.left + rect.width / 2 - width / 2),
Math.max(margin, window.innerWidth - width - margin)
);
const placement = window.innerHeight - rect.bottom >= 72 || rect.top < 72 ? "below" : "above";
setTooltip({
left,
placement,
top: placement === "below" ? rect.bottom + gap : rect.top - gap,
width
});
};
return (
<div className="flex min-w-0 items-center px-2" title={title}>
<span className="min-w-0 max-w-[45%] truncate">{requestModel}</span>
<MoveRight className="mx-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
<span className="min-w-0 max-w-[45%] truncate">{responseModel}</span>
</div>
<>
<div
className="flex min-w-0 items-center px-2"
onMouseEnter={showTooltip}
onMouseLeave={() => setTooltip(undefined)}
ref={triggerRef}
>
<span className="min-w-0 max-w-[45%] truncate">{requestModel}</span>
<MoveRight className="mx-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
<span className="min-w-0 max-w-[45%] truncate">{responseModel}</span>
</div>
{tooltip ? createPortal(
<div
className="pointer-events-none fixed z-[100] break-all rounded-md border border-border bg-popover px-2.5 py-1.5 font-mono text-[11px] font-medium text-popover-foreground shadow-card-elevated"
role="tooltip"
style={{
left: tooltip.left,
top: tooltip.top,
transform: tooltip.placement === "above" ? "translateY(-100%)" : undefined,
width: tooltip.width
}}
>
{value}
</div>,
document.body
) : null}
</>
);
}
+6 -2
View File
@@ -423,11 +423,15 @@ export function formatLogTokenSummary(entry: RequestLogEntry, t: (value: string)
}
export function logRequestModel(entry: RequestLogEntry): string {
return logBodyModel(entry.requestBody) || entry.model || "unknown";
return entry.requestedModel || logBodyModel(entry.requestBody) || entry.model || "unknown";
}
export function logResponseModel(entry: RequestLogEntry): string {
return logBodyModel(entry.responseBody) || entry.model || "unknown";
return entry.responseModel ||
logBodyModel(entry.responseBody) ||
entry.resolvedModel ||
entry.model ||
"unknown";
}
export function logBodyModel(body: RequestLogBody | undefined): string | undefined {
+38 -1
View File
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { formatLogTokenSummary } from "@ccr/ui/pages/home/shared/logs.ts";
import { formatLogTokenSummary, logRequestModel, logResponseModel } from "@ccr/ui/pages/home/shared/logs.ts";
import { formatCompactNumber, formatUsdCost as formatHomeUsdCost } from "@ccr/ui/pages/home/shared/usage.ts";
import { formatUsdCost as formatTrayUsdCost } from "@ccr/ui/pages/tray/shared.tsx";
import type { RequestLogEntry } from "@ccr/core/contracts/app.ts";
@@ -51,3 +51,40 @@ test("formatLogTokenSummary uses the provided locale for token counts", () => {
assert.equal(formatLogTokenSummary(entry, translate, "en-US"), "123.5K 入 12K 出");
assert.equal(formatLogTokenSummary(entry, translate, "zh-CN"), "12.3万 入 1.2万 出");
});
test("request log model summaries stay stable without list body text", () => {
const entry: RequestLogEntry = {
cacheReadTokens: 0,
cacheWriteTokens: 0,
client: "test",
createdAt: "2026-07-20T00:00:00.000Z",
credentialChain: [],
credentialSaturated: false,
durationMs: 10,
id: 1,
inputTokens: 0,
isStream: true,
method: "POST",
model: "legacy-model",
ok: true,
outputTokens: 0,
path: "/v1/messages",
provider: "test-provider",
reasoningTokens: 0,
requestedModel: "request-model",
requestBody: { encoding: "utf8", sizeBytes: 128, text: "", truncated: false },
requestHeaders: {},
requestId: "req_models",
resolvedModel: "resolved-model",
responseHeaders: {},
responseModel: "response-model",
retryAttempts: [],
statusCode: 200,
totalTokens: 0,
url: "https://example.test/v1/messages"
};
assert.equal(logRequestModel(entry), "request-model");
assert.equal(logResponseModel(entry), "response-model");
assert.equal(logResponseModel({ ...entry, responseModel: "" }), "resolved-model");
});