@diogonzafe/tokenwatch 0.8.0 → 0.10.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/dist/adapters.cjs +16 -6
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +2 -1
- package/dist/adapters.d.ts +2 -1
- package/dist/adapters.js +16 -6
- package/dist/adapters.js.map +1 -1
- package/dist/cli.js +158 -13
- package/dist/cli.js.map +1 -1
- package/dist/exporters.d.cts +1 -1
- package/dist/exporters.d.ts +1 -1
- package/dist/{index-CFBI-1ab.d.cts → index-B5OF0YCl.d.cts} +13 -0
- package/dist/{index-CFBI-1ab.d.ts → index-B5OF0YCl.d.ts} +13 -0
- package/dist/index.cjs +101 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +101 -29
- package/dist/index.js.map +1 -1
- package/dist/langchain.d.cts +1 -1
- package/dist/langchain.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ function rowToEntry(r) {
|
|
|
30
30
|
...r["user_id"] != null && { userId: r["user_id"] },
|
|
31
31
|
...r["feature"] != null && { feature: r["feature"] },
|
|
32
32
|
...r["app_id"] != null && { appId: r["app_id"] },
|
|
33
|
+
...r["metadata"] != null && { metadata: r["metadata"] },
|
|
33
34
|
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
34
35
|
};
|
|
35
36
|
}
|
|
@@ -59,6 +60,7 @@ var init_postgres = __esm({
|
|
|
59
60
|
user_id TEXT,
|
|
60
61
|
feature TEXT,
|
|
61
62
|
app_id TEXT,
|
|
63
|
+
metadata JSONB,
|
|
62
64
|
timestamp TIMESTAMPTZ NOT NULL
|
|
63
65
|
)
|
|
64
66
|
`);
|
|
@@ -67,7 +69,8 @@ var init_postgres = __esm({
|
|
|
67
69
|
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT",
|
|
68
70
|
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0",
|
|
69
71
|
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0",
|
|
70
|
-
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS app_id TEXT"
|
|
72
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS app_id TEXT",
|
|
73
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS metadata JSONB"
|
|
71
74
|
]) {
|
|
72
75
|
await this.client.query(col).catch(() => {
|
|
73
76
|
});
|
|
@@ -77,8 +80,8 @@ var init_postgres = __esm({
|
|
|
77
80
|
this.client.query(
|
|
78
81
|
`INSERT INTO tokenwatch_usage
|
|
79
82
|
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
80
|
-
cost_usd, session_id, user_id, feature, app_id, timestamp)
|
|
81
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
|
83
|
+
cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
|
|
84
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
82
85
|
[
|
|
83
86
|
entry.model,
|
|
84
87
|
entry.inputTokens,
|
|
@@ -91,6 +94,7 @@ var init_postgres = __esm({
|
|
|
91
94
|
entry.userId ?? null,
|
|
92
95
|
entry.feature ?? null,
|
|
93
96
|
entry.appId ?? null,
|
|
97
|
+
entry.metadata ?? null,
|
|
94
98
|
entry.timestamp
|
|
95
99
|
]
|
|
96
100
|
).catch((err) => {
|
|
@@ -137,6 +141,7 @@ function rowToEntry2(r) {
|
|
|
137
141
|
...r["user_id"] != null && { userId: r["user_id"] },
|
|
138
142
|
...r["feature"] != null && { feature: r["feature"] },
|
|
139
143
|
...r["app_id"] != null && { appId: r["app_id"] },
|
|
144
|
+
...r["metadata"] != null && { metadata: typeof r["metadata"] === "string" ? JSON.parse(r["metadata"]) : r["metadata"] },
|
|
140
145
|
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
141
146
|
};
|
|
142
147
|
}
|
|
@@ -166,6 +171,7 @@ var init_mysql = __esm({
|
|
|
166
171
|
user_id VARCHAR(255),
|
|
167
172
|
feature VARCHAR(255),
|
|
168
173
|
app_id VARCHAR(255),
|
|
174
|
+
metadata JSON,
|
|
169
175
|
timestamp DATETIME(3) NOT NULL
|
|
170
176
|
)
|
|
171
177
|
`);
|
|
@@ -175,7 +181,8 @@ var init_mysql = __esm({
|
|
|
175
181
|
ADD COLUMN IF NOT EXISTS feature VARCHAR(255),
|
|
176
182
|
ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,
|
|
177
183
|
ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0,
|
|
178
|
-
ADD COLUMN IF NOT EXISTS app_id VARCHAR(255)
|
|
184
|
+
ADD COLUMN IF NOT EXISTS app_id VARCHAR(255),
|
|
185
|
+
ADD COLUMN IF NOT EXISTS metadata JSON
|
|
179
186
|
`).catch(() => {
|
|
180
187
|
});
|
|
181
188
|
}
|
|
@@ -183,8 +190,8 @@ var init_mysql = __esm({
|
|
|
183
190
|
this.client.execute(
|
|
184
191
|
`INSERT INTO tokenwatch_usage
|
|
185
192
|
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
186
|
-
cost_usd, session_id, user_id, feature, app_id, timestamp)
|
|
187
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
193
|
+
cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
188
195
|
[
|
|
189
196
|
entry.model,
|
|
190
197
|
entry.inputTokens,
|
|
@@ -197,6 +204,7 @@ var init_mysql = __esm({
|
|
|
197
204
|
entry.userId ?? null,
|
|
198
205
|
entry.feature ?? null,
|
|
199
206
|
entry.appId ?? null,
|
|
207
|
+
entry.metadata != null ? JSON.stringify(entry.metadata) : null,
|
|
200
208
|
entry.timestamp
|
|
201
209
|
]
|
|
202
210
|
).catch((err) => {
|
|
@@ -240,6 +248,7 @@ function docToEntry(doc) {
|
|
|
240
248
|
...doc.userId != null && { userId: doc.userId },
|
|
241
249
|
...doc.feature != null && { feature: doc.feature },
|
|
242
250
|
...doc.appId != null && { appId: doc.appId },
|
|
251
|
+
...doc.metadata != null && { metadata: doc.metadata },
|
|
243
252
|
timestamp: doc.timestamp
|
|
244
253
|
};
|
|
245
254
|
}
|
|
@@ -274,6 +283,7 @@ var init_mongodb = __esm({
|
|
|
274
283
|
userId: entry.userId ?? null,
|
|
275
284
|
...entry.feature !== void 0 && { feature: entry.feature },
|
|
276
285
|
...entry.appId !== void 0 && { appId: entry.appId },
|
|
286
|
+
...entry.metadata !== void 0 && { metadata: entry.metadata },
|
|
277
287
|
timestamp: entry.timestamp
|
|
278
288
|
}).catch((err) => {
|
|
279
289
|
console.warn("[tokenwatch] MongoStorage.record failed:", err);
|
|
@@ -404,6 +414,7 @@ var SqliteStorage = class {
|
|
|
404
414
|
user_id TEXT,
|
|
405
415
|
feature TEXT,
|
|
406
416
|
app_id TEXT,
|
|
417
|
+
metadata TEXT,
|
|
407
418
|
timestamp TEXT NOT NULL
|
|
408
419
|
)
|
|
409
420
|
`);
|
|
@@ -423,13 +434,16 @@ var SqliteStorage = class {
|
|
|
423
434
|
if (!cols.includes("app_id")) {
|
|
424
435
|
this.db.exec(`ALTER TABLE usage ADD COLUMN app_id TEXT`);
|
|
425
436
|
}
|
|
437
|
+
if (!cols.includes("metadata")) {
|
|
438
|
+
this.db.exec(`ALTER TABLE usage ADD COLUMN metadata TEXT`);
|
|
439
|
+
}
|
|
426
440
|
}
|
|
427
441
|
record(entry) {
|
|
428
442
|
this.db.prepare(
|
|
429
443
|
`INSERT INTO usage
|
|
430
444
|
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
431
|
-
cost_usd, session_id, user_id, feature, app_id, timestamp)
|
|
432
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
445
|
+
cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
|
|
446
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
433
447
|
).run(
|
|
434
448
|
entry.model,
|
|
435
449
|
entry.inputTokens,
|
|
@@ -442,6 +456,7 @@ var SqliteStorage = class {
|
|
|
442
456
|
entry.userId ?? null,
|
|
443
457
|
entry.feature ?? null,
|
|
444
458
|
entry.appId ?? null,
|
|
459
|
+
entry.metadata != null ? JSON.stringify(entry.metadata) : null,
|
|
445
460
|
entry.timestamp
|
|
446
461
|
);
|
|
447
462
|
}
|
|
@@ -459,6 +474,7 @@ var SqliteStorage = class {
|
|
|
459
474
|
...r.user_id != null && { userId: r.user_id },
|
|
460
475
|
...r.feature != null && { feature: r.feature },
|
|
461
476
|
...r.app_id != null && { appId: r.app_id },
|
|
477
|
+
...r.metadata != null && { metadata: JSON.parse(r.metadata) },
|
|
462
478
|
timestamp: r.timestamp
|
|
463
479
|
}));
|
|
464
480
|
}
|
|
@@ -509,6 +525,41 @@ function calculateCost(inputTokens, outputTokens, price, cachedTokens = 0, cache
|
|
|
509
525
|
return regularInputCost + cachedReadCost + cacheCreationCost + outputCost;
|
|
510
526
|
}
|
|
511
527
|
|
|
528
|
+
// src/exporters/cloud.ts
|
|
529
|
+
var DEFAULT_ENDPOINT = "https://api.tokenwatch.dev/v1/ingest";
|
|
530
|
+
var CloudExporter = class {
|
|
531
|
+
constructor(apiKey, endpoint) {
|
|
532
|
+
this.apiKey = apiKey;
|
|
533
|
+
this.endpoint = endpoint ?? DEFAULT_ENDPOINT;
|
|
534
|
+
}
|
|
535
|
+
apiKey;
|
|
536
|
+
endpoint;
|
|
537
|
+
export(entry) {
|
|
538
|
+
fetch(this.endpoint, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: {
|
|
541
|
+
"Content-Type": "application/json",
|
|
542
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
543
|
+
},
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
model: entry.model,
|
|
546
|
+
inputTokens: entry.inputTokens,
|
|
547
|
+
outputTokens: entry.outputTokens,
|
|
548
|
+
reasoningTokens: entry.reasoningTokens ?? 0,
|
|
549
|
+
cachedTokens: entry.cachedTokens ?? 0,
|
|
550
|
+
cacheCreationTokens: entry.cacheCreationTokens ?? 0,
|
|
551
|
+
costUSD: entry.costUSD,
|
|
552
|
+
sessionId: entry.sessionId,
|
|
553
|
+
userId: entry.userId,
|
|
554
|
+
feature: entry.feature,
|
|
555
|
+
metadata: entry.metadata,
|
|
556
|
+
timestamp: entry.timestamp
|
|
557
|
+
})
|
|
558
|
+
}).catch(() => {
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
512
563
|
// src/core/suggestions.ts
|
|
513
564
|
var PROVIDER_PREFIXES = ["gpt-", "claude-", "gemini-", "deepseek-"];
|
|
514
565
|
function getProviderPrefix(model) {
|
|
@@ -1994,7 +2045,9 @@ var TrackerConfigSchema = z.object({
|
|
|
1994
2045
|
mode: z.enum(["once", "always"]).optional().default("once")
|
|
1995
2046
|
}).optional(),
|
|
1996
2047
|
exporter: z.custom((v) => v !== null && typeof v === "object" && typeof v.export === "function").optional(),
|
|
1997
|
-
appId: z.string().optional()
|
|
2048
|
+
appId: z.string().optional(),
|
|
2049
|
+
cloudApiKey: z.string().optional(),
|
|
2050
|
+
cloudEndpoint: z.string().url().optional()
|
|
1998
2051
|
});
|
|
1999
2052
|
function createTracker(config = {}) {
|
|
2000
2053
|
const parsed = TrackerConfigSchema.safeParse(config);
|
|
@@ -2014,9 +2067,12 @@ ${issues}`);
|
|
|
2014
2067
|
suggestions,
|
|
2015
2068
|
anomalyDetection,
|
|
2016
2069
|
exporter,
|
|
2017
|
-
appId
|
|
2070
|
+
appId,
|
|
2071
|
+
cloudApiKey,
|
|
2072
|
+
cloudEndpoint
|
|
2018
2073
|
} = parsed.data;
|
|
2019
2074
|
const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
|
|
2075
|
+
const cloudExporter = cloudApiKey ? new CloudExporter(cloudApiKey, cloudEndpoint) : null;
|
|
2020
2076
|
let remotePrices;
|
|
2021
2077
|
let pricesUpdatedAt = bundledUpdatedAt;
|
|
2022
2078
|
if (syncPrices) {
|
|
@@ -2077,6 +2133,9 @@ ${issues}`);
|
|
|
2077
2133
|
Promise.resolve(exporter.export(full)).catch(() => {
|
|
2078
2134
|
});
|
|
2079
2135
|
}
|
|
2136
|
+
if (cloudExporter) {
|
|
2137
|
+
cloudExporter.export(full);
|
|
2138
|
+
}
|
|
2080
2139
|
maybeFireAlerts(full);
|
|
2081
2140
|
if (anomalyDetection) maybeDetectAnomaly(full);
|
|
2082
2141
|
if (suggestions) {
|
|
@@ -2158,6 +2217,7 @@ ${issues}`);
|
|
|
2158
2217
|
const byUser = {};
|
|
2159
2218
|
const byFeature = {};
|
|
2160
2219
|
const byApp = {};
|
|
2220
|
+
const byMetadata = {};
|
|
2161
2221
|
let totalInput = 0;
|
|
2162
2222
|
let totalOutput = 0;
|
|
2163
2223
|
let totalCost = 0;
|
|
@@ -2199,6 +2259,14 @@ ${issues}`);
|
|
|
2199
2259
|
a.costUSD += e.costUSD;
|
|
2200
2260
|
a.calls += 1;
|
|
2201
2261
|
}
|
|
2262
|
+
if (e.metadata) {
|
|
2263
|
+
for (const [key, val] of Object.entries(e.metadata)) {
|
|
2264
|
+
const group = byMetadata[key] ??= {};
|
|
2265
|
+
const slot = group[val] ??= { costUSD: 0, calls: 0 };
|
|
2266
|
+
slot.costUSD += e.costUSD;
|
|
2267
|
+
slot.calls += 1;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2202
2270
|
}
|
|
2203
2271
|
if (options && entries.length > 0) {
|
|
2204
2272
|
periodFrom = entries[0]?.timestamp ?? periodFrom;
|
|
@@ -2211,6 +2279,7 @@ ${issues}`);
|
|
|
2211
2279
|
byUser,
|
|
2212
2280
|
byFeature,
|
|
2213
2281
|
byApp,
|
|
2282
|
+
byMetadata,
|
|
2214
2283
|
period: { from: periodFrom, to: lastTimestamp },
|
|
2215
2284
|
...pricesUpdatedAt ? { pricesUpdatedAt } : {}
|
|
2216
2285
|
};
|
|
@@ -2315,7 +2384,7 @@ ${issues}`);
|
|
|
2315
2384
|
}
|
|
2316
2385
|
async function exportCSV() {
|
|
2317
2386
|
const entries = await Promise.resolve(storage.getAll());
|
|
2318
|
-
const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,cachedTokens,cacheCreationTokens,costUSD,sessionId,userId,feature,appId";
|
|
2387
|
+
const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,cachedTokens,cacheCreationTokens,costUSD,sessionId,userId,feature,appId,metadata";
|
|
2319
2388
|
const rows = entries.map(
|
|
2320
2389
|
(e) => [
|
|
2321
2390
|
csvEscape(e.timestamp),
|
|
@@ -2329,7 +2398,8 @@ ${issues}`);
|
|
|
2329
2398
|
csvEscape(e.sessionId ?? ""),
|
|
2330
2399
|
csvEscape(e.userId ?? ""),
|
|
2331
2400
|
csvEscape(e.feature ?? ""),
|
|
2332
|
-
csvEscape(e.appId ?? "")
|
|
2401
|
+
csvEscape(e.appId ?? ""),
|
|
2402
|
+
csvEscape(e.metadata ? JSON.stringify(e.metadata) : "")
|
|
2333
2403
|
].join(",")
|
|
2334
2404
|
);
|
|
2335
2405
|
return [header, ...rows].join("\n");
|
|
@@ -2448,6 +2518,7 @@ async function getDashboardData(storage, filter) {
|
|
|
2448
2518
|
const byUser = {};
|
|
2449
2519
|
const byFeature = {};
|
|
2450
2520
|
const byApp = {};
|
|
2521
|
+
const byMetadata = {};
|
|
2451
2522
|
let totalInput = 0;
|
|
2452
2523
|
let totalOutput = 0;
|
|
2453
2524
|
let totalCost = 0;
|
|
@@ -2486,6 +2557,14 @@ async function getDashboardData(storage, filter) {
|
|
|
2486
2557
|
a.costUSD += e.costUSD;
|
|
2487
2558
|
a.calls += 1;
|
|
2488
2559
|
}
|
|
2560
|
+
if (e.metadata) {
|
|
2561
|
+
for (const [key, val] of Object.entries(e.metadata)) {
|
|
2562
|
+
const group = byMetadata[key] ??= {};
|
|
2563
|
+
const slot = group[val] ??= { costUSD: 0, calls: 0 };
|
|
2564
|
+
slot.costUSD += e.costUSD;
|
|
2565
|
+
slot.calls += 1;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2489
2568
|
}
|
|
2490
2569
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2491
2570
|
const periodFrom = entries[0]?.timestamp ?? now;
|
|
@@ -2498,6 +2577,7 @@ async function getDashboardData(storage, filter) {
|
|
|
2498
2577
|
byUser,
|
|
2499
2578
|
byFeature,
|
|
2500
2579
|
byApp,
|
|
2580
|
+
byMetadata,
|
|
2501
2581
|
period: { from: periodFrom, to: periodTo }
|
|
2502
2582
|
};
|
|
2503
2583
|
const forecastWindowMs = 24 * 60 * 60 * 1e3;
|
|
@@ -3473,7 +3553,65 @@ function ModelTable({ models, total, t, onRowClick, exampleHover }) {
|
|
|
3473
3553
|
</section>
|
|
3474
3554
|
);
|
|
3475
3555
|
}
|
|
3476
|
-
|
|
3556
|
+
function MetaGroup({ label, rows, t }) {
|
|
3557
|
+
const [open, setOpen] = useStateT(true);
|
|
3558
|
+
const { fmtUSD, fmtInt } = window.TW;
|
|
3559
|
+
const total = rows.reduce(function(s, r) { return s + r[1].costUSD; }, 0);
|
|
3560
|
+
return (
|
|
3561
|
+
<section className="tw-section">
|
|
3562
|
+
<div className="tw-sec-head" style={{ cursor: 'pointer' }} onClick={() => setOpen(function(v) { return !v; })}>
|
|
3563
|
+
<h3 style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
3564
|
+
<button className="tw-collapse" style={{ transform: open ? 'none' : 'rotate(-90deg)' }}><Ico.chevron /></button>
|
|
3565
|
+
{label}
|
|
3566
|
+
</h3>
|
|
3567
|
+
<span className="tw-sec-sub">{rows.length + ' values \xB7 ' + fmtUSD(total, 4)}</span>
|
|
3568
|
+
</div>
|
|
3569
|
+
{open && (
|
|
3570
|
+
<div className="tw-table-wrap">
|
|
3571
|
+
<table className={'tw-table' + (t.density === 'compact' ? ' compact' : '')}>
|
|
3572
|
+
<thead>
|
|
3573
|
+
<tr>
|
|
3574
|
+
<th style={{ textAlign: 'left' }}>Value</th>
|
|
3575
|
+
<th style={{ textAlign: 'right' }}>Cost</th>
|
|
3576
|
+
<th style={{ textAlign: 'right' }}>Calls</th>
|
|
3577
|
+
<th style={{ textAlign: 'right' }}>Avg / call</th>
|
|
3578
|
+
</tr>
|
|
3579
|
+
</thead>
|
|
3580
|
+
<tbody>
|
|
3581
|
+
{rows.map(function(r) {
|
|
3582
|
+
var val = r[0], stats = r[1];
|
|
3583
|
+
return (
|
|
3584
|
+
<tr key={val} className="tw-row">
|
|
3585
|
+
<td>{val}</td>
|
|
3586
|
+
<td style={{ textAlign: 'right' }}>{fmtUSD(stats.costUSD, 4)}</td>
|
|
3587
|
+
<td style={{ textAlign: 'right' }}>{fmtInt(stats.calls)}</td>
|
|
3588
|
+
<td style={{ textAlign: 'right' }}>{fmtUSD(stats.costUSD / Math.max(stats.calls, 1), 4)}</td>
|
|
3589
|
+
</tr>
|
|
3590
|
+
);
|
|
3591
|
+
})}
|
|
3592
|
+
</tbody>
|
|
3593
|
+
</table>
|
|
3594
|
+
</div>
|
|
3595
|
+
)}
|
|
3596
|
+
</section>
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
function MetadataSection({ byMetadata, t }) {
|
|
3601
|
+
var keys = Object.keys(byMetadata || {});
|
|
3602
|
+
if (keys.length === 0) return null;
|
|
3603
|
+
return (
|
|
3604
|
+
<div>
|
|
3605
|
+
{keys.map(function(key) {
|
|
3606
|
+
var group = byMetadata[key];
|
|
3607
|
+
var rows = Object.entries(group).sort(function(a, b) { return b[1].costUSD - a[1].costUSD; });
|
|
3608
|
+
return <MetaGroup key={key} label={key} rows={rows} t={t} />;
|
|
3609
|
+
})}
|
|
3610
|
+
</div>
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
Object.assign(window, { ModelTable, MetaGroup, MetadataSection });
|
|
3477
3615
|
</script>
|
|
3478
3616
|
|
|
3479
3617
|
<script type="text/babel">
|
|
@@ -3782,6 +3920,7 @@ Object.assign(window, {
|
|
|
3782
3920
|
BASE_MODELS, RANGES, modelsForRange, kpisForRange,
|
|
3783
3921
|
seriesForRange, seedFeed, makeCall, callsForModel,
|
|
3784
3922
|
FEATURES, FEATURE_COLOR, BUDGET, _buildSeries: buildSeries,
|
|
3923
|
+
byMetadata: {},
|
|
3785
3924
|
},
|
|
3786
3925
|
});
|
|
3787
3926
|
|
|
@@ -3826,6 +3965,7 @@ Object.assign(window, {
|
|
|
3826
3965
|
var elapsed = window.TW.BUDGET.cycleDays - window.TW.BUDGET.daysLeft;
|
|
3827
3966
|
window.TW.BUDGET.used = fc.burnRatePerHour * 24 * Math.max(elapsed, 1);
|
|
3828
3967
|
}
|
|
3968
|
+
if (r.byMetadata) window.TW.byMetadata = r.byMetadata;
|
|
3829
3969
|
window.dispatchEvent(new CustomEvent('tw-data-update'));
|
|
3830
3970
|
}
|
|
3831
3971
|
var evtSource = null;
|
|
@@ -3968,6 +4108,10 @@ function App() {
|
|
|
3968
4108
|
|
|
3969
4109
|
const models = kpis.models;
|
|
3970
4110
|
|
|
4111
|
+
const byMetadata = useMemoApp(() => {
|
|
4112
|
+
return window.TW.byMetadata || {};
|
|
4113
|
+
}, [_sseV]);
|
|
4114
|
+
|
|
3971
4115
|
useEffectApp(() => {
|
|
3972
4116
|
const onKey = (e) => {
|
|
3973
4117
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
@@ -4003,6 +4147,7 @@ function App() {
|
|
|
4003
4147
|
<ChartsRow range={range} models={models} kpis={kpis} series={series} t={t} />
|
|
4004
4148
|
<ModelTable models={models} total={kpis.cost} t={t} exampleHover={t.smartHighlight}
|
|
4005
4149
|
onRowClick={(m) => setSelModel({ ...m, share: m.cost / Math.max(kpis.cost, 0.0001) })} />
|
|
4150
|
+
<MetadataSection byMetadata={byMetadata} t={t} />
|
|
4006
4151
|
<ForecastSection t={t} />
|
|
4007
4152
|
<LiveActivity t={t} />
|
|
4008
4153
|
</main>
|