token-studio 4.8.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/.nvmrc +1 -0
- package/CHANGELOG.md +89 -0
- package/Dockerfile +17 -0
- package/LICENSE +22 -0
- package/NOTICE.md +21 -0
- package/PRIVACY.md +68 -0
- package/README.en.md +220 -0
- package/README.md +220 -0
- package/config/collectors.json +54 -0
- package/data/.gitkeep +1 -0
- package/docker-compose.yml +17 -0
- package/docs/assets/.gitkeep +1 -0
- package/docs/assets/token-studio-v44-dashboard.png +0 -0
- package/docs/assets/token-studio-v44-live.png +0 -0
- package/docs/assets/token-studio-v44-review-mobile.png +0 -0
- package/docs/assets/token-studio-v44-review.png +0 -0
- package/docs/assets/token-studio-v45-dashboard.png +0 -0
- package/docs/assets/token-studio-v45-live.png +0 -0
- package/docs/assets/token-studio-v45-review-mobile.png +0 -0
- package/docs/assets/token-studio-v45-review.png +0 -0
- package/docs/blog-case-study.md +34 -0
- package/docs/collector-support-matrix.md +65 -0
- package/docs/competitive-notes.md +87 -0
- package/docs/demo-data/README.md +12 -0
- package/docs/demo-data/token-studio-v2-demo.json +146 -0
- package/docs/demo-flow.md +39 -0
- package/docs/first-run.md +95 -0
- package/docs/local-collectors.md +49 -0
- package/docs/public-launch-checklist.md +45 -0
- package/docs/resume-bullets.md +7 -0
- package/docs/statusline.md +52 -0
- package/index.html +16 -0
- package/package.json +36 -0
- package/render.yaml +17 -0
- package/src/auto-attribution.mjs +396 -0
- package/src/ccusage-bridge.mjs +74 -0
- package/src/ccusage-import.mjs +415 -0
- package/src/cli.mjs +643 -0
- package/src/client/dashboard/App.jsx +1734 -0
- package/src/client/dashboard/annotation-presets.js +138 -0
- package/src/client/dashboard/attribution.js +328 -0
- package/src/client/dashboard/components-charts.jsx +622 -0
- package/src/client/dashboard/components-tables.jsx +1531 -0
- package/src/client/dashboard/components-top.jsx +307 -0
- package/src/client/dashboard/import-budget.js +41 -0
- package/src/client/dashboard/model-usage.js +108 -0
- package/src/client/dashboard/onboarding.js +80 -0
- package/src/client/dashboard/styles.css +2606 -0
- package/src/client/live/LiveApp.jsx +226 -0
- package/src/client/live/styles.css +446 -0
- package/src/client/main.jsx +20 -0
- package/src/client/review/ReviewApp.jsx +507 -0
- package/src/client/review/closure-progress.js +165 -0
- package/src/client/review/markdown-report.js +401 -0
- package/src/client/review/model-strategy.js +273 -0
- package/src/client/review/roi-advisor.js +255 -0
- package/src/client/review/roi-evidence.js +78 -0
- package/src/client/review/savings-simulator.js +252 -0
- package/src/client/review/sections-1.jsx +277 -0
- package/src/client/review/sections-2.jsx +927 -0
- package/src/client/review/styles.css +2321 -0
- package/src/client/review/utils.js +345 -0
- package/src/client/shared/utils.js +236 -0
- package/src/closure-check.mjs +537 -0
- package/src/closure-import.mjs +646 -0
- package/src/collect.mjs +247 -0
- package/src/collector-config.mjs +82 -0
- package/src/collector-registry.mjs +333 -0
- package/src/collectors/claude-code.mjs +355 -0
- package/src/collectors/codex.mjs +418 -0
- package/src/collectors/copilot.mjs +19 -0
- package/src/collectors/cursor.mjs +23 -0
- package/src/collectors/gemini.mjs +530 -0
- package/src/collectors/goose.mjs +15 -0
- package/src/collectors/hermes.mjs +206 -0
- package/src/collectors/kimi.mjs +15 -0
- package/src/collectors/openclaw.mjs +400 -0
- package/src/collectors/opencode.mjs +349 -0
- package/src/collectors/qwen.mjs +15 -0
- package/src/collectors/structured-usage.mjs +437 -0
- package/src/collectors/utils.mjs +93 -0
- package/src/db.mjs +1397 -0
- package/src/demo-seed.mjs +39 -0
- package/src/dev.mjs +43 -0
- package/src/live.mjs +428 -0
- package/src/model-policy.mjs +147 -0
- package/src/pricing.mjs +434 -0
- package/src/privacy-check.mjs +126 -0
- package/src/server.mjs +1240 -0
- package/src/source-health.mjs +195 -0
- package/src/statusline.mjs +156 -0
- package/src/terminal-report.mjs +245 -0
- package/src/update-pricing.mjs +8 -0
- package/test/annotation-presets.test.mjs +137 -0
- package/test/api-annotations.test.mjs +202 -0
- package/test/api-auto-attribution.test.mjs +169 -0
- package/test/api-source-health.test.mjs +109 -0
- package/test/api-v2.test.mjs +278 -0
- package/test/api-v43.test.mjs +151 -0
- package/test/api-v44.test.mjs +128 -0
- package/test/attribution-summary.test.mjs +164 -0
- package/test/auto-attribution.test.mjs +116 -0
- package/test/ccusage-bridge.test.mjs +36 -0
- package/test/ccusage-import.test.mjs +93 -0
- package/test/cli-v43.test.mjs +64 -0
- package/test/cli-v45.test.mjs +34 -0
- package/test/cli-v46.test.mjs +129 -0
- package/test/cli-v47.test.mjs +98 -0
- package/test/closure-check.test.mjs +202 -0
- package/test/closure-import.test.mjs +263 -0
- package/test/collector-config.test.mjs +25 -0
- package/test/collector-registry.test.mjs +56 -0
- package/test/csv.test.mjs +19 -0
- package/test/db-annotations.test.mjs +186 -0
- package/test/db-v2.test.mjs +200 -0
- package/test/db-v4.test.mjs +178 -0
- package/test/experimental-collectors.test.mjs +103 -0
- package/test/fixtures/collectors/copilot/usage.jsonl +2 -0
- package/test/fixtures/collectors/cursor/usage.jsonl +2 -0
- package/test/fixtures/collectors/goose/usage.jsonl +2 -0
- package/test/fixtures/collectors/kimi/usage.jsonl +2 -0
- package/test/fixtures/collectors/qwen/usage.jsonl +2 -0
- package/test/import-budget.test.mjs +40 -0
- package/test/live.test.mjs +256 -0
- package/test/markdown-report.test.mjs +193 -0
- package/test/model-policy.test.mjs +34 -0
- package/test/model-strategy.test.mjs +116 -0
- package/test/model-usage.test.mjs +99 -0
- package/test/official-pricing.test.mjs +70 -0
- package/test/onboarding.test.mjs +55 -0
- package/test/privacy-check.test.mjs +33 -0
- package/test/review-closure-progress.test.mjs +99 -0
- package/test/roi-advisor.test.mjs +188 -0
- package/test/roi-evidence.test.mjs +48 -0
- package/test/roi-summary.test.mjs +101 -0
- package/test/savings-simulator.test.mjs +141 -0
- package/test/source-health.test.mjs +62 -0
- package/test/statusline.test.mjs +148 -0
- package/vite.config.js +23 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode data collector (pure JS).
|
|
3
|
+
*
|
|
4
|
+
* Reads the local OpenCode usage stores:
|
|
5
|
+
* ~/.local/share/opencode/opencode*.db — OpenCode 1.2+ SQLite
|
|
6
|
+
* ~/.local/share/opencode/storage/message/.../*.json — legacy JSON messages
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { basename, extname, join } from 'node:path';
|
|
13
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { configuredPath, configuredPaths, expandPath } from '../collector-config.mjs';
|
|
15
|
+
import { calculateCost } from '../pricing.mjs';
|
|
16
|
+
import { canonicalProvider, inferProviderFromModel, localDateFromTimestamp, normalizeModelForGrouping } from './utils.mjs';
|
|
17
|
+
|
|
18
|
+
export const CLIENT_KEY = 'opencode';
|
|
19
|
+
export const SOURCE_LABEL = 'OpenCode';
|
|
20
|
+
|
|
21
|
+
function opencodeDataDir() {
|
|
22
|
+
return configuredPath(
|
|
23
|
+
'opencode',
|
|
24
|
+
'dataDir',
|
|
25
|
+
`${homedir()}/.local/share/opencode`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function legacyMessageDir() {
|
|
30
|
+
const dataDir = opencodeDataDir();
|
|
31
|
+
if (!dataDir) return null;
|
|
32
|
+
return join(dataDir, 'storage', 'message');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isOpenCodeDbFilename(name) {
|
|
36
|
+
if (extname(name) !== '.db') return false;
|
|
37
|
+
const stem = basename(name, '.db');
|
|
38
|
+
if (stem === 'opencode') return true;
|
|
39
|
+
const channel = stem.startsWith('opencode-') ? stem.slice('opencode-'.length) : '';
|
|
40
|
+
return channel.length > 0 && /^[A-Za-z0-9._-]+$/.test(channel);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function discoverDbPaths() {
|
|
44
|
+
const dataDir = opencodeDataDir();
|
|
45
|
+
const paths = [];
|
|
46
|
+
for (const entry of await safeReaddir(dataDir)) {
|
|
47
|
+
if (entry.isFile() && isOpenCodeDbFilename(entry.name)) {
|
|
48
|
+
paths.push(join(dataDir, entry.name));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const explicit = String(process.env.OPENCODE_DB || '').trim();
|
|
53
|
+
const explicitPath = expandPath(explicit);
|
|
54
|
+
if (explicitPath && existsSync(explicitPath) && isOpenCodeDbFilename(basename(explicitPath))) {
|
|
55
|
+
paths.push(explicitPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const extraPath of configuredPaths('opencode', 'extraDbPaths')) {
|
|
59
|
+
if (existsSync(extraPath) && isOpenCodeDbFilename(basename(extraPath))) {
|
|
60
|
+
paths.push(extraPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [...new Set(paths)].sort();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function collectJsonFiles(dir) {
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const entry of await safeReaddir(dir)) {
|
|
70
|
+
const full = join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
results.push(...await collectJsonFiles(full));
|
|
73
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
74
|
+
results.push(full);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function safeReaddir(dir) {
|
|
81
|
+
try {
|
|
82
|
+
return await readdir(dir, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function safeReadJson(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pos(v) {
|
|
97
|
+
const n = Number(v ?? 0);
|
|
98
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function posFloat(v) {
|
|
102
|
+
const n = Number(v ?? 0);
|
|
103
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function zero() {
|
|
107
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function addInto(agg, t) {
|
|
111
|
+
agg.input += t.input;
|
|
112
|
+
agg.output += t.output;
|
|
113
|
+
agg.cacheRead += t.cacheRead;
|
|
114
|
+
agg.cacheWrite += t.cacheWrite;
|
|
115
|
+
agg.reasoning += t.reasoning;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function workspaceLabel(raw) {
|
|
119
|
+
if (!raw) return null;
|
|
120
|
+
const normalized = String(raw).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
121
|
+
return normalized.split('/').filter(Boolean).pop() || raw;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tokensFromMessage(msg) {
|
|
125
|
+
const tokens = msg?.tokens;
|
|
126
|
+
if (!tokens) return null;
|
|
127
|
+
return {
|
|
128
|
+
input: pos(tokens.input),
|
|
129
|
+
output: pos(tokens.output),
|
|
130
|
+
cacheRead: pos(tokens.cache?.read),
|
|
131
|
+
cacheWrite: pos(tokens.cache?.write),
|
|
132
|
+
reasoning: pos(tokens.reasoning)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseMessageObject(msg, fallbackId, fallbackSessionId, fallbackWorkspace) {
|
|
137
|
+
if (!msg || msg.role !== 'assistant') return null;
|
|
138
|
+
|
|
139
|
+
const tokens = tokensFromMessage(msg);
|
|
140
|
+
if (!tokens) return null;
|
|
141
|
+
|
|
142
|
+
const model = typeof msg.modelID === 'string' && msg.modelID.trim()
|
|
143
|
+
? normalizeModelForGrouping(msg.modelID)
|
|
144
|
+
: null;
|
|
145
|
+
if (!model) return null;
|
|
146
|
+
|
|
147
|
+
const provider = canonicalProvider(msg.providerID) || inferProviderFromModel(model) || 'unknown';
|
|
148
|
+
const workspace = msg.path?.root || fallbackWorkspace || null;
|
|
149
|
+
const timestamp = Number(msg.time?.created || 0);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
client: CLIENT_KEY,
|
|
153
|
+
sessionId: msg.sessionID || fallbackSessionId || 'unknown',
|
|
154
|
+
dedupKey: msg.id || fallbackId || null,
|
|
155
|
+
fingerprint: fingerprintFor(msg, tokens, model, provider),
|
|
156
|
+
date: localDateFromTimestamp(timestamp, 'unknown'),
|
|
157
|
+
model,
|
|
158
|
+
provider,
|
|
159
|
+
workspace,
|
|
160
|
+
workspaceLabel: workspaceLabel(workspace),
|
|
161
|
+
tokens,
|
|
162
|
+
cost: posFloat(msg.cost),
|
|
163
|
+
agent: msg.mode || msg.agent || null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function fingerprintFor(msg, tokens, model, provider) {
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
created: msg.time?.created ?? null,
|
|
170
|
+
completed: msg.time?.completed ?? null,
|
|
171
|
+
model,
|
|
172
|
+
provider,
|
|
173
|
+
input: tokens.input,
|
|
174
|
+
output: tokens.output,
|
|
175
|
+
reasoning: tokens.reasoning,
|
|
176
|
+
cacheRead: tokens.cacheRead,
|
|
177
|
+
cacheWrite: tokens.cacheWrite,
|
|
178
|
+
cost: posFloat(msg.cost),
|
|
179
|
+
agent: msg.mode || msg.agent || null
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseDbRows(dbPath) {
|
|
184
|
+
let db;
|
|
185
|
+
try {
|
|
186
|
+
db = new DatabaseSync(dbPath);
|
|
187
|
+
} catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let rows;
|
|
192
|
+
try {
|
|
193
|
+
rows = db.prepare(`
|
|
194
|
+
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root
|
|
195
|
+
FROM message m
|
|
196
|
+
LEFT JOIN session s ON s.id = m.session_id
|
|
197
|
+
WHERE json_extract(m.data, '$.role') = 'assistant'
|
|
198
|
+
AND json_extract(m.data, '$.tokens') IS NOT NULL
|
|
199
|
+
ORDER BY m.id, m.session_id
|
|
200
|
+
`).all();
|
|
201
|
+
} catch {
|
|
202
|
+
try {
|
|
203
|
+
rows = db.prepare(`
|
|
204
|
+
SELECT m.id, m.session_id, m.data, NULL AS workspace_root
|
|
205
|
+
FROM message m
|
|
206
|
+
WHERE json_extract(m.data, '$.role') = 'assistant'
|
|
207
|
+
AND json_extract(m.data, '$.tokens') IS NOT NULL
|
|
208
|
+
ORDER BY m.id, m.session_id
|
|
209
|
+
`).all();
|
|
210
|
+
} catch {
|
|
211
|
+
try { db.close(); } catch { /* ignore */ }
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try { db.close(); } catch { /* ignore */ }
|
|
217
|
+
|
|
218
|
+
const messages = [];
|
|
219
|
+
const fingerprintIndices = new Map();
|
|
220
|
+
|
|
221
|
+
for (const row of rows) {
|
|
222
|
+
let msg;
|
|
223
|
+
try {
|
|
224
|
+
msg = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
|
|
225
|
+
} catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const parsed = parseMessageObject(msg, row.id, row.session_id, row.workspace_root);
|
|
230
|
+
if (!parsed) continue;
|
|
231
|
+
|
|
232
|
+
const existingIndex = fingerprintIndices.get(parsed.fingerprint);
|
|
233
|
+
if (existingIndex != null) {
|
|
234
|
+
const existing = messages[existingIndex];
|
|
235
|
+
if (!existing.dedupKey && parsed.dedupKey) existing.dedupKey = parsed.dedupKey;
|
|
236
|
+
if (!existing.workspace && parsed.workspace) {
|
|
237
|
+
existing.workspace = parsed.workspace;
|
|
238
|
+
existing.workspaceLabel = parsed.workspaceLabel;
|
|
239
|
+
} else if (existing.workspace && parsed.workspace && existing.workspace !== parsed.workspace) {
|
|
240
|
+
existing.workspace = null;
|
|
241
|
+
existing.workspaceLabel = null;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fingerprintIndices.set(parsed.fingerprint, messages.length);
|
|
247
|
+
messages.push(parsed);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return messages;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function parseLegacyJsonFile(filePath) {
|
|
254
|
+
const msg = await safeReadJson(filePath);
|
|
255
|
+
const fallbackId = basename(filePath, '.json');
|
|
256
|
+
return parseMessageObject(msg, fallbackId, msg?.sessionID, msg?.path?.root);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function collect(pricingData = null) {
|
|
260
|
+
const dailyMap = new Map();
|
|
261
|
+
const wmMap = new Map();
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
|
|
264
|
+
const addMessage = (message) => {
|
|
265
|
+
if (!message) return;
|
|
266
|
+
const dedupKey = message.dedupKey || message.fingerprint;
|
|
267
|
+
if (dedupKey && seen.has(dedupKey)) return;
|
|
268
|
+
if (dedupKey) seen.add(dedupKey);
|
|
269
|
+
|
|
270
|
+
const calculatedCost = calculateCost(message.model, message.tokens, pricingData, message.provider);
|
|
271
|
+
const cost = calculatedCost;
|
|
272
|
+
|
|
273
|
+
const dk = `${message.date}::${message.model}`;
|
|
274
|
+
if (!dailyMap.has(dk)) {
|
|
275
|
+
dailyMap.set(dk, { date: message.date, model: message.model, provider: message.provider, ...zero(), cost: 0 });
|
|
276
|
+
}
|
|
277
|
+
const day = dailyMap.get(dk);
|
|
278
|
+
addInto(day, message.tokens);
|
|
279
|
+
day.cost += cost;
|
|
280
|
+
|
|
281
|
+
const workspaceKey = message.workspace || message.sessionId || 'unknown';
|
|
282
|
+
const wmk = `${workspaceKey}::${message.model}`;
|
|
283
|
+
if (!wmMap.has(wmk)) {
|
|
284
|
+
wmMap.set(wmk, {
|
|
285
|
+
workspace: workspaceKey,
|
|
286
|
+
workspaceLabel: message.workspaceLabel || workspaceKey,
|
|
287
|
+
model: message.model,
|
|
288
|
+
provider: message.provider,
|
|
289
|
+
...zero(),
|
|
290
|
+
cost: 0
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const wm = wmMap.get(wmk);
|
|
294
|
+
addInto(wm, message.tokens);
|
|
295
|
+
wm.cost += cost;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
for (const dbPath of await discoverDbPaths()) {
|
|
299
|
+
for (const message of parseDbRows(dbPath)) addMessage(message);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const jsonPath of await collectJsonFiles(legacyMessageDir())) {
|
|
303
|
+
addMessage(await parseLegacyJsonFile(jsonPath));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return buildOutput(dailyMap, wmMap);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildOutput(dailyMap, wmMap) {
|
|
310
|
+
const byDate = new Map();
|
|
311
|
+
for (const row of dailyMap.values()) {
|
|
312
|
+
if (!byDate.has(row.date)) byDate.set(row.date, []);
|
|
313
|
+
byDate.get(row.date).push(row);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const contributions = [...byDate.entries()]
|
|
317
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
318
|
+
.map(([date, rows]) => ({
|
|
319
|
+
date,
|
|
320
|
+
clients: rows.map(row => ({
|
|
321
|
+
client: CLIENT_KEY,
|
|
322
|
+
modelId: row.model,
|
|
323
|
+
tokens: {
|
|
324
|
+
input: row.input,
|
|
325
|
+
output: row.output,
|
|
326
|
+
cacheRead: row.cacheRead,
|
|
327
|
+
cacheWrite: row.cacheWrite,
|
|
328
|
+
reasoning: row.reasoning
|
|
329
|
+
},
|
|
330
|
+
cost: row.cost
|
|
331
|
+
}))
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
const entries = [...wmMap.values()].map(wm => ({
|
|
335
|
+
client: CLIENT_KEY,
|
|
336
|
+
workspaceKey: wm.workspace,
|
|
337
|
+
workspaceLabel: wm.workspaceLabel,
|
|
338
|
+
model: wm.model,
|
|
339
|
+
provider: wm.provider,
|
|
340
|
+
input: wm.input,
|
|
341
|
+
output: wm.output,
|
|
342
|
+
cacheRead: wm.cacheRead,
|
|
343
|
+
cacheWrite: wm.cacheWrite,
|
|
344
|
+
reasoning: wm.reasoning,
|
|
345
|
+
cost: wm.cost
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
return { graphJson: { contributions }, modelsJson: { entries } };
|
|
349
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { configuredPaths, expandPath } from '../collector-config.mjs';
|
|
2
|
+
import { collectStructuredUsage } from './structured-usage.mjs';
|
|
3
|
+
|
|
4
|
+
export const CLIENT_KEY = 'qwen';
|
|
5
|
+
export const SOURCE_LABEL = 'Qwen Code';
|
|
6
|
+
|
|
7
|
+
export function roots() {
|
|
8
|
+
return configuredPaths('qwen', 'roots', ['~/.qwen', '~/.qwen-code'])
|
|
9
|
+
.map(expandPath)
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function collect(pricingData = null) {
|
|
14
|
+
return collectStructuredUsage({ clientKey: CLIENT_KEY, roots: roots(), pricingData });
|
|
15
|
+
}
|