mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Improve app packaging diagnostics and usage formatting
This commit is contained in:
parent
13517cc2a0
commit
f6174752b7
10 changed files with 365 additions and 25 deletions
212
build/verify-packaged-app.cjs
Normal file
212
build/verify-packaged-app.cjs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const betterSqliteNativeRelativePath = path.join(
|
||||
"app.asar.unpacked",
|
||||
"node_modules",
|
||||
"better-sqlite3",
|
||||
"build",
|
||||
"Release",
|
||||
"better_sqlite3.node"
|
||||
);
|
||||
|
||||
module.exports = async function verifyPackagedApp(context) {
|
||||
const platform = context?.electronPlatformName;
|
||||
const arch = normalizeArch(context?.arch);
|
||||
const appOutDir = context?.appOutDir;
|
||||
|
||||
if (!platform || !appOutDir) {
|
||||
throw new Error("Packaged app verification did not receive electron-builder platform context.");
|
||||
}
|
||||
|
||||
const resourcesDir = findResourcesDir(appOutDir, platform);
|
||||
assertFile(path.join(resourcesDir, "app.asar"), "Packaged app archive");
|
||||
|
||||
const nativeModule = path.join(resourcesDir, betterSqliteNativeRelativePath);
|
||||
assertFile(nativeModule, "better-sqlite3 native module");
|
||||
|
||||
const nativeInfo = inspectNativeModule(nativeModule);
|
||||
if (nativeInfo.platform !== platform) {
|
||||
throw new Error(
|
||||
`Packaged better-sqlite3 native module targets ${formatNativeInfo(nativeInfo)}, ` +
|
||||
`but electron-builder is packaging ${platform}/${arch}. Rebuild native dependencies for the target platform before packaging.`
|
||||
);
|
||||
}
|
||||
|
||||
if (arch && !nativeArchMatches(nativeInfo, arch)) {
|
||||
throw new Error(
|
||||
`Packaged better-sqlite3 native module targets ${formatNativeInfo(nativeInfo)}, ` +
|
||||
`but electron-builder is packaging ${platform}/${arch}. Run npm run rebuild:sqlite3 on the target platform before packaging.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function findResourcesDir(appOutDir, platform) {
|
||||
if (platform !== "darwin") {
|
||||
return path.join(appOutDir, "resources");
|
||||
}
|
||||
|
||||
const appBundle = fs.readdirSync(appOutDir)
|
||||
.find((entry) => entry.endsWith(".app") && fs.statSync(path.join(appOutDir, entry)).isDirectory());
|
||||
if (!appBundle) {
|
||||
throw new Error(`Could not find a .app bundle in ${appOutDir}`);
|
||||
}
|
||||
return path.join(appOutDir, appBundle, "Contents", "Resources");
|
||||
}
|
||||
|
||||
function assertFile(file, label) {
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.statSync(file);
|
||||
} catch {
|
||||
throw new Error(`${label} is missing: ${file}`);
|
||||
}
|
||||
|
||||
if (!stat.isFile() || stat.size === 0) {
|
||||
throw new Error(`${label} is empty or not a file: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
function inspectNativeModule(file) {
|
||||
const buffer = fs.readFileSync(file);
|
||||
if (buffer.length < 32) {
|
||||
throw new Error(`Native module is too small to identify: ${file}`);
|
||||
}
|
||||
|
||||
if (buffer[0] === 0x4d && buffer[1] === 0x5a) {
|
||||
return inspectPe(buffer, file);
|
||||
}
|
||||
if (buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46) {
|
||||
return inspectElf(buffer);
|
||||
}
|
||||
|
||||
const magicLe = buffer.readUInt32LE(0);
|
||||
const magicBe = buffer.readUInt32BE(0);
|
||||
if (magicLe === 0xfeedface || magicLe === 0xfeedfacf || magicLe === 0xcefaedfe || magicLe === 0xcffaedfe) {
|
||||
return inspectMachO(buffer, magicLe);
|
||||
}
|
||||
if (magicBe === 0xcafebabe || magicBe === 0xcafebabf) {
|
||||
return inspectFatMachO(buffer, magicBe);
|
||||
}
|
||||
|
||||
throw new Error(`Native module has an unknown binary format: ${file}`);
|
||||
}
|
||||
|
||||
function inspectPe(buffer, file) {
|
||||
const peOffset = buffer.readUInt32LE(0x3c);
|
||||
if (peOffset + 6 >= buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0") {
|
||||
throw new Error(`Native module has an invalid PE header: ${file}`);
|
||||
}
|
||||
const machine = buffer.readUInt16LE(peOffset + 4);
|
||||
return {
|
||||
arch: peMachineArch(machine),
|
||||
platform: "win32"
|
||||
};
|
||||
}
|
||||
|
||||
function inspectElf(buffer) {
|
||||
const machine = buffer.readUInt16LE(18);
|
||||
return {
|
||||
arch: elfMachineArch(machine),
|
||||
platform: "linux"
|
||||
};
|
||||
}
|
||||
|
||||
function inspectMachO(buffer, magicLe) {
|
||||
const bigEndian = magicLe === 0xcefaedfe || magicLe === 0xcffaedfe;
|
||||
const cpuType = bigEndian ? buffer.readUInt32BE(4) : buffer.readUInt32LE(4);
|
||||
return {
|
||||
arch: machCpuArch(cpuType),
|
||||
platform: "darwin"
|
||||
};
|
||||
}
|
||||
|
||||
function inspectFatMachO(buffer, magicBe) {
|
||||
const archs = [];
|
||||
const count = buffer.readUInt32BE(4);
|
||||
const entrySize = magicBe === 0xcafebabf ? 32 : 20;
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const offset = 8 + index * entrySize;
|
||||
if (offset + 8 > buffer.length) {
|
||||
break;
|
||||
}
|
||||
archs.push(machCpuArch(buffer.readUInt32BE(offset)));
|
||||
}
|
||||
return {
|
||||
arch: "universal",
|
||||
archs,
|
||||
platform: "darwin"
|
||||
};
|
||||
}
|
||||
|
||||
function peMachineArch(machine) {
|
||||
if (machine === 0x8664) {
|
||||
return "x64";
|
||||
}
|
||||
if (machine === 0xaa64) {
|
||||
return "arm64";
|
||||
}
|
||||
if (machine === 0x014c) {
|
||||
return "ia32";
|
||||
}
|
||||
return `unknown-pe-${machine.toString(16)}`;
|
||||
}
|
||||
|
||||
function elfMachineArch(machine) {
|
||||
if (machine === 0x3e) {
|
||||
return "x64";
|
||||
}
|
||||
if (machine === 0xb7) {
|
||||
return "arm64";
|
||||
}
|
||||
if (machine === 0x03) {
|
||||
return "ia32";
|
||||
}
|
||||
if (machine === 0x28) {
|
||||
return "armv7l";
|
||||
}
|
||||
return `unknown-elf-${machine.toString(16)}`;
|
||||
}
|
||||
|
||||
function machCpuArch(cpuType) {
|
||||
if (cpuType === 0x01000007) {
|
||||
return "x64";
|
||||
}
|
||||
if (cpuType === 0x0100000c) {
|
||||
return "arm64";
|
||||
}
|
||||
if (cpuType === 0x00000007) {
|
||||
return "ia32";
|
||||
}
|
||||
return `unknown-macho-${cpuType.toString(16)}`;
|
||||
}
|
||||
|
||||
function normalizeArch(arch) {
|
||||
if (typeof arch === "string") {
|
||||
return arch;
|
||||
}
|
||||
const electronBuilderArchNames = new Map([
|
||||
[0, "ia32"],
|
||||
[1, "x64"],
|
||||
[2, "armv7l"],
|
||||
[3, "arm64"],
|
||||
[4, "universal"]
|
||||
]);
|
||||
return electronBuilderArchNames.get(arch);
|
||||
}
|
||||
|
||||
function nativeArchMatches(info, arch) {
|
||||
if (info.arch === arch) {
|
||||
return true;
|
||||
}
|
||||
if (info.arch === "universal") {
|
||||
return arch === "universal" || Boolean(info.archs?.includes(arch));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function formatNativeInfo(info) {
|
||||
return info.arch === "universal" && info.archs?.length
|
||||
? `${info.platform}/${info.arch} (${info.archs.join(", ")})`
|
||||
: `${info.platform}/${info.arch}`;
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"directories": {
|
||||
"output": "release/${version}"
|
||||
},
|
||||
"afterPack": "build/verify-packaged-app.cjs",
|
||||
"afterAllArtifactBuild": "build/verify-update-metadata.cjs",
|
||||
"protocols": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function startPrimaryInstance(): void {
|
|||
queueEnsureConfiguredProxyModeActive("second-instance");
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
void app.whenReady().then(() => {
|
||||
configureProxyDesktopIntegration();
|
||||
try {
|
||||
ensureCcrCliLauncher();
|
||||
|
|
@ -57,6 +57,15 @@ function startPrimaryInstance(): void {
|
|||
}
|
||||
queueEnsureConfiguredProxyModeActive("activate");
|
||||
});
|
||||
}).catch((error) => {
|
||||
const detail = formatErrorDetail(error);
|
||||
console.error(`Failed to initialize ${app.name || "application"}: ${detail}`);
|
||||
try {
|
||||
dialog.showErrorBox("Claude Code Router failed to start", detail);
|
||||
} catch {
|
||||
// Keep the console log as the fallback diagnostic channel.
|
||||
}
|
||||
app.exit(1);
|
||||
});
|
||||
|
||||
app.on("before-quit", (event) => {
|
||||
|
|
@ -247,3 +256,7 @@ function logProxySystemProxyIssue(reason: string, status: ReturnType<typeof prox
|
|||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function formatErrorDetail(error: unknown): string {
|
||||
return error instanceof Error ? error.stack || error.message : String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { app } from "electron";
|
||||
import { app, dialog } from "electron";
|
||||
import { setRuntimeAppPaths } from "./app-paths";
|
||||
|
||||
setRuntimeAppPaths({
|
||||
|
|
@ -7,7 +7,60 @@ setRuntimeAppPaths({
|
|||
userData: app.getPath("userData")
|
||||
});
|
||||
|
||||
void import("./main-app.js").catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
app.quit();
|
||||
});
|
||||
let fatalStartupErrorReported = false;
|
||||
|
||||
process.once("uncaughtException", reportFatalStartupError);
|
||||
process.once("unhandledRejection", reportFatalStartupError);
|
||||
|
||||
void import("./main-app.js")
|
||||
.then(() => {
|
||||
process.off("uncaughtException", reportFatalStartupError);
|
||||
process.off("unhandledRejection", reportFatalStartupError);
|
||||
})
|
||||
.catch((error) => {
|
||||
reportFatalStartupError(error);
|
||||
});
|
||||
|
||||
function reportFatalStartupError(error: unknown): void {
|
||||
if (fatalStartupErrorReported) {
|
||||
return;
|
||||
}
|
||||
fatalStartupErrorReported = true;
|
||||
|
||||
const detail = formatErrorDetail(error);
|
||||
console.error(detail);
|
||||
|
||||
try {
|
||||
dialog.showErrorBox("Claude Code Router failed to start", startupErrorMessage(detail));
|
||||
} catch {
|
||||
// If the platform dialog is unavailable, the console output above still
|
||||
// preserves the actionable failure for command-line launches.
|
||||
}
|
||||
|
||||
app.exit(1);
|
||||
}
|
||||
|
||||
function startupErrorMessage(detail: string): string {
|
||||
if (isBetterSqliteNativeError(detail)) {
|
||||
return [
|
||||
"The bundled SQLite native module could not be loaded.",
|
||||
"",
|
||||
"This usually means the Windows package was built with a missing or incompatible better-sqlite3 binary. Rebuild the app with npm run rebuild:sqlite3 before packaging, or build the Windows artifact on a Windows x64 runner.",
|
||||
"",
|
||||
detail
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
function isBetterSqliteNativeError(detail: string): boolean {
|
||||
return /better[-_]sqlite3|better_sqlite3\.node|NODE_MODULE_VERSION|ERR_DLOPEN_FAILED|Cannot find module ['"]better-sqlite3/i.test(detail);
|
||||
}
|
||||
|
||||
function formatErrorDetail(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.stack || error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
Pause, Play, ProxyNetworkBody, ProxyNetworkExchange, ProxyNetworkSnapshot, ProxyStatus,
|
||||
ReactNode, ReactPointerEvent, RefreshCw, RequestLogBody, RequestLogEntry, RequestLogListFilter,
|
||||
RequestLogPage, requestLogPageSizeOptions, RequestLogStatusFilter, requestLogStatusOptions, Search, Select,
|
||||
translateOptions, Trash2, useAppText, useCallback, useEffect, useMemo, useRef,
|
||||
translateOptions, Trash2, useAppNumberLocale, useAppText, useCallback, useEffect, useMemo, useRef,
|
||||
useState
|
||||
} from "../shared";
|
||||
type NetworkRequestTab = "body" | "header" | "query" | "raw" | "summary";
|
||||
|
|
@ -489,8 +489,9 @@ const LogRow = memo(function LogRow({
|
|||
onToggle: (id: number) => void;
|
||||
}) {
|
||||
const t = useAppText();
|
||||
const numberLocale = useAppNumberLocale();
|
||||
const createdAt = useMemo(() => formatLogDateTime(item.createdAt), [item.createdAt]);
|
||||
const tokenSummary = useMemo(() => formatLogTokenSummary(item, t), [item, t]);
|
||||
const tokenSummary = useMemo(() => formatLogTokenSummary(item, t, numberLocale), [item, numberLocale, t]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -542,6 +543,7 @@ function LogExpandedDetails({
|
|||
entry: RequestLogEntry;
|
||||
}) {
|
||||
const t = useAppText();
|
||||
const numberLocale = useAppNumberLocale();
|
||||
const hasCredentialInfo = logHasCredentialInfo(entry);
|
||||
|
||||
return (
|
||||
|
|
@ -564,12 +566,12 @@ function LogExpandedDetails({
|
|||
{entry.credentialChain.length ? <LogMetric label={t("Credential chain")} value={entry.credentialChain.join(" > ")} /> : null}
|
||||
{hasCredentialInfo ? <LogMetric label={t("Credential saturated")} value={entry.credentialSaturated ? t("Yes") : t("No")} /> : null}
|
||||
{entry.retryAttempts.length > 0 ? <LogMetric label={t("Retry attempts")} value={String(entry.retryAttempts.length)} /> : null}
|
||||
<LogMetric label={t("输入")} value={formatCompactNumber(entry.inputTokens)} />
|
||||
<LogMetric label={t("输出")} value={formatCompactNumber(entry.outputTokens)} />
|
||||
<LogMetric label={t("Thinking")} value={formatCompactNumber(entry.reasoningTokens)} />
|
||||
<LogMetric label={t("缓存读取")} value={formatCompactNumber(entry.cacheReadTokens)} />
|
||||
<LogMetric label={t("缓存写入")} value={formatCompactNumber(entry.cacheWriteTokens)} />
|
||||
<LogMetric label={t("总计")} value={formatCompactNumber(entry.totalTokens)} />
|
||||
<LogMetric label={t("输入")} value={formatCompactNumber(entry.inputTokens, numberLocale)} />
|
||||
<LogMetric label={t("输出")} value={formatCompactNumber(entry.outputTokens, numberLocale)} />
|
||||
<LogMetric label={t("Thinking")} value={formatCompactNumber(entry.reasoningTokens, numberLocale)} />
|
||||
<LogMetric label={t("缓存读取")} value={formatCompactNumber(entry.cacheReadTokens, numberLocale)} />
|
||||
<LogMetric label={t("缓存写入")} value={formatCompactNumber(entry.cacheWriteTokens, numberLocale)} />
|
||||
<LogMetric label={t("总计")} value={formatCompactNumber(entry.totalTokens, numberLocale)} />
|
||||
<LogMetric label={t("Cost")} value={formatUsdCost(entry.costUsd ?? 0)} />
|
||||
</div>
|
||||
{entry.retryAttempts.length > 0 ? <LogRetryAttempts attempts={entry.retryAttempts} /> : null}
|
||||
|
|
|
|||
|
|
@ -1563,6 +1563,11 @@ export function useAppText() {
|
|||
return useMemo(() => (value: string) => translateText(copy, value), [copy]);
|
||||
}
|
||||
|
||||
export function useAppNumberLocale(): Intl.LocalesArgument {
|
||||
const copy = useContext(AppI18nContext);
|
||||
return copy === appCopy.zh ? "zh-CN" : "en-US";
|
||||
}
|
||||
|
||||
export function translateText(copy: AppCopy, value: string): string {
|
||||
return copy.text[value] ?? value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ export function formatLogDateTime(value: string): string {
|
|||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${offset}`;
|
||||
}
|
||||
|
||||
export function formatLogTokenSummary(entry: RequestLogEntry, t: (value: string) => string): string {
|
||||
export function formatLogTokenSummary(entry: RequestLogEntry, t: (value: string) => string, locale?: Intl.LocalesArgument): string {
|
||||
if (
|
||||
entry.totalTokens === 0 &&
|
||||
entry.inputTokens === 0 &&
|
||||
|
|
@ -405,18 +405,18 @@ export function formatLogTokenSummary(entry: RequestLogEntry, t: (value: string)
|
|||
return "-";
|
||||
}
|
||||
const values = [
|
||||
`${formatCompactNumber(entry.inputTokens)} ${t("入")}`,
|
||||
`${formatCompactNumber(entry.outputTokens)} ${t("出")}`
|
||||
`${formatCompactNumber(entry.inputTokens, locale)} ${t("入")}`,
|
||||
`${formatCompactNumber(entry.outputTokens, locale)} ${t("出")}`
|
||||
];
|
||||
|
||||
if (entry.cacheReadTokens > 0) {
|
||||
values.push(`${formatCompactNumber(entry.cacheReadTokens)} ${t("Cache")}`);
|
||||
values.push(`${formatCompactNumber(entry.cacheReadTokens, locale)} ${t("Cache")}`);
|
||||
}
|
||||
if (entry.cacheWriteTokens > 0) {
|
||||
values.push(`${formatCompactNumber(entry.cacheWriteTokens)} ${t("Cache write")}`);
|
||||
values.push(`${formatCompactNumber(entry.cacheWriteTokens, locale)} ${t("Cache write")}`);
|
||||
}
|
||||
if (entry.reasoningTokens > 0) {
|
||||
values.push(`${formatCompactNumber(entry.reasoningTokens)} ${t("Thinking")}`);
|
||||
values.push(`${formatCompactNumber(entry.reasoningTokens, locale)} ${t("Thinking")}`);
|
||||
}
|
||||
|
||||
return values.join(" ");
|
||||
|
|
|
|||
|
|
@ -137,8 +137,8 @@ export function emptyUsageTotals(): UsageTotals {
|
|||
};
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value: number): string {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
export function formatCompactNumber(value: number, locale?: Intl.LocalesArgument): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
maximumFractionDigits: value >= 1000 ? 1 : 0,
|
||||
notation: value >= 10000 ? "compact" : "standard"
|
||||
}).format(value);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
accountMetersForDisplay, accountProgressClass, accountProgressColor, accountSnapshotLabel, accountStatusClass, compareAccountSnapshots, formatAccountMeterTitle, formatAccountMeterValue,
|
||||
accountMetersForDisplay, accountProgressClass, accountProgressColor, accountSnapshotLabel, compareAccountSnapshots, formatAccountMeterTitle, formatAccountMeterValue,
|
||||
LoaderCircle, meterProgress, meterRemainingRatio, ProviderAccountMeter, ProviderAccountSnapshot, RefreshCw, TrayComponentVariants,
|
||||
useTrayText
|
||||
} from "../shared";
|
||||
|
|
@ -38,7 +38,7 @@ export function AccountSummaryPanel({
|
|||
<h3 className="truncate text-[11px] font-bold text-slate-100">{accountSnapshotLabel(snapshot)}</h3>
|
||||
<button
|
||||
aria-label={t("Refresh")}
|
||||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition disabled:cursor-not-allowed disabled:opacity-50 ${accountStatusButtonClass(snapshot.status)}`}
|
||||
className={`m-0 inline-flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent p-0 shadow-none transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${accountStatusButtonClass(snapshot.status)}`}
|
||||
disabled={refreshing || !onRefresh}
|
||||
onClick={() => {
|
||||
void onRefresh?.();
|
||||
|
|
@ -59,7 +59,16 @@ export function AccountSummaryPanel({
|
|||
}
|
||||
|
||||
function accountStatusButtonClass(status: ProviderAccountSnapshot["status"]): string {
|
||||
return `${accountStatusClass(status)} hover:border-white/20 hover:bg-white/[.08] hover:text-slate-50`;
|
||||
if (status === "critical" || status === "error") {
|
||||
return "text-rose-100 hover:text-rose-50";
|
||||
}
|
||||
if (status === "warning") {
|
||||
return "text-amber-100 hover:text-amber-50";
|
||||
}
|
||||
if (status === "ok") {
|
||||
return "text-teal-100 hover:text-teal-50";
|
||||
}
|
||||
return "text-slate-200 hover:text-slate-50";
|
||||
}
|
||||
|
||||
function AccountMeters({
|
||||
|
|
|
|||
45
tests/renderer/usage-format.test.ts
Normal file
45
tests/renderer/usage-format.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { formatLogTokenSummary } from "../../src/renderer/pages/home/shared/logs.ts";
|
||||
import { formatCompactNumber } from "../../src/renderer/pages/home/shared/usage.ts";
|
||||
import type { RequestLogEntry } from "../../src/shared/app.ts";
|
||||
|
||||
test("formatCompactNumber can be bound to the UI language locale", () => {
|
||||
assert.equal(formatCompactNumber(123456, "en-US"), "123.5K");
|
||||
assert.equal(formatCompactNumber(123456, "zh-CN"), "12.3万");
|
||||
});
|
||||
|
||||
test("formatLogTokenSummary uses the provided locale for token counts", () => {
|
||||
const entry: RequestLogEntry = {
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
client: "test",
|
||||
costUsd: 0,
|
||||
createdAt: "2026-06-30T00:00:00.000Z",
|
||||
credentialChain: [],
|
||||
credentialSaturated: false,
|
||||
durationMs: 10,
|
||||
id: 1,
|
||||
inputTokens: 123456,
|
||||
isStream: false,
|
||||
method: "POST",
|
||||
model: "test-model",
|
||||
ok: true,
|
||||
outputTokens: 12000,
|
||||
path: "/v1/messages",
|
||||
provider: "test-provider",
|
||||
reasoningTokens: 0,
|
||||
requestBody: { encoding: "utf8", text: "" },
|
||||
requestHeaders: {},
|
||||
requestId: "req_test",
|
||||
retryAttempts: [],
|
||||
responseHeaders: {},
|
||||
statusCode: 200,
|
||||
totalTokens: 135456,
|
||||
url: "https://example.test/v1/messages"
|
||||
};
|
||||
const translate = (value: string) => value;
|
||||
|
||||
assert.equal(formatLogTokenSummary(entry, translate, "en-US"), "123.5K 入 12K 出");
|
||||
assert.equal(formatLogTokenSummary(entry, translate, "zh-CN"), "12.3万 入 1.2万 出");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue