tokentrace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +167 -0
- package/.next/app-path-routes-manifest.json +22 -0
- package/.next/build-manifest.json +33 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +37 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +323 -0
- package/.next/routes-manifest.json +119 -0
- package/.next/server/app/_not-found/page.js +2 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +37 -0
- package/.next/server/app/api/analytics/route.js +1 -0
- package/.next/server/app/api/analytics/route.js.nft.json +1 -0
- package/.next/server/app/api/analytics/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/data/route.js +151 -0
- package/.next/server/app/api/data/route.js.nft.json +1 -0
- package/.next/server/app/api/data/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/export/route.js +1 -0
- package/.next/server/app/api/export/route.js.nft.json +1 -0
- package/.next/server/app/api/export/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/files/route.js +1 -0
- package/.next/server/app/api/files/route.js.nft.json +1 -0
- package/.next/server/app/api/files/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/prices/route.js +151 -0
- package/.next/server/app/api/prices/route.js.nft.json +1 -0
- package/.next/server/app/api/prices/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/scan/route.js +144 -0
- package/.next/server/app/api/scan/route.js.nft.json +1 -0
- package/.next/server/app/api/scan/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/settings/route.js +128 -0
- package/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
- package/.next/server/app/debug/page.js +2 -0
- package/.next/server/app/debug/page.js.nft.json +1 -0
- package/.next/server/app/debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/diagnostics/page.js +2 -0
- package/.next/server/app/diagnostics/page.js.nft.json +1 -0
- package/.next/server/app/diagnostics/page_client-reference-manifest.js +1 -0
- package/.next/server/app/discovery/page.js +2 -0
- package/.next/server/app/discovery/page.js.nft.json +1 -0
- package/.next/server/app/discovery/page_client-reference-manifest.js +1 -0
- package/.next/server/app/models/page.js +2 -0
- package/.next/server/app/models/page.js.nft.json +1 -0
- package/.next/server/app/models/page_client-reference-manifest.js +1 -0
- package/.next/server/app/optimisation/page.js +2 -0
- package/.next/server/app/optimisation/page.js.nft.json +1 -0
- package/.next/server/app/optimisation/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +2 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/parser-debug/page.js +2 -0
- package/.next/server/app/parser-debug/page.js.nft.json +1 -0
- package/.next/server/app/parser-debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/pricing/page.js +152 -0
- package/.next/server/app/pricing/page.js.nft.json +1 -0
- package/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
- package/.next/server/app/projects/page.js +2 -0
- package/.next/server/app/projects/page.js.nft.json +1 -0
- package/.next/server/app/projects/page_client-reference-manifest.js +1 -0
- package/.next/server/app/sessions/page.js +2 -0
- package/.next/server/app/sessions/page.js.nft.json +1 -0
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -0
- package/.next/server/app/settings/page.js +129 -0
- package/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -0
- package/.next/server/app/tools/page.js +2 -0
- package/.next/server/app/tools/page.js.nft.json +1 -0
- package/.next/server/app/tools/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/123.js +9 -0
- package/.next/server/chunks/153.js +1 -0
- package/.next/server/chunks/237.js +13 -0
- package/.next/server/chunks/331.js +22 -0
- package/.next/server/chunks/366.js +1 -0
- package/.next/server/chunks/444.js +267 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/692.js +1 -0
- package/.next/server/chunks/779.js +1 -0
- package/.next/server/chunks/815.js +1 -0
- package/.next/server/chunks/868.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_buildManifest.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_ssgManifest.js +1 -0
- package/.next/static/chunks/125-ab0f8db8f84c1166.js +1 -0
- package/.next/static/chunks/255-e881f48ae1d2333a.js +1 -0
- package/.next/static/chunks/4bd1b696-409494caf8c83275.js +1 -0
- package/.next/static/chunks/619-f072ac750404f9da.js +1 -0
- package/.next/static/chunks/850-8bc31e41590b5831.js +1 -0
- package/.next/static/chunks/938-23236de1c47554ea.js +1 -0
- package/.next/static/chunks/app/_not-found/page-6d75243350d9e0b5.js +1 -0
- package/.next/static/chunks/app/api/analytics/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/data/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/export/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/files/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/prices/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/scan/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/settings/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/diagnostics/page-053a5e810a59e548.js +1 -0
- package/.next/static/chunks/app/discovery/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/layout-8942804176ff26f3.js +1 -0
- package/.next/static/chunks/app/models/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/app/optimisation/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/parser-debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/pricing/page-5e27b1ae27314539.js +1 -0
- package/.next/static/chunks/app/projects/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/sessions/page-0abcdc88aac9dcaf.js +1 -0
- package/.next/static/chunks/app/settings/page-59fc80673f0750cd.js +1 -0
- package/.next/static/chunks/app/tools/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/framework-3457b9c2619cdd96.js +1 -0
- package/.next/static/chunks/main-8744520a8a31e6ae.js +1 -0
- package/.next/static/chunks/main-app-e9ccddef393e28c3.js +1 -0
- package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +1 -0
- package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-3fcacae817f3ffab.js +1 -0
- package/.next/static/css/366bb38b386229a5.css +3 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/app/api/analytics/route.ts +8 -0
- package/app/api/data/route.ts +9 -0
- package/app/api/export/route.ts +26 -0
- package/app/api/files/route.ts +8 -0
- package/app/api/prices/route.ts +33 -0
- package/app/api/scan/route.ts +15 -0
- package/app/api/settings/route.ts +25 -0
- package/app/debug/page.tsx +101 -0
- package/app/diagnostics/page.tsx +113 -0
- package/app/discovery/page.tsx +61 -0
- package/app/globals.css +51 -0
- package/app/layout.tsx +30 -0
- package/app/models/page.tsx +97 -0
- package/app/optimisation/page.tsx +67 -0
- package/app/page.tsx +164 -0
- package/app/parser-debug/page.tsx +57 -0
- package/app/pricing/page.tsx +18 -0
- package/app/projects/page.tsx +111 -0
- package/app/sessions/page.tsx +24 -0
- package/app/settings/page.tsx +26 -0
- package/app/tools/page.tsx +92 -0
- package/bin/tokentrace.js +316 -0
- package/components/charts/rank-bar-chart.tsx +69 -0
- package/components/charts/trend-chart.tsx +123 -0
- package/components/empty-state.tsx +14 -0
- package/components/pricing-settings.tsx +171 -0
- package/components/session-explorer.tsx +210 -0
- package/components/settings-panel.tsx +203 -0
- package/components/sidebar.tsx +88 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button.tsx +47 -0
- package/components/ui/card.tsx +22 -0
- package/components/ui/input.tsx +19 -0
- package/components/ui/label.tsx +6 -0
- package/components/ui/table.tsx +31 -0
- package/components/ui/textarea.tsx +18 -0
- package/components.json +16 -0
- package/dist/runtime/db-migrate.mjs +410 -0
- package/dist/runtime/db-seed.mjs +506 -0
- package/dist/runtime/reset.mjs +519 -0
- package/dist/runtime/scan.mjs +1817 -0
- package/fixtures/generic-jsonl/sample.jsonl +2 -0
- package/next.config.mjs +7 -0
- package/package.json +96 -0
- package/postcss.config.mjs +8 -0
- package/scripts/build-cli-runtime.mjs +40 -0
- package/scripts/db-migrate.ts +5 -0
- package/scripts/db-seed.ts +5 -0
- package/scripts/reset.ts +5 -0
- package/scripts/scan.ts +30 -0
- package/src/db/client.ts +32 -0
- package/src/db/migrate-core.ts +147 -0
- package/src/db/reset.ts +14 -0
- package/src/db/schema.ts +259 -0
- package/src/db/seed.ts +110 -0
- package/src/db/settings.ts +47 -0
- package/src/ingestion/adapters/claude-code.ts +78 -0
- package/src/ingestion/adapters/codex-cli.ts +82 -0
- package/src/ingestion/adapters/generic-json.ts +93 -0
- package/src/ingestion/adapters/generic-jsonl.ts +62 -0
- package/src/ingestion/adapters/generic-log.ts +144 -0
- package/src/ingestion/adapters/generic-records.ts +178 -0
- package/src/ingestion/adapters/helpers.ts +309 -0
- package/src/ingestion/adapters/index.ts +15 -0
- package/src/ingestion/discovery.ts +130 -0
- package/src/ingestion/persist.ts +283 -0
- package/src/ingestion/scan.ts +247 -0
- package/src/ingestion/types.ts +78 -0
- package/src/lib/analytics.ts +592 -0
- package/src/lib/cost.ts +62 -0
- package/src/lib/csv.ts +15 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/ids.ts +23 -0
- package/src/lib/pricing.ts +86 -0
- package/src/lib/token-estimator.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +53 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import getPort, { portNumbers } from "get-port";
|
|
11
|
+
import open from "open";
|
|
12
|
+
|
|
13
|
+
const binPath = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageRoot = path.resolve(path.dirname(binPath), "..");
|
|
15
|
+
const invocationCwd = process.cwd();
|
|
16
|
+
const packageJson = JSON.parse(
|
|
17
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
function help() {
|
|
21
|
+
return `TokenTrace CLI
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
tokentrace Start local dashboard
|
|
25
|
+
tokentrace serve Start local dashboard
|
|
26
|
+
tokentrace scan Scan local AI CLI usage logs
|
|
27
|
+
tokentrace run <cmd> Run a command and record wrapper diagnostics
|
|
28
|
+
tokentrace reset Reset local database
|
|
29
|
+
tokentrace --version Print version`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appDataDir() {
|
|
33
|
+
if (process.env.TOKENTRACE_HOME) return path.resolve(process.env.TOKENTRACE_HOME);
|
|
34
|
+
const home = os.homedir();
|
|
35
|
+
if (process.platform === "darwin") {
|
|
36
|
+
return path.join(home, "Library", "Application Support", "TokenTrace");
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "TokenTrace");
|
|
40
|
+
}
|
|
41
|
+
return path.join(process.env.XDG_DATA_HOME ?? path.join(home, ".local", "share"), "tokentrace");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runtimeEnv() {
|
|
45
|
+
const dataDir = appDataDir();
|
|
46
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
47
|
+
const dbPath = path.join(dataDir, "tokentrace.db");
|
|
48
|
+
return {
|
|
49
|
+
...process.env,
|
|
50
|
+
TOKENTRACE_DB: process.env.TOKENTRACE_DB ?? dbPath,
|
|
51
|
+
DATABASE_URL: process.env.DATABASE_URL ?? `file:${dbPath}`,
|
|
52
|
+
TOKENTRACE_APP_DATA_DIR: dataDir,
|
|
53
|
+
TOKENTRACE_WORKDIR: invocationCwd,
|
|
54
|
+
NEXT_TELEMETRY_DISABLED: "1"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nextBin() {
|
|
59
|
+
return path.join(packageRoot, "node_modules", "next", "dist", "bin", "next");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runtimeScriptPath(scriptName) {
|
|
63
|
+
const compiled = path.join(packageRoot, "dist", "runtime", `${scriptName}.mjs`);
|
|
64
|
+
if (fs.existsSync(compiled)) return compiled;
|
|
65
|
+
return path.join(packageRoot, "scripts", `${scriptName}.ts`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scriptCommand(scriptName, args) {
|
|
69
|
+
const scriptPath = runtimeScriptPath(scriptName);
|
|
70
|
+
if (scriptPath.endsWith(".mjs")) {
|
|
71
|
+
return [process.execPath, [scriptPath, ...args]];
|
|
72
|
+
}
|
|
73
|
+
const tsx = path.join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
74
|
+
return [process.execPath, [tsx, scriptPath, ...args]];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runNodeScript(scriptName, args = [], options = {}) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const [command, commandArgs] = scriptCommand(scriptName, args);
|
|
80
|
+
const child = spawn(command, commandArgs, {
|
|
81
|
+
cwd: packageRoot,
|
|
82
|
+
env: runtimeEnv(),
|
|
83
|
+
stdio: options.stdio ?? "inherit"
|
|
84
|
+
});
|
|
85
|
+
child.on("error", reject);
|
|
86
|
+
child.on("exit", (code) => {
|
|
87
|
+
if (code === 0) resolve();
|
|
88
|
+
else reject(new Error(`${scriptName} exited with code ${code}`));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function initializeDatabase({ quiet = false } = {}) {
|
|
94
|
+
const env = runtimeEnv();
|
|
95
|
+
if (!quiet) {
|
|
96
|
+
console.log(`TokenTrace data: ${env.TOKENTRACE_APP_DATA_DIR}`);
|
|
97
|
+
}
|
|
98
|
+
await runNodeScript("db-migrate", [], { stdio: quiet ? "ignore" : "inherit" });
|
|
99
|
+
await runNodeScript("db-seed", [], { stdio: quiet ? "ignore" : "inherit" });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sleep(ms) {
|
|
103
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function waitForServer(url, child) {
|
|
107
|
+
const deadline = Date.now() + 30_000;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
if (child.exitCode != null) {
|
|
110
|
+
throw new Error(`TokenTrace server exited with code ${child.exitCode}`);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
114
|
+
if (response.ok) return;
|
|
115
|
+
} catch {
|
|
116
|
+
// Keep polling until the server is ready or the timeout expires.
|
|
117
|
+
}
|
|
118
|
+
await sleep(300);
|
|
119
|
+
}
|
|
120
|
+
throw new Error("Timed out waiting for the TokenTrace server to start.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function serve() {
|
|
124
|
+
const buildId = path.join(packageRoot, ".next", "BUILD_ID");
|
|
125
|
+
if (!fs.existsSync(buildId)) {
|
|
126
|
+
console.error("TokenTrace is not built yet. Run `npm run build` before using the package CLI from a source checkout.");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await initializeDatabase();
|
|
131
|
+
const port = await getPort({ port: portNumbers(3030, 3999) });
|
|
132
|
+
const hostname = "127.0.0.1";
|
|
133
|
+
const url = `http://localhost:${port}`;
|
|
134
|
+
|
|
135
|
+
console.log(`Starting TokenTrace at ${url}`);
|
|
136
|
+
console.log("Press Ctrl+C to stop the server.");
|
|
137
|
+
|
|
138
|
+
const child = spawn(
|
|
139
|
+
process.execPath,
|
|
140
|
+
[nextBin(), "start", "--hostname", hostname, "--port", String(port)],
|
|
141
|
+
{
|
|
142
|
+
cwd: packageRoot,
|
|
143
|
+
env: {
|
|
144
|
+
...runtimeEnv(),
|
|
145
|
+
PORT: String(port),
|
|
146
|
+
HOSTNAME: hostname
|
|
147
|
+
},
|
|
148
|
+
stdio: "inherit"
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const stop = () => {
|
|
153
|
+
if (!child.killed) child.kill("SIGINT");
|
|
154
|
+
};
|
|
155
|
+
process.on("SIGINT", stop);
|
|
156
|
+
process.on("SIGTERM", stop);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await waitForServer(url, child);
|
|
160
|
+
await open(url).catch(() => {
|
|
161
|
+
console.log(`Open this URL in your browser: ${url}`);
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(error instanceof Error ? error.message : "Failed to start TokenTrace.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function scan(args) {
|
|
171
|
+
await initializeDatabase({ quiet: true });
|
|
172
|
+
await runNodeScript("scan", args);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function reset(args) {
|
|
176
|
+
await initializeDatabase({ quiet: true });
|
|
177
|
+
if (!args.includes("--yes")) {
|
|
178
|
+
const rl = createInterface({ input, output });
|
|
179
|
+
const answer = await rl.question(
|
|
180
|
+
"Reset TokenTrace imported data and scan history? Settings and pricing will be kept. Continue? [y/N] "
|
|
181
|
+
);
|
|
182
|
+
rl.close();
|
|
183
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
184
|
+
console.log("Reset cancelled.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
await runNodeScript("reset", []);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function looksStructured(text) {
|
|
192
|
+
const trimmed = text.trim();
|
|
193
|
+
if (!trimmed || trimmed.length > 100_000) return null;
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(trimmed);
|
|
196
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
197
|
+
} catch {
|
|
198
|
+
// Not structured JSON.
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runWrapped(args) {
|
|
204
|
+
if (!args.length) {
|
|
205
|
+
console.error("Usage: tokentrace run <command> [args...]");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const env = runtimeEnv();
|
|
210
|
+
fs.mkdirSync(path.join(env.TOKENTRACE_APP_DATA_DIR, "wrapper-runs"), {
|
|
211
|
+
recursive: true
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const [command, ...commandArgs] = args;
|
|
215
|
+
const startedAt = new Date();
|
|
216
|
+
let stdoutBytes = 0;
|
|
217
|
+
let stderrBytes = 0;
|
|
218
|
+
const stdoutChunks = [];
|
|
219
|
+
const stderrChunks = [];
|
|
220
|
+
|
|
221
|
+
const child = spawn(command, commandArgs, {
|
|
222
|
+
cwd: invocationCwd,
|
|
223
|
+
env: process.env,
|
|
224
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
225
|
+
shell: process.platform === "win32"
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
child.stdout.on("data", (chunk) => {
|
|
229
|
+
stdoutBytes += chunk.length;
|
|
230
|
+
if (stdoutBytes <= 100_000) stdoutChunks.push(chunk);
|
|
231
|
+
process.stdout.write(chunk);
|
|
232
|
+
});
|
|
233
|
+
child.stderr.on("data", (chunk) => {
|
|
234
|
+
stderrBytes += chunk.length;
|
|
235
|
+
if (stderrBytes <= 100_000) stderrChunks.push(chunk);
|
|
236
|
+
process.stderr.write(chunk);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
240
|
+
child.on("error", reject);
|
|
241
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const endedAt = new Date();
|
|
245
|
+
const durationMs = endedAt.getTime() - startedAt.getTime();
|
|
246
|
+
const stdoutSample = Buffer.concat(stdoutChunks).toString("utf8");
|
|
247
|
+
const stderrSample = Buffer.concat(stderrChunks).toString("utf8");
|
|
248
|
+
const structuredOutput = looksStructured(stdoutSample);
|
|
249
|
+
const sessionId = `wrapper-${randomUUID()}`;
|
|
250
|
+
const record = {
|
|
251
|
+
timestamp: endedAt.toISOString(),
|
|
252
|
+
session_id: sessionId,
|
|
253
|
+
role: "tool",
|
|
254
|
+
type: "tokentrace.wrapper_run",
|
|
255
|
+
cwd: invocationCwd,
|
|
256
|
+
content: `Wrapper run for ${command} completed in ${durationMs}ms with ${stdoutBytes} stdout bytes and ${stderrBytes} stderr bytes.`,
|
|
257
|
+
command,
|
|
258
|
+
args: commandArgs,
|
|
259
|
+
duration_ms: durationMs,
|
|
260
|
+
stdout_bytes: stdoutBytes,
|
|
261
|
+
stderr_bytes: stderrBytes,
|
|
262
|
+
exit_code: exitCode,
|
|
263
|
+
structured_output_detected: Boolean(structuredOutput),
|
|
264
|
+
structured_output_preview: structuredOutput ?? undefined
|
|
265
|
+
};
|
|
266
|
+
const logPath = path.join(env.TOKENTRACE_APP_DATA_DIR, "wrapper-runs", "runs.jsonl");
|
|
267
|
+
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
268
|
+
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log("TokenTrace wrapper summary");
|
|
271
|
+
console.log(`Duration: ${durationMs}ms`);
|
|
272
|
+
console.log(`stdout bytes: ${stdoutBytes}`);
|
|
273
|
+
console.log(`stderr bytes: ${stderrBytes}`);
|
|
274
|
+
console.log(`exit code: ${exitCode}`);
|
|
275
|
+
console.log(`diagnostic log: ${logPath}`);
|
|
276
|
+
|
|
277
|
+
process.exit(exitCode);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function main() {
|
|
281
|
+
const [command = "serve", ...args] = process.argv.slice(2);
|
|
282
|
+
|
|
283
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
284
|
+
console.log(help());
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (command === "--version" || command === "-v") {
|
|
288
|
+
console.log(packageJson.version);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (command === "serve") {
|
|
292
|
+
await serve();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (command === "scan") {
|
|
296
|
+
await scan(args);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (command === "run") {
|
|
300
|
+
await runWrapped(args);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (command === "reset") {
|
|
304
|
+
await reset(args);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.error(`Unknown command: ${command}\n`);
|
|
309
|
+
console.error(help());
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
main().catch((error) => {
|
|
314
|
+
console.error(error instanceof Error ? error.message : error);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
ResponsiveContainer,
|
|
8
|
+
Tooltip,
|
|
9
|
+
XAxis,
|
|
10
|
+
YAxis
|
|
11
|
+
} from "recharts";
|
|
12
|
+
import { formatCurrency, formatTokens } from "@/src/lib/format";
|
|
13
|
+
|
|
14
|
+
export function RankBarChart({
|
|
15
|
+
data,
|
|
16
|
+
nameKey,
|
|
17
|
+
valueKey,
|
|
18
|
+
mode = "tokens",
|
|
19
|
+
color = "#ea580c"
|
|
20
|
+
}: {
|
|
21
|
+
data: Array<Record<string, string | number | null>>;
|
|
22
|
+
nameKey: string;
|
|
23
|
+
valueKey: string;
|
|
24
|
+
mode?: "tokens" | "cost" | "count";
|
|
25
|
+
color?: string;
|
|
26
|
+
}) {
|
|
27
|
+
const chartData = data
|
|
28
|
+
.slice(0, 8)
|
|
29
|
+
.map((item) => ({
|
|
30
|
+
name: String(item[nameKey] ?? "Unknown"),
|
|
31
|
+
value: Number(item[valueKey] ?? 0)
|
|
32
|
+
}))
|
|
33
|
+
.filter((item) => item.value > 0);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="h-72">
|
|
37
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
38
|
+
<BarChart data={chartData} layout="vertical" margin={{ left: 16, right: 12, top: 8, bottom: 0 }}>
|
|
39
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#e7ded3" />
|
|
40
|
+
<XAxis
|
|
41
|
+
type="number"
|
|
42
|
+
tick={{ fontSize: 12 }}
|
|
43
|
+
stroke="#786f65"
|
|
44
|
+
tickFormatter={(value) =>
|
|
45
|
+
mode === "cost" ? `$${Number(value).toFixed(0)}` : formatTokens(Number(value))
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
<YAxis
|
|
49
|
+
dataKey="name"
|
|
50
|
+
type="category"
|
|
51
|
+
width={120}
|
|
52
|
+
tick={{ fontSize: 12 }}
|
|
53
|
+
stroke="#786f65"
|
|
54
|
+
/>
|
|
55
|
+
<Tooltip
|
|
56
|
+
formatter={(value) =>
|
|
57
|
+
mode === "cost"
|
|
58
|
+
? formatCurrency(Number(value))
|
|
59
|
+
: mode === "tokens"
|
|
60
|
+
? `${formatTokens(Number(value))} tokens`
|
|
61
|
+
: Number(value).toLocaleString()
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
<Bar dataKey="value" fill={color} radius={[0, 4, 4, 0]} />
|
|
65
|
+
</BarChart>
|
|
66
|
+
</ResponsiveContainer>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Area,
|
|
6
|
+
AreaChart,
|
|
7
|
+
CartesianGrid,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
Tooltip,
|
|
10
|
+
XAxis,
|
|
11
|
+
YAxis
|
|
12
|
+
} from "recharts";
|
|
13
|
+
import type { TrendPoint } from "@/src/lib/analytics";
|
|
14
|
+
import { formatCurrency, formatShortDate, formatTokens } from "@/src/lib/format";
|
|
15
|
+
import { Button } from "@/components/ui/button";
|
|
16
|
+
|
|
17
|
+
type Period = "daily" | "weekly" | "monthly";
|
|
18
|
+
|
|
19
|
+
function bucketFor(date: string, period: Period) {
|
|
20
|
+
const parsed = new Date(`${date}T00:00:00`);
|
|
21
|
+
if (period === "monthly") return `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, "0")}-01`;
|
|
22
|
+
if (period === "weekly") {
|
|
23
|
+
const day = parsed.getDay() || 7;
|
|
24
|
+
parsed.setDate(parsed.getDate() - day + 1);
|
|
25
|
+
return parsed.toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
return date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function TrendChart({
|
|
31
|
+
data,
|
|
32
|
+
metric,
|
|
33
|
+
color = "#0f766e"
|
|
34
|
+
}: {
|
|
35
|
+
data: TrendPoint[];
|
|
36
|
+
metric: "totalTokens" | "cost";
|
|
37
|
+
color?: string;
|
|
38
|
+
}) {
|
|
39
|
+
const [period, setPeriod] = useState<Period>("daily");
|
|
40
|
+
const chartData = useMemo(() => {
|
|
41
|
+
const buckets = new Map<string, TrendPoint>();
|
|
42
|
+
data.forEach((point) => {
|
|
43
|
+
const key = bucketFor(point.date, period);
|
|
44
|
+
const existing =
|
|
45
|
+
buckets.get(key) ??
|
|
46
|
+
({
|
|
47
|
+
date: key,
|
|
48
|
+
totalTokens: 0,
|
|
49
|
+
inputTokens: 0,
|
|
50
|
+
outputTokens: 0,
|
|
51
|
+
cachedTokens: 0,
|
|
52
|
+
reasoningTokens: 0,
|
|
53
|
+
cost: 0
|
|
54
|
+
} satisfies TrendPoint);
|
|
55
|
+
existing.totalTokens += point.totalTokens;
|
|
56
|
+
existing.inputTokens += point.inputTokens;
|
|
57
|
+
existing.outputTokens += point.outputTokens;
|
|
58
|
+
existing.cachedTokens += point.cachedTokens;
|
|
59
|
+
existing.reasoningTokens += point.reasoningTokens;
|
|
60
|
+
existing.cost += point.cost;
|
|
61
|
+
buckets.set(key, existing);
|
|
62
|
+
});
|
|
63
|
+
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
64
|
+
}, [data, period]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-3">
|
|
68
|
+
<div className="flex flex-wrap gap-2">
|
|
69
|
+
{(["daily", "weekly", "monthly"] as const).map((item) => (
|
|
70
|
+
<Button
|
|
71
|
+
key={item}
|
|
72
|
+
size="sm"
|
|
73
|
+
variant={period === item ? "default" : "outline"}
|
|
74
|
+
onClick={() => setPeriod(item)}
|
|
75
|
+
>
|
|
76
|
+
{item[0].toUpperCase() + item.slice(1)}
|
|
77
|
+
</Button>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="h-72">
|
|
81
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
82
|
+
<AreaChart data={chartData} margin={{ left: 0, right: 8, top: 8, bottom: 0 }}>
|
|
83
|
+
<defs>
|
|
84
|
+
<linearGradient id={`trend-${metric}`} x1="0" y1="0" x2="0" y2="1">
|
|
85
|
+
<stop offset="5%" stopColor={color} stopOpacity={0.35} />
|
|
86
|
+
<stop offset="95%" stopColor={color} stopOpacity={0.02} />
|
|
87
|
+
</linearGradient>
|
|
88
|
+
</defs>
|
|
89
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#e7ded3" />
|
|
90
|
+
<XAxis
|
|
91
|
+
dataKey="date"
|
|
92
|
+
tickFormatter={formatShortDate}
|
|
93
|
+
tick={{ fontSize: 12 }}
|
|
94
|
+
stroke="#786f65"
|
|
95
|
+
/>
|
|
96
|
+
<YAxis
|
|
97
|
+
tick={{ fontSize: 12 }}
|
|
98
|
+
stroke="#786f65"
|
|
99
|
+
tickFormatter={(value) =>
|
|
100
|
+
metric === "cost" ? `$${Number(value).toFixed(0)}` : formatTokens(Number(value))
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
<Tooltip
|
|
104
|
+
formatter={(value) =>
|
|
105
|
+
metric === "cost"
|
|
106
|
+
? formatCurrency(Number(value))
|
|
107
|
+
: `${formatTokens(Number(value))} tokens`
|
|
108
|
+
}
|
|
109
|
+
labelFormatter={formatShortDate}
|
|
110
|
+
/>
|
|
111
|
+
<Area
|
|
112
|
+
type="monotone"
|
|
113
|
+
dataKey={metric}
|
|
114
|
+
stroke={color}
|
|
115
|
+
fill={`url(#trend-${metric})`}
|
|
116
|
+
strokeWidth={2}
|
|
117
|
+
/>
|
|
118
|
+
</AreaChart>
|
|
119
|
+
</ResponsiveContainer>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Database } from "lucide-react";
|
|
2
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
3
|
+
|
|
4
|
+
export function EmptyState({ title, description }: { title: string; description: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<Card>
|
|
7
|
+
<CardContent className="flex min-h-48 flex-col items-center justify-center gap-2 text-center">
|
|
8
|
+
<Database className="h-8 w-8 text-muted-foreground" />
|
|
9
|
+
<div className="font-medium">{title}</div>
|
|
10
|
+
<p className="max-w-md text-sm text-muted-foreground">{description}</p>
|
|
11
|
+
</CardContent>
|
|
12
|
+
</Card>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from "react";
|
|
4
|
+
import { Plus, Save } from "lucide-react";
|
|
5
|
+
import type { PricingRow } from "@/src/lib/pricing";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { Label } from "@/components/ui/label";
|
|
10
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
11
|
+
|
|
12
|
+
type EditablePricingRow = PricingRow & {
|
|
13
|
+
providerName?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function numberInputValue(value: number | null) {
|
|
17
|
+
return value == null ? "" : String(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function PricingSettings({ initialRows }: { initialRows: PricingRow[] }) {
|
|
21
|
+
const [rows, setRows] = useState<EditablePricingRow[]>(initialRows);
|
|
22
|
+
const [isPending, startTransition] = useTransition();
|
|
23
|
+
const [message, setMessage] = useState("");
|
|
24
|
+
|
|
25
|
+
function updateRow(index: number, patch: Partial<EditablePricingRow>) {
|
|
26
|
+
setRows((current) =>
|
|
27
|
+
current.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addRow() {
|
|
32
|
+
setRows((current) => [
|
|
33
|
+
{
|
|
34
|
+
id: `new-${Date.now()}`,
|
|
35
|
+
providerId: "custom",
|
|
36
|
+
provider: "Custom",
|
|
37
|
+
providerName: "Custom",
|
|
38
|
+
model: "",
|
|
39
|
+
inputTokenPrice: null,
|
|
40
|
+
outputTokenPrice: null,
|
|
41
|
+
cachedInputTokenPrice: null,
|
|
42
|
+
currency: "USD",
|
|
43
|
+
effectiveFrom: null
|
|
44
|
+
},
|
|
45
|
+
...current
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveRow(row: EditablePricingRow) {
|
|
50
|
+
startTransition(async () => {
|
|
51
|
+
setMessage("");
|
|
52
|
+
const response = await fetch("/api/prices", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
providerId: row.providerId,
|
|
57
|
+
providerName: row.providerName ?? row.provider,
|
|
58
|
+
model: row.model,
|
|
59
|
+
inputTokenPrice: row.inputTokenPrice,
|
|
60
|
+
outputTokenPrice: row.outputTokenPrice,
|
|
61
|
+
cachedInputTokenPrice: row.cachedInputTokenPrice,
|
|
62
|
+
currency: row.currency
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
setMessage("Price save failed.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const latest = (await fetch("/api/prices").then((res) => res.json())) as PricingRow[];
|
|
70
|
+
setRows(latest);
|
|
71
|
+
setMessage("Price saved.");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
79
|
+
<div>
|
|
80
|
+
<CardTitle>Model Pricing</CardTitle>
|
|
81
|
+
<CardDescription>
|
|
82
|
+
Prices are per 1M tokens. Seed values are editable placeholders.
|
|
83
|
+
</CardDescription>
|
|
84
|
+
</div>
|
|
85
|
+
<Button variant="outline" onClick={addRow}>
|
|
86
|
+
<Plus className="h-4 w-4" />
|
|
87
|
+
Add model
|
|
88
|
+
</Button>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent className="table-scroll">
|
|
91
|
+
<Table>
|
|
92
|
+
<TableHeader>
|
|
93
|
+
<TableRow>
|
|
94
|
+
<TableHead>Provider ID</TableHead>
|
|
95
|
+
<TableHead>Provider</TableHead>
|
|
96
|
+
<TableHead>Model</TableHead>
|
|
97
|
+
<TableHead>Input / 1M</TableHead>
|
|
98
|
+
<TableHead>Output / 1M</TableHead>
|
|
99
|
+
<TableHead>Cached input / 1M</TableHead>
|
|
100
|
+
<TableHead>Currency</TableHead>
|
|
101
|
+
<TableHead></TableHead>
|
|
102
|
+
</TableRow>
|
|
103
|
+
</TableHeader>
|
|
104
|
+
<TableBody>
|
|
105
|
+
{rows.map((row, index) => (
|
|
106
|
+
<TableRow key={row.id}>
|
|
107
|
+
<TableCell>
|
|
108
|
+
<Input value={row.providerId} onChange={(event) => updateRow(index, { providerId: event.target.value })} />
|
|
109
|
+
</TableCell>
|
|
110
|
+
<TableCell>
|
|
111
|
+
<Input value={row.providerName ?? row.provider} onChange={(event) => updateRow(index, { providerName: event.target.value })} />
|
|
112
|
+
</TableCell>
|
|
113
|
+
<TableCell>
|
|
114
|
+
<Input value={row.model} onChange={(event) => updateRow(index, { model: event.target.value })} />
|
|
115
|
+
</TableCell>
|
|
116
|
+
<TableCell>
|
|
117
|
+
<Input
|
|
118
|
+
inputMode="decimal"
|
|
119
|
+
value={numberInputValue(row.inputTokenPrice)}
|
|
120
|
+
onChange={(event) => updateRow(index, { inputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
|
|
121
|
+
/>
|
|
122
|
+
</TableCell>
|
|
123
|
+
<TableCell>
|
|
124
|
+
<Input
|
|
125
|
+
inputMode="decimal"
|
|
126
|
+
value={numberInputValue(row.outputTokenPrice)}
|
|
127
|
+
onChange={(event) => updateRow(index, { outputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
|
|
128
|
+
/>
|
|
129
|
+
</TableCell>
|
|
130
|
+
<TableCell>
|
|
131
|
+
<Input
|
|
132
|
+
inputMode="decimal"
|
|
133
|
+
value={numberInputValue(row.cachedInputTokenPrice)}
|
|
134
|
+
onChange={(event) => updateRow(index, { cachedInputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
|
|
135
|
+
/>
|
|
136
|
+
</TableCell>
|
|
137
|
+
<TableCell>
|
|
138
|
+
<Input value={row.currency} onChange={(event) => updateRow(index, { currency: event.target.value })} />
|
|
139
|
+
</TableCell>
|
|
140
|
+
<TableCell>
|
|
141
|
+
<Button size="sm" onClick={() => saveRow(row)} disabled={isPending || !row.model}>
|
|
142
|
+
<Save className="h-4 w-4" />
|
|
143
|
+
Save
|
|
144
|
+
</Button>
|
|
145
|
+
</TableCell>
|
|
146
|
+
</TableRow>
|
|
147
|
+
))}
|
|
148
|
+
</TableBody>
|
|
149
|
+
</Table>
|
|
150
|
+
{message ? <div className="mt-3 text-sm text-muted-foreground">{message}</div> : null}
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
|
|
154
|
+
<Card>
|
|
155
|
+
<CardHeader>
|
|
156
|
+
<CardTitle>Cost Formula</CardTitle>
|
|
157
|
+
<CardDescription>Costs are computed per interaction and then aggregated.</CardDescription>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
|
160
|
+
<Label>Formula</Label>
|
|
161
|
+
<div className="rounded-md border bg-muted/40 p-3 font-mono text-xs text-foreground">
|
|
162
|
+
input * inputPrice + output * outputPrice + cacheRead * cachedInputPrice + cacheWrite * inputPrice
|
|
163
|
+
</div>
|
|
164
|
+
<p>
|
|
165
|
+
TokenTrace separates exact token counts from estimated counts. Unknown model prices produce unknown costs.
|
|
166
|
+
</p>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|