dsh-plugin-usage 0.1.2 → 0.1.4
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/lib/host/index.js +68 -57
- package/package.json +1 -1
package/lib/host/index.js
CHANGED
|
@@ -355,69 +355,80 @@ export function apply(ctx) {
|
|
|
355
355
|
// browser half calls `connection.rpc.call('/usage', endpoint, args)`);
|
|
356
356
|
// dynamic-plugin runs expose `harness` directly. Support both from one file.
|
|
357
357
|
const usageHandlers = new Map();
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
},
|
|
375
|
-
};
|
|
376
|
-
})();
|
|
377
|
-
if (usageHarness !== null) {
|
|
378
|
-
usageHarness.handle(RPC_QUERY, (args) => query(args || {}));
|
|
379
|
-
usageHarness.handle(RPC_PRICES, (args) => {
|
|
380
|
-
const incoming = (args && args.prices) || null;
|
|
381
|
-
if (incoming && typeof incoming === 'object') {
|
|
382
|
-
for (const [model, p] of Object.entries(incoming)) {
|
|
383
|
-
if (p && typeof p === 'object') {
|
|
384
|
-
prices[model] = {
|
|
385
|
-
inputPerM: Number(p.inputPerM) || 0,
|
|
386
|
-
outputPerM: Number(p.outputPerM) || 0,
|
|
387
|
-
cacheReadPerM: Number(p.cacheReadPerM) || 0,
|
|
388
|
-
cacheWritePerM: Number(p.cacheWritePerM) || 0,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
358
|
+
function usageHandle(method, fn) {
|
|
359
|
+
const name = String(method);
|
|
360
|
+
usageHandlers.set(name.indexOf('/') === -1 ? name : name.slice(name.indexOf('/') + 1), fn);
|
|
361
|
+
}
|
|
362
|
+
usageHandle(RPC_QUERY, (args) => query(args || {}));
|
|
363
|
+
usageHandle(RPC_PRICES, (args) => {
|
|
364
|
+
const incoming = (args && args.prices) || null;
|
|
365
|
+
if (incoming && typeof incoming === 'object') {
|
|
366
|
+
for (const [model, p] of Object.entries(incoming)) {
|
|
367
|
+
if (p && typeof p === 'object') {
|
|
368
|
+
prices[model] = {
|
|
369
|
+
inputPerM: Number(p.inputPerM) || 0,
|
|
370
|
+
outputPerM: Number(p.outputPerM) || 0,
|
|
371
|
+
cacheReadPerM: Number(p.cacheReadPerM) || 0,
|
|
372
|
+
cacheWritePerM: Number(p.cacheWritePerM) || 0,
|
|
373
|
+
};
|
|
391
374
|
}
|
|
392
375
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
376
|
+
}
|
|
377
|
+
if (args && typeof args.currency === 'string')
|
|
378
|
+
currency = args.currency;
|
|
379
|
+
return { prices: Object.assign({}, prices), currency };
|
|
380
|
+
});
|
|
381
|
+
// ------------------------------------------------- client source loader
|
|
382
|
+
// The full dashboard UI lives in src/client/index.js (single source of
|
|
383
|
+
// truth). A dynamic plugin cannot import files, so the host reads the file
|
|
384
|
+
// and serves it to the browser half, which evaluates it with new Function.
|
|
385
|
+
const CLIENT_SOURCE_CANDIDATES = [
|
|
386
|
+
'/Users/ruanbw/projects/usage/src/client/index.js',
|
|
387
|
+
'/Users/ruanbw/projects/usage/lib/client/index.js',
|
|
388
|
+
];
|
|
389
|
+
usageHandle('usage/client-source', async () => {
|
|
390
|
+
const fs = ctx.get('fs');
|
|
391
|
+
if (fs === undefined)
|
|
392
|
+
return { ok: false, error: 'fs service unavailable' };
|
|
393
|
+
for (const p of CLIENT_SOURCE_CANDIDATES) {
|
|
394
|
+
try {
|
|
395
|
+
const target = await fs.resolve(p);
|
|
396
|
+
const body = await fs.readText(target);
|
|
397
|
+
if (typeof body === 'string' && body.length > 0)
|
|
398
|
+
return { ok: true, body };
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
// try the next candidate
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { ok: false, error: 'client source not found (tried: ' + CLIENT_SOURCE_CANDIDATES.join(', ') + ')' };
|
|
405
|
+
});
|
|
406
|
+
const dynamicHarness = (typeof harness !== 'undefined' && harness && typeof harness.handle === 'function') ? harness : null;
|
|
407
|
+
if (dynamicHarness !== null) {
|
|
408
|
+
for (const [endpoint, fn] of usageHandlers) {
|
|
409
|
+
dynamicHarness.handle('usage/' + endpoint, fn);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// Static (bundle) installs: the `connection` service is provided
|
|
414
|
+
// asynchronously by the client-connection plugin, so wait for it before
|
|
415
|
+
// registering the /usage channel (the official api-gateway pattern).
|
|
416
|
+
ctx.inject(['connection'], (connectionCtx) => {
|
|
417
|
+
const connection = connectionCtx.connection;
|
|
418
|
+
if (connection === undefined || connection.rpc === undefined)
|
|
419
|
+
return;
|
|
420
|
+
connection.rpc.handle('/usage', async (endpoint, args) => {
|
|
421
|
+
const fn = usageHandlers.get(endpoint);
|
|
422
|
+
if (fn === undefined) {
|
|
423
|
+
return { ok: false, error: { code: 'bad-request', message: `unknown endpoint: ${endpoint}`, details: { issues: [] } } };
|
|
424
|
+
}
|
|
410
425
|
try {
|
|
411
|
-
|
|
412
|
-
const body = await fs.readText(target);
|
|
413
|
-
if (typeof body === 'string' && body.length > 0)
|
|
414
|
-
return { ok: true, body };
|
|
426
|
+
return { ok: true, value: await fn(args) };
|
|
415
427
|
}
|
|
416
428
|
catch (error) {
|
|
417
|
-
|
|
429
|
+
return { ok: false, error: { code: 'internal', message: String((error && error.message) || error), details: {} } };
|
|
418
430
|
}
|
|
419
|
-
}
|
|
420
|
-
return { ok: false, error: 'client source not found (tried: ' + CLIENT_SOURCE_CANDIDATES.join(', ') + ')' };
|
|
431
|
+
}, { authority: 'loopback' });
|
|
421
432
|
});
|
|
422
433
|
}
|
|
423
434
|
// ---------------------------------------------------------------- startup
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-usage",
|
|
3
3
|
"description": "Token usage statistics for DeepSeek Harness — daily/custom-range views, model filters, call counts and cost, inspired by CC Switch statistics.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|