ondeckllm 1.3.0 → 1.4.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/package.json +11 -2
- package/src/cost-tracker.js +91 -68
- package/src/public/app.js +403 -100
- package/src/public/index.html +2 -2
- package/src/public/styles.css +39 -1
- package/src/server.js +138 -33
- package/src/storage.js +109 -147
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ondeckllm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Localhost dashboard for managing LLM providers, model routing, and batting-order fallback chains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
"start": "node src/server.js",
|
|
12
12
|
"dev": "node src/server.js"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"llm",
|
|
16
|
+
"ai",
|
|
17
|
+
"dashboard",
|
|
18
|
+
"model-routing",
|
|
19
|
+
"openai",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"ollama",
|
|
22
|
+
"ondeckllm"
|
|
23
|
+
],
|
|
15
24
|
"author": "Canonflip",
|
|
16
25
|
"license": "MIT",
|
|
17
26
|
"dependencies": {
|
package/src/cost-tracker.js
CHANGED
|
@@ -196,92 +196,115 @@ export function getUsageSummary(range = 'all') {
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
// ── OpenClaw Session Import ──
|
|
199
|
+
// ── OpenClaw Session Import (Continuous) ──
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const SYNC_STATE_FILE = join(DATA_DIR, 'openclaw-sync-state.json');
|
|
202
|
+
const OPENCLAW_AGENTS_DIR = join(homedir(), '.openclaw', 'agents');
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
function loadSyncState() {
|
|
205
|
+
ensureDataDir();
|
|
206
|
+
if (!existsSync(SYNC_STATE_FILE)) return { files: {} };
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(readFileSync(SYNC_STATE_FILE, 'utf-8'));
|
|
209
|
+
} catch { return { files: {} }; }
|
|
210
|
+
}
|
|
206
211
|
|
|
207
|
-
|
|
212
|
+
function saveSyncState(state) {
|
|
213
|
+
ensureDataDir();
|
|
214
|
+
writeFileSync(SYNC_STATE_FILE, JSON.stringify(state));
|
|
215
|
+
}
|
|
208
216
|
|
|
217
|
+
export async function importOpenClawSessions() {
|
|
218
|
+
ensureDataDir();
|
|
219
|
+
const state = loadSyncState();
|
|
209
220
|
let imported = 0;
|
|
221
|
+
let filesScanned = 0;
|
|
210
222
|
|
|
223
|
+
// Scan all agent session dirs
|
|
224
|
+
const agentDirs = [];
|
|
211
225
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
if (existsSync(OPENCLAW_AGENTS_DIR)) {
|
|
227
|
+
for (const agent of readdirSync(OPENCLAW_AGENTS_DIR)) {
|
|
228
|
+
const sessDir = join(OPENCLAW_AGENTS_DIR, agent, 'sessions');
|
|
229
|
+
if (existsSync(sessDir)) agentDirs.push(sessDir);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch { /* ignore */ }
|
|
233
|
+
|
|
234
|
+
// Also check legacy path
|
|
235
|
+
if (existsSync(OPENCLAW_SESSIONS_DIR)) agentDirs.push(OPENCLAW_SESSIONS_DIR);
|
|
214
236
|
|
|
215
|
-
|
|
237
|
+
for (const sessDir of agentDirs) {
|
|
238
|
+
let files;
|
|
239
|
+
try { files = readdirSync(sessDir).filter(f => f.endsWith('.jsonl')); } catch { continue; }
|
|
240
|
+
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
const filePath = join(sessDir, file);
|
|
243
|
+
filesScanned++;
|
|
244
|
+
|
|
245
|
+
// Get file size to detect changes
|
|
246
|
+
let fileSize;
|
|
247
|
+
try {
|
|
248
|
+
const { statSync } = await import('fs');
|
|
249
|
+
fileSize = statSync(filePath).size;
|
|
250
|
+
} catch { continue; }
|
|
251
|
+
|
|
252
|
+
const prevOffset = state.files[filePath]?.offset || 0;
|
|
253
|
+
if (fileSize <= prevOffset) continue; // No new data
|
|
254
|
+
|
|
255
|
+
// Read only new bytes
|
|
216
256
|
try {
|
|
217
|
-
const
|
|
218
|
-
const
|
|
257
|
+
const fd = (await import('fs')).openSync(filePath, 'r');
|
|
258
|
+
const buf = Buffer.alloc(fileSize - prevOffset);
|
|
259
|
+
(await import('fs')).readSync(fd, buf, 0, buf.length, prevOffset);
|
|
260
|
+
(await import('fs')).closeSync(fd);
|
|
261
|
+
|
|
262
|
+
const newData = buf.toString('utf-8');
|
|
263
|
+
const lines = newData.split('\n');
|
|
219
264
|
|
|
220
|
-
// Try JSONL first (one JSON per line)
|
|
221
|
-
const lines = raw.trim().split('\n');
|
|
222
265
|
for (const line of lines) {
|
|
266
|
+
if (!line.trim()) continue;
|
|
223
267
|
try {
|
|
224
268
|
const data = JSON.parse(line);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const msg of data.messages) {
|
|
255
|
-
if (msg.usage) {
|
|
256
|
-
const inputTokens = msg.usage.input_tokens || msg.usage.prompt_tokens || 0;
|
|
257
|
-
const outputTokens = msg.usage.output_tokens || msg.usage.completion_tokens || 0;
|
|
258
|
-
if (inputTokens > 0 || outputTokens > 0) {
|
|
259
|
-
const model = msg.model || data.model || 'unknown';
|
|
260
|
-
const provider = msg.provider || data.provider || guessProvider(model);
|
|
261
|
-
logUsage({
|
|
262
|
-
ts: msg.timestamp ? new Date(msg.timestamp).getTime() : Date.now(),
|
|
263
|
-
provider,
|
|
264
|
-
model,
|
|
265
|
-
inputTokens,
|
|
266
|
-
outputTokens,
|
|
267
|
-
cost: calculateCost(model, inputTokens, outputTokens),
|
|
268
|
-
latencyMs: msg.latencyMs || 0
|
|
269
|
-
});
|
|
270
|
-
imported++;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
269
|
+
// Only process assistant messages with usage data
|
|
270
|
+
if (data.type !== 'message') continue;
|
|
271
|
+
const msg = data.message;
|
|
272
|
+
if (!msg || msg.role !== 'assistant' || !msg.usage) continue;
|
|
273
|
+
|
|
274
|
+
const usage = msg.usage;
|
|
275
|
+
const inputTokens = usage.input || usage.input_tokens || 0;
|
|
276
|
+
const outputTokens = usage.output || usage.output_tokens || 0;
|
|
277
|
+
const cacheRead = usage.cacheRead || 0;
|
|
278
|
+
const cacheWrite = usage.cacheWrite || 0;
|
|
279
|
+
|
|
280
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) continue;
|
|
281
|
+
|
|
282
|
+
const model = msg.model || 'unknown';
|
|
283
|
+
const provider = msg.provider || guessProvider(model);
|
|
284
|
+
const cost = usage.cost?.total ?? calculateCost(model, inputTokens, outputTokens);
|
|
285
|
+
const ts = data.timestamp ? new Date(data.timestamp).getTime() : Date.now();
|
|
286
|
+
|
|
287
|
+
logUsage({
|
|
288
|
+
ts,
|
|
289
|
+
provider,
|
|
290
|
+
model,
|
|
291
|
+
inputTokens: inputTokens + cacheRead + cacheWrite,
|
|
292
|
+
outputTokens,
|
|
293
|
+
cost,
|
|
294
|
+
latencyMs: 0,
|
|
295
|
+
source: 'openclaw'
|
|
296
|
+
});
|
|
297
|
+
imported++;
|
|
275
298
|
} catch { /* skip unparseable lines */ }
|
|
276
299
|
}
|
|
300
|
+
|
|
301
|
+
state.files[filePath] = { offset: fileSize, lastSync: Date.now() };
|
|
277
302
|
} catch { /* skip unreadable files */ }
|
|
278
303
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Mark as imported
|
|
282
|
-
writeFileSync(IMPORT_MARKER, new Date().toISOString());
|
|
304
|
+
}
|
|
283
305
|
|
|
284
|
-
|
|
306
|
+
saveSyncState(state);
|
|
307
|
+
return { imported, filesScanned, totalTracked: Object.keys(state.files).length };
|
|
285
308
|
}
|
|
286
309
|
|
|
287
310
|
function guessProvider(model) {
|