@diogonzafe/tokenwatch 0.4.0 → 0.6.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/README.md +132 -6
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +848 -8
- package/dist/cli.js.map +1 -1
- package/dist/exporters.cjs +76 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +60 -0
- package/dist/exporters.d.ts +60 -0
- package/dist/exporters.js +56 -0
- package/dist/exporters.js.map +1 -0
- package/dist/{index-CJKk1hHw.d.cts → index-D9xq0RNg.d.cts} +19 -1
- package/dist/{index-CJKk1hHw.d.ts → index-D9xq0RNg.d.ts} +19 -1
- package/dist/index.cjs +63 -3
- 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 +63 -3
- package/dist/index.js.map +1 -1
- package/dist/langchain.d.cts +1 -1
- package/dist/langchain.d.ts +1 -1
- package/package.json +13 -3
- package/prices.json +1 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/exporters/index.ts
|
|
21
|
+
var exporters_exports = {};
|
|
22
|
+
__export(exporters_exports, {
|
|
23
|
+
OTelExporter: () => OTelExporter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(exporters_exports);
|
|
26
|
+
|
|
27
|
+
// src/exporters/otel.ts
|
|
28
|
+
var OTelExporter = class {
|
|
29
|
+
calls;
|
|
30
|
+
inputTokens;
|
|
31
|
+
outputTokens;
|
|
32
|
+
costUsd;
|
|
33
|
+
constructor(options = {}, _metricsApi) {
|
|
34
|
+
let metricsApi;
|
|
35
|
+
if (_metricsApi) {
|
|
36
|
+
metricsApi = _metricsApi;
|
|
37
|
+
} else {
|
|
38
|
+
try {
|
|
39
|
+
metricsApi = require("@opentelemetry/api").metrics;
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"[tokenwatch] OTelExporter requires @opentelemetry/api. Run: npm install @opentelemetry/api"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const meter = metricsApi.getMeter(options.meterName ?? "tokenwatch");
|
|
47
|
+
this.calls = meter.createCounter("tokenwatch.calls", {
|
|
48
|
+
description: "Number of LLM API calls tracked"
|
|
49
|
+
});
|
|
50
|
+
this.inputTokens = meter.createCounter("tokenwatch.input_tokens", {
|
|
51
|
+
description: "Input tokens consumed (includes cached and cache-creation tokens)"
|
|
52
|
+
});
|
|
53
|
+
this.outputTokens = meter.createCounter("tokenwatch.output_tokens", {
|
|
54
|
+
description: "Output tokens generated"
|
|
55
|
+
});
|
|
56
|
+
this.costUsd = meter.createHistogram("tokenwatch.cost_usd", {
|
|
57
|
+
description: "Cost per LLM API call in USD",
|
|
58
|
+
unit: "USD"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export(entry) {
|
|
62
|
+
const attrs = { model: entry.model };
|
|
63
|
+
if (entry.sessionId !== void 0) attrs["session.id"] = entry.sessionId;
|
|
64
|
+
if (entry.userId !== void 0) attrs["user.id"] = entry.userId;
|
|
65
|
+
if (entry.feature !== void 0) attrs["feature"] = entry.feature;
|
|
66
|
+
this.calls.add(1, attrs);
|
|
67
|
+
this.inputTokens.add(entry.inputTokens + (entry.cachedTokens ?? 0) + (entry.cacheCreationTokens ?? 0), attrs);
|
|
68
|
+
this.outputTokens.add(entry.outputTokens, attrs);
|
|
69
|
+
this.costUsd.record(entry.costUSD, attrs);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
73
|
+
0 && (module.exports = {
|
|
74
|
+
OTelExporter
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=exporters.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/exporters/index.ts","../src/exporters/otel.ts"],"sourcesContent":["export { OTelExporter } from './otel.js'\nexport type { OTelExporterOptions } from './otel.js'\n","import type { UsageEntry, IExporter } from '../types/index.js'\n\n// ─── Local type stubs ─────────────────────────────────────────────────────────\n// Mirror the minimal shape of @opentelemetry/api so this file compiles without\n// a hard compile-time dependency on the package.\n\ninterface OTelAttributes {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface OTelCounter {\n add(value: number, attributes?: OTelAttributes): void\n}\n\ninterface OTelHistogram {\n record(value: number, attributes?: OTelAttributes): void\n}\n\ninterface OTelMeter {\n createCounter(name: string, options?: { description?: string; unit?: string }): OTelCounter\n createHistogram(name: string, options?: { description?: string; unit?: string }): OTelHistogram\n}\n\ninterface OTelMetricsAPI {\n getMeter(name: string, version?: string): OTelMeter\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport interface OTelExporterOptions {\n /**\n * Name passed to `metrics.getMeter()`.\n * @default 'tokenwatch'\n */\n meterName?: string\n}\n\n/**\n * OpenTelemetry exporter for tokenwatch.\n *\n * Requires `@opentelemetry/api` to be installed in your project and a\n * `MeterProvider` to be registered globally (e.g. via the OTel SDK).\n *\n * Emits four instruments per tracked call:\n * - `tokenwatch.calls` — Counter\n * - `tokenwatch.input_tokens` — Counter\n * - `tokenwatch.output_tokens` — Counter\n * - `tokenwatch.cost_usd` — Histogram\n *\n * Attributes: `model`, `session.id` (optional), `user.id` (optional), `feature` (optional)\n *\n * @example\n * import { OTelExporter } from '@diogonzafe/tokenwatch/exporters'\n *\n * const tracker = createTracker({ exporter: new OTelExporter() })\n */\nexport class OTelExporter implements IExporter {\n private readonly calls: OTelCounter\n private readonly inputTokens: OTelCounter\n private readonly outputTokens: OTelCounter\n private readonly costUsd: OTelHistogram\n\n constructor(options: OTelExporterOptions = {}, _metricsApi?: OTelMetricsAPI) {\n let metricsApi: OTelMetricsAPI\n if (_metricsApi) {\n metricsApi = _metricsApi\n } else {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n metricsApi = (require('@opentelemetry/api') as { metrics: OTelMetricsAPI }).metrics\n } catch {\n throw new Error(\n '[tokenwatch] OTelExporter requires @opentelemetry/api. Run: npm install @opentelemetry/api',\n )\n }\n }\n const meter = metricsApi.getMeter(options.meterName ?? 'tokenwatch')\n this.calls = meter.createCounter('tokenwatch.calls', {\n description: 'Number of LLM API calls tracked',\n })\n this.inputTokens = meter.createCounter('tokenwatch.input_tokens', {\n description: 'Input tokens consumed (includes cached and cache-creation tokens)',\n })\n this.outputTokens = meter.createCounter('tokenwatch.output_tokens', {\n description: 'Output tokens generated',\n })\n this.costUsd = meter.createHistogram('tokenwatch.cost_usd', {\n description: 'Cost per LLM API call in USD',\n unit: 'USD',\n })\n }\n\n export(entry: UsageEntry): void {\n const attrs: OTelAttributes = { model: entry.model }\n if (entry.sessionId !== undefined) attrs['session.id'] = entry.sessionId\n if (entry.userId !== undefined) attrs['user.id'] = entry.userId\n if (entry.feature !== undefined) attrs['feature'] = entry.feature\n\n this.calls.add(1, attrs)\n this.inputTokens.add(entry.inputTokens + (entry.cachedTokens ?? 0) + (entry.cacheCreationTokens ?? 0), attrs)\n this.outputTokens.add(entry.outputTokens, attrs)\n this.costUsd.record(entry.costUSD, attrs)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACwDO,IAAM,eAAN,MAAwC;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAA+B,CAAC,GAAG,aAA8B;AAC3E,QAAI;AACJ,QAAI,aAAa;AACf,mBAAa;AAAA,IACf,OAAO;AACL,UAAI;AAEF,qBAAc,QAAQ,oBAAoB,EAAkC;AAAA,MAC9E,QAAQ;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAQ,WAAW,SAAS,QAAQ,aAAa,YAAY;AACnE,SAAK,QAAQ,MAAM,cAAc,oBAAoB;AAAA,MACnD,aAAa;AAAA,IACf,CAAC;AACD,SAAK,cAAc,MAAM,cAAc,2BAA2B;AAAA,MAChE,aAAa;AAAA,IACf,CAAC;AACD,SAAK,eAAe,MAAM,cAAc,4BAA4B;AAAA,MAClE,aAAa;AAAA,IACf,CAAC;AACD,SAAK,UAAU,MAAM,gBAAgB,uBAAuB;AAAA,MAC1D,aAAa;AAAA,MACb,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,OAAyB;AAC9B,UAAM,QAAwB,EAAE,OAAO,MAAM,MAAM;AACnD,QAAI,MAAM,cAAc,OAAW,OAAM,YAAY,IAAI,MAAM;AAC/D,QAAI,MAAM,WAAW,OAAW,OAAM,SAAS,IAAI,MAAM;AACzD,QAAI,MAAM,YAAY,OAAW,OAAM,SAAS,IAAI,MAAM;AAE1D,SAAK,MAAM,IAAI,GAAG,KAAK;AACvB,SAAK,YAAY,IAAI,MAAM,eAAe,MAAM,gBAAgB,MAAM,MAAM,uBAAuB,IAAI,KAAK;AAC5G,SAAK,aAAa,IAAI,MAAM,cAAc,KAAK;AAC/C,SAAK,QAAQ,OAAO,MAAM,SAAS,KAAK;AAAA,EAC1C;AACF;","names":[]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { I as IExporter, U as UsageEntry } from './index-D9xq0RNg.cjs';
|
|
2
|
+
|
|
3
|
+
interface OTelAttributes {
|
|
4
|
+
[key: string]: string | number | boolean | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface OTelCounter {
|
|
7
|
+
add(value: number, attributes?: OTelAttributes): void;
|
|
8
|
+
}
|
|
9
|
+
interface OTelHistogram {
|
|
10
|
+
record(value: number, attributes?: OTelAttributes): void;
|
|
11
|
+
}
|
|
12
|
+
interface OTelMeter {
|
|
13
|
+
createCounter(name: string, options?: {
|
|
14
|
+
description?: string;
|
|
15
|
+
unit?: string;
|
|
16
|
+
}): OTelCounter;
|
|
17
|
+
createHistogram(name: string, options?: {
|
|
18
|
+
description?: string;
|
|
19
|
+
unit?: string;
|
|
20
|
+
}): OTelHistogram;
|
|
21
|
+
}
|
|
22
|
+
interface OTelMetricsAPI {
|
|
23
|
+
getMeter(name: string, version?: string): OTelMeter;
|
|
24
|
+
}
|
|
25
|
+
interface OTelExporterOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Name passed to `metrics.getMeter()`.
|
|
28
|
+
* @default 'tokenwatch'
|
|
29
|
+
*/
|
|
30
|
+
meterName?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* OpenTelemetry exporter for tokenwatch.
|
|
34
|
+
*
|
|
35
|
+
* Requires `@opentelemetry/api` to be installed in your project and a
|
|
36
|
+
* `MeterProvider` to be registered globally (e.g. via the OTel SDK).
|
|
37
|
+
*
|
|
38
|
+
* Emits four instruments per tracked call:
|
|
39
|
+
* - `tokenwatch.calls` — Counter
|
|
40
|
+
* - `tokenwatch.input_tokens` — Counter
|
|
41
|
+
* - `tokenwatch.output_tokens` — Counter
|
|
42
|
+
* - `tokenwatch.cost_usd` — Histogram
|
|
43
|
+
*
|
|
44
|
+
* Attributes: `model`, `session.id` (optional), `user.id` (optional), `feature` (optional)
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* import { OTelExporter } from '@diogonzafe/tokenwatch/exporters'
|
|
48
|
+
*
|
|
49
|
+
* const tracker = createTracker({ exporter: new OTelExporter() })
|
|
50
|
+
*/
|
|
51
|
+
declare class OTelExporter implements IExporter {
|
|
52
|
+
private readonly calls;
|
|
53
|
+
private readonly inputTokens;
|
|
54
|
+
private readonly outputTokens;
|
|
55
|
+
private readonly costUsd;
|
|
56
|
+
constructor(options?: OTelExporterOptions, _metricsApi?: OTelMetricsAPI);
|
|
57
|
+
export(entry: UsageEntry): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { OTelExporter, type OTelExporterOptions };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { I as IExporter, U as UsageEntry } from './index-D9xq0RNg.js';
|
|
2
|
+
|
|
3
|
+
interface OTelAttributes {
|
|
4
|
+
[key: string]: string | number | boolean | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface OTelCounter {
|
|
7
|
+
add(value: number, attributes?: OTelAttributes): void;
|
|
8
|
+
}
|
|
9
|
+
interface OTelHistogram {
|
|
10
|
+
record(value: number, attributes?: OTelAttributes): void;
|
|
11
|
+
}
|
|
12
|
+
interface OTelMeter {
|
|
13
|
+
createCounter(name: string, options?: {
|
|
14
|
+
description?: string;
|
|
15
|
+
unit?: string;
|
|
16
|
+
}): OTelCounter;
|
|
17
|
+
createHistogram(name: string, options?: {
|
|
18
|
+
description?: string;
|
|
19
|
+
unit?: string;
|
|
20
|
+
}): OTelHistogram;
|
|
21
|
+
}
|
|
22
|
+
interface OTelMetricsAPI {
|
|
23
|
+
getMeter(name: string, version?: string): OTelMeter;
|
|
24
|
+
}
|
|
25
|
+
interface OTelExporterOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Name passed to `metrics.getMeter()`.
|
|
28
|
+
* @default 'tokenwatch'
|
|
29
|
+
*/
|
|
30
|
+
meterName?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* OpenTelemetry exporter for tokenwatch.
|
|
34
|
+
*
|
|
35
|
+
* Requires `@opentelemetry/api` to be installed in your project and a
|
|
36
|
+
* `MeterProvider` to be registered globally (e.g. via the OTel SDK).
|
|
37
|
+
*
|
|
38
|
+
* Emits four instruments per tracked call:
|
|
39
|
+
* - `tokenwatch.calls` — Counter
|
|
40
|
+
* - `tokenwatch.input_tokens` — Counter
|
|
41
|
+
* - `tokenwatch.output_tokens` — Counter
|
|
42
|
+
* - `tokenwatch.cost_usd` — Histogram
|
|
43
|
+
*
|
|
44
|
+
* Attributes: `model`, `session.id` (optional), `user.id` (optional), `feature` (optional)
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* import { OTelExporter } from '@diogonzafe/tokenwatch/exporters'
|
|
48
|
+
*
|
|
49
|
+
* const tracker = createTracker({ exporter: new OTelExporter() })
|
|
50
|
+
*/
|
|
51
|
+
declare class OTelExporter implements IExporter {
|
|
52
|
+
private readonly calls;
|
|
53
|
+
private readonly inputTokens;
|
|
54
|
+
private readonly outputTokens;
|
|
55
|
+
private readonly costUsd;
|
|
56
|
+
constructor(options?: OTelExporterOptions, _metricsApi?: OTelMetricsAPI);
|
|
57
|
+
export(entry: UsageEntry): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { OTelExporter, type OTelExporterOptions };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/exporters/otel.ts
|
|
9
|
+
var OTelExporter = class {
|
|
10
|
+
calls;
|
|
11
|
+
inputTokens;
|
|
12
|
+
outputTokens;
|
|
13
|
+
costUsd;
|
|
14
|
+
constructor(options = {}, _metricsApi) {
|
|
15
|
+
let metricsApi;
|
|
16
|
+
if (_metricsApi) {
|
|
17
|
+
metricsApi = _metricsApi;
|
|
18
|
+
} else {
|
|
19
|
+
try {
|
|
20
|
+
metricsApi = __require("@opentelemetry/api").metrics;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"[tokenwatch] OTelExporter requires @opentelemetry/api. Run: npm install @opentelemetry/api"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const meter = metricsApi.getMeter(options.meterName ?? "tokenwatch");
|
|
28
|
+
this.calls = meter.createCounter("tokenwatch.calls", {
|
|
29
|
+
description: "Number of LLM API calls tracked"
|
|
30
|
+
});
|
|
31
|
+
this.inputTokens = meter.createCounter("tokenwatch.input_tokens", {
|
|
32
|
+
description: "Input tokens consumed (includes cached and cache-creation tokens)"
|
|
33
|
+
});
|
|
34
|
+
this.outputTokens = meter.createCounter("tokenwatch.output_tokens", {
|
|
35
|
+
description: "Output tokens generated"
|
|
36
|
+
});
|
|
37
|
+
this.costUsd = meter.createHistogram("tokenwatch.cost_usd", {
|
|
38
|
+
description: "Cost per LLM API call in USD",
|
|
39
|
+
unit: "USD"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export(entry) {
|
|
43
|
+
const attrs = { model: entry.model };
|
|
44
|
+
if (entry.sessionId !== void 0) attrs["session.id"] = entry.sessionId;
|
|
45
|
+
if (entry.userId !== void 0) attrs["user.id"] = entry.userId;
|
|
46
|
+
if (entry.feature !== void 0) attrs["feature"] = entry.feature;
|
|
47
|
+
this.calls.add(1, attrs);
|
|
48
|
+
this.inputTokens.add(entry.inputTokens + (entry.cachedTokens ?? 0) + (entry.cacheCreationTokens ?? 0), attrs);
|
|
49
|
+
this.outputTokens.add(entry.outputTokens, attrs);
|
|
50
|
+
this.costUsd.record(entry.costUSD, attrs);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
OTelExporter
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=exporters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/exporters/otel.ts"],"sourcesContent":["import type { UsageEntry, IExporter } from '../types/index.js'\n\n// ─── Local type stubs ─────────────────────────────────────────────────────────\n// Mirror the minimal shape of @opentelemetry/api so this file compiles without\n// a hard compile-time dependency on the package.\n\ninterface OTelAttributes {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface OTelCounter {\n add(value: number, attributes?: OTelAttributes): void\n}\n\ninterface OTelHistogram {\n record(value: number, attributes?: OTelAttributes): void\n}\n\ninterface OTelMeter {\n createCounter(name: string, options?: { description?: string; unit?: string }): OTelCounter\n createHistogram(name: string, options?: { description?: string; unit?: string }): OTelHistogram\n}\n\ninterface OTelMetricsAPI {\n getMeter(name: string, version?: string): OTelMeter\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport interface OTelExporterOptions {\n /**\n * Name passed to `metrics.getMeter()`.\n * @default 'tokenwatch'\n */\n meterName?: string\n}\n\n/**\n * OpenTelemetry exporter for tokenwatch.\n *\n * Requires `@opentelemetry/api` to be installed in your project and a\n * `MeterProvider` to be registered globally (e.g. via the OTel SDK).\n *\n * Emits four instruments per tracked call:\n * - `tokenwatch.calls` — Counter\n * - `tokenwatch.input_tokens` — Counter\n * - `tokenwatch.output_tokens` — Counter\n * - `tokenwatch.cost_usd` — Histogram\n *\n * Attributes: `model`, `session.id` (optional), `user.id` (optional), `feature` (optional)\n *\n * @example\n * import { OTelExporter } from '@diogonzafe/tokenwatch/exporters'\n *\n * const tracker = createTracker({ exporter: new OTelExporter() })\n */\nexport class OTelExporter implements IExporter {\n private readonly calls: OTelCounter\n private readonly inputTokens: OTelCounter\n private readonly outputTokens: OTelCounter\n private readonly costUsd: OTelHistogram\n\n constructor(options: OTelExporterOptions = {}, _metricsApi?: OTelMetricsAPI) {\n let metricsApi: OTelMetricsAPI\n if (_metricsApi) {\n metricsApi = _metricsApi\n } else {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n metricsApi = (require('@opentelemetry/api') as { metrics: OTelMetricsAPI }).metrics\n } catch {\n throw new Error(\n '[tokenwatch] OTelExporter requires @opentelemetry/api. Run: npm install @opentelemetry/api',\n )\n }\n }\n const meter = metricsApi.getMeter(options.meterName ?? 'tokenwatch')\n this.calls = meter.createCounter('tokenwatch.calls', {\n description: 'Number of LLM API calls tracked',\n })\n this.inputTokens = meter.createCounter('tokenwatch.input_tokens', {\n description: 'Input tokens consumed (includes cached and cache-creation tokens)',\n })\n this.outputTokens = meter.createCounter('tokenwatch.output_tokens', {\n description: 'Output tokens generated',\n })\n this.costUsd = meter.createHistogram('tokenwatch.cost_usd', {\n description: 'Cost per LLM API call in USD',\n unit: 'USD',\n })\n }\n\n export(entry: UsageEntry): void {\n const attrs: OTelAttributes = { model: entry.model }\n if (entry.sessionId !== undefined) attrs['session.id'] = entry.sessionId\n if (entry.userId !== undefined) attrs['user.id'] = entry.userId\n if (entry.feature !== undefined) attrs['feature'] = entry.feature\n\n this.calls.add(1, attrs)\n this.inputTokens.add(entry.inputTokens + (entry.cachedTokens ?? 0) + (entry.cacheCreationTokens ?? 0), attrs)\n this.outputTokens.add(entry.outputTokens, attrs)\n this.costUsd.record(entry.costUSD, attrs)\n }\n}\n"],"mappings":";;;;;;;;AAwDO,IAAM,eAAN,MAAwC;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAA+B,CAAC,GAAG,aAA8B;AAC3E,QAAI;AACJ,QAAI,aAAa;AACf,mBAAa;AAAA,IACf,OAAO;AACL,UAAI;AAEF,qBAAc,UAAQ,oBAAoB,EAAkC;AAAA,MAC9E,QAAQ;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAQ,WAAW,SAAS,QAAQ,aAAa,YAAY;AACnE,SAAK,QAAQ,MAAM,cAAc,oBAAoB;AAAA,MACnD,aAAa;AAAA,IACf,CAAC;AACD,SAAK,cAAc,MAAM,cAAc,2BAA2B;AAAA,MAChE,aAAa;AAAA,IACf,CAAC;AACD,SAAK,eAAe,MAAM,cAAc,4BAA4B;AAAA,MAClE,aAAa;AAAA,IACf,CAAC;AACD,SAAK,UAAU,MAAM,gBAAgB,uBAAuB;AAAA,MAC1D,aAAa;AAAA,MACb,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,OAAyB;AAC9B,UAAM,QAAwB,EAAE,OAAO,MAAM,MAAM;AACnD,QAAI,MAAM,cAAc,OAAW,OAAM,YAAY,IAAI,MAAM;AAC/D,QAAI,MAAM,WAAW,OAAW,OAAM,SAAS,IAAI,MAAM;AACzD,QAAI,MAAM,YAAY,OAAW,OAAM,SAAS,IAAI,MAAM;AAE1D,SAAK,MAAM,IAAI,GAAG,KAAK;AACvB,SAAK,YAAY,IAAI,MAAM,eAAe,MAAM,gBAAgB,MAAM,MAAM,uBAAuB,IAAI,KAAK;AAC5G,SAAK,aAAa,IAAI,MAAM,cAAc,KAAK;AAC/C,SAAK,QAAQ,OAAO,MAAM,SAAS,KAAK;AAAA,EAC1C;AACF;","names":[]}
|
|
@@ -24,6 +24,20 @@ interface BudgetConfig {
|
|
|
24
24
|
/** 'once' (default) — fire once per entity lifetime; 'always' — fire on every call that exceeds */
|
|
25
25
|
mode?: 'once' | 'always';
|
|
26
26
|
}
|
|
27
|
+
interface AnomalyDetectionConfig {
|
|
28
|
+
/** Alert when a call's cost exceeds this multiple of the rolling per-entity average (e.g. 3 = 3×) */
|
|
29
|
+
multiplierThreshold: number;
|
|
30
|
+
/** Discord / Slack / generic webhook URL */
|
|
31
|
+
webhookUrl: string;
|
|
32
|
+
/** Hours of history to use as the baseline window (default: 24) */
|
|
33
|
+
windowHours?: number;
|
|
34
|
+
/** 'once' (default) — fire once per entity; 'always' — fire on every anomalous call */
|
|
35
|
+
mode?: 'once' | 'always';
|
|
36
|
+
}
|
|
37
|
+
interface IExporter {
|
|
38
|
+
/** Called after every successful track() — fire-and-forget, errors are swallowed */
|
|
39
|
+
export(entry: UsageEntry): void | Promise<void>;
|
|
40
|
+
}
|
|
27
41
|
interface TrackerConfig {
|
|
28
42
|
/** 'memory' (default), 'sqlite', or a custom IStorage instance (e.g. PostgresStorage, MySQLStorage, MongoStorage) */
|
|
29
43
|
storage?: 'memory' | 'sqlite' | IStorage;
|
|
@@ -44,6 +58,10 @@ interface TrackerConfig {
|
|
|
44
58
|
};
|
|
45
59
|
/** Log a hint after each call suggesting a cheaper model in the same family when savings > 50% */
|
|
46
60
|
suggestions?: boolean;
|
|
61
|
+
/** Alert via webhook when a call's cost is Nx above the rolling average for that user or model */
|
|
62
|
+
anomalyDetection?: AnomalyDetectionConfig;
|
|
63
|
+
/** Custom exporter called after every tracked call (e.g. OTelExporter) */
|
|
64
|
+
exporter?: IExporter;
|
|
47
65
|
}
|
|
48
66
|
interface UsageEntry {
|
|
49
67
|
model: string;
|
|
@@ -160,4 +178,4 @@ interface TrackingMeta {
|
|
|
160
178
|
__feature?: string;
|
|
161
179
|
}
|
|
162
180
|
|
|
163
|
-
export type { BudgetConfig as B, CostForecast as C, FeatureStats as F,
|
|
181
|
+
export type { AnomalyDetectionConfig as A, BudgetConfig as B, CostForecast as C, FeatureStats as F, IExporter as I, LazyTracker as L, ModelPrice as M, PriceMap as P, Report as R, SessionStats as S, TrackerConfig as T, UsageEntry as U, Tracker as a, TrackingMeta as b, ForecastOptions as c, IStorage as d, ModelStats as e, PricesFile as f, ReportOptions as g, UserStats as h };
|
|
@@ -24,6 +24,20 @@ interface BudgetConfig {
|
|
|
24
24
|
/** 'once' (default) — fire once per entity lifetime; 'always' — fire on every call that exceeds */
|
|
25
25
|
mode?: 'once' | 'always';
|
|
26
26
|
}
|
|
27
|
+
interface AnomalyDetectionConfig {
|
|
28
|
+
/** Alert when a call's cost exceeds this multiple of the rolling per-entity average (e.g. 3 = 3×) */
|
|
29
|
+
multiplierThreshold: number;
|
|
30
|
+
/** Discord / Slack / generic webhook URL */
|
|
31
|
+
webhookUrl: string;
|
|
32
|
+
/** Hours of history to use as the baseline window (default: 24) */
|
|
33
|
+
windowHours?: number;
|
|
34
|
+
/** 'once' (default) — fire once per entity; 'always' — fire on every anomalous call */
|
|
35
|
+
mode?: 'once' | 'always';
|
|
36
|
+
}
|
|
37
|
+
interface IExporter {
|
|
38
|
+
/** Called after every successful track() — fire-and-forget, errors are swallowed */
|
|
39
|
+
export(entry: UsageEntry): void | Promise<void>;
|
|
40
|
+
}
|
|
27
41
|
interface TrackerConfig {
|
|
28
42
|
/** 'memory' (default), 'sqlite', or a custom IStorage instance (e.g. PostgresStorage, MySQLStorage, MongoStorage) */
|
|
29
43
|
storage?: 'memory' | 'sqlite' | IStorage;
|
|
@@ -44,6 +58,10 @@ interface TrackerConfig {
|
|
|
44
58
|
};
|
|
45
59
|
/** Log a hint after each call suggesting a cheaper model in the same family when savings > 50% */
|
|
46
60
|
suggestions?: boolean;
|
|
61
|
+
/** Alert via webhook when a call's cost is Nx above the rolling average for that user or model */
|
|
62
|
+
anomalyDetection?: AnomalyDetectionConfig;
|
|
63
|
+
/** Custom exporter called after every tracked call (e.g. OTelExporter) */
|
|
64
|
+
exporter?: IExporter;
|
|
47
65
|
}
|
|
48
66
|
interface UsageEntry {
|
|
49
67
|
model: string;
|
|
@@ -160,4 +178,4 @@ interface TrackingMeta {
|
|
|
160
178
|
__feature?: string;
|
|
161
179
|
}
|
|
162
180
|
|
|
163
|
-
export type { BudgetConfig as B, CostForecast as C, FeatureStats as F,
|
|
181
|
+
export type { AnomalyDetectionConfig as A, BudgetConfig as B, CostForecast as C, FeatureStats as F, IExporter as I, LazyTracker as L, ModelPrice as M, PriceMap as P, Report as R, SessionStats as S, TrackerConfig as T, UsageEntry as U, Tracker as a, TrackingMeta as b, ForecastOptions as c, IStorage as d, ModelStats as e, PricesFile as f, ReportOptions as g, UserStats as h };
|
package/dist/index.cjs
CHANGED
|
@@ -269,7 +269,7 @@ async function getRemotePrices() {
|
|
|
269
269
|
|
|
270
270
|
// prices.json
|
|
271
271
|
var prices_default = {
|
|
272
|
-
updated_at: "2026-04-
|
|
272
|
+
updated_at: "2026-04-23",
|
|
273
273
|
source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
274
274
|
models: {
|
|
275
275
|
"gpt-4o": {
|
|
@@ -1618,7 +1618,14 @@ var TrackerConfigSchema = import_zod.z.object({
|
|
|
1618
1618
|
perUser: BudgetConfigSchema.optional(),
|
|
1619
1619
|
perSession: BudgetConfigSchema.optional()
|
|
1620
1620
|
}).optional(),
|
|
1621
|
-
suggestions: import_zod.z.boolean().optional().default(false)
|
|
1621
|
+
suggestions: import_zod.z.boolean().optional().default(false),
|
|
1622
|
+
anomalyDetection: import_zod.z.object({
|
|
1623
|
+
multiplierThreshold: import_zod.z.number().positive(),
|
|
1624
|
+
webhookUrl: import_zod.z.string().url(),
|
|
1625
|
+
windowHours: import_zod.z.number().positive().optional().default(24),
|
|
1626
|
+
mode: import_zod.z.enum(["once", "always"]).optional().default("once")
|
|
1627
|
+
}).optional(),
|
|
1628
|
+
exporter: import_zod.z.custom((v) => v !== null && typeof v === "object" && typeof v.export === "function").optional()
|
|
1622
1629
|
});
|
|
1623
1630
|
function createTracker(config = {}) {
|
|
1624
1631
|
const parsed = TrackerConfigSchema.safeParse(config);
|
|
@@ -1635,7 +1642,9 @@ ${issues}`);
|
|
|
1635
1642
|
customPrices,
|
|
1636
1643
|
warnIfStaleAfterHours,
|
|
1637
1644
|
budgets,
|
|
1638
|
-
suggestions
|
|
1645
|
+
suggestions,
|
|
1646
|
+
anomalyDetection,
|
|
1647
|
+
exporter
|
|
1639
1648
|
} = parsed.data;
|
|
1640
1649
|
const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
|
|
1641
1650
|
let remotePrices;
|
|
@@ -1668,6 +1677,7 @@ ${issues}`);
|
|
|
1668
1677
|
let alertFired = false;
|
|
1669
1678
|
const firedUserAlerts = /* @__PURE__ */ new Set();
|
|
1670
1679
|
const firedSessionAlerts = /* @__PURE__ */ new Set();
|
|
1680
|
+
const firedAnomalyKeys = /* @__PURE__ */ new Set();
|
|
1671
1681
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1672
1682
|
function resolveModelPrice(model) {
|
|
1673
1683
|
maybeWarnStaleness();
|
|
@@ -1692,7 +1702,12 @@ ${issues}`);
|
|
|
1692
1702
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1693
1703
|
};
|
|
1694
1704
|
storage.record(full);
|
|
1705
|
+
if (exporter) {
|
|
1706
|
+
Promise.resolve(exporter.export(full)).catch(() => {
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1695
1709
|
maybeFireAlerts(full);
|
|
1710
|
+
if (anomalyDetection) maybeDetectAnomaly(full);
|
|
1696
1711
|
if (suggestions) {
|
|
1697
1712
|
maybeSuggestCheaperModel(entry.model, costUSD, entry.inputTokens, entry.outputTokens, {
|
|
1698
1713
|
bundledPrices,
|
|
@@ -1862,11 +1877,56 @@ ${issues}`);
|
|
|
1862
1877
|
basedOnPeriod: { from: first, to: last }
|
|
1863
1878
|
};
|
|
1864
1879
|
}
|
|
1880
|
+
function maybeDetectAnomaly(entry) {
|
|
1881
|
+
if (entry.costUSD <= 0) return;
|
|
1882
|
+
const { multiplierThreshold, webhookUrl: aUrl, windowHours: wh, mode: modeRaw } = anomalyDetection;
|
|
1883
|
+
const wHours = wh ?? 24;
|
|
1884
|
+
const mode = modeRaw ?? "once";
|
|
1885
|
+
const windowStart = Date.now() - wHours * 60 * 60 * 1e3;
|
|
1886
|
+
const entryTs = new Date(entry.timestamp).getTime();
|
|
1887
|
+
function checkEntity(key, label, predicate) {
|
|
1888
|
+
if (mode !== "always" && firedAnomalyKeys.has(key)) return;
|
|
1889
|
+
if (mode !== "always") firedAnomalyKeys.add(key);
|
|
1890
|
+
Promise.resolve(storage.getAll()).then((all) => {
|
|
1891
|
+
const history = all.filter(
|
|
1892
|
+
(e) => predicate(e) && new Date(e.timestamp).getTime() >= windowStart && new Date(e.timestamp).getTime() !== entryTs
|
|
1893
|
+
);
|
|
1894
|
+
if (history.length === 0) {
|
|
1895
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const avg = history.reduce((s, e) => s + e.costUSD, 0) / history.length;
|
|
1899
|
+
if (avg <= 0 || entry.costUSD <= avg * multiplierThreshold) {
|
|
1900
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const multiple = (entry.costUSD / avg).toFixed(1);
|
|
1904
|
+
fireWebhook(aUrl, {
|
|
1905
|
+
text: `[tokenwatch] Anomaly: ${label} call cost $${entry.costUSD.toFixed(4)} is ${multiple}x above ${wHours}h average ($${avg.toFixed(4)})`
|
|
1906
|
+
});
|
|
1907
|
+
}).catch(() => {
|
|
1908
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
if (entry.userId) {
|
|
1912
|
+
checkEntity(
|
|
1913
|
+
`user:${entry.userId}`,
|
|
1914
|
+
`user "${entry.userId}"`,
|
|
1915
|
+
(e) => e.userId === entry.userId
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
checkEntity(
|
|
1919
|
+
`model:${entry.model}`,
|
|
1920
|
+
`model "${entry.model}"`,
|
|
1921
|
+
(e) => e.model === entry.model
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1865
1924
|
async function reset() {
|
|
1866
1925
|
await Promise.resolve(storage.clearAll());
|
|
1867
1926
|
alertFired = false;
|
|
1868
1927
|
firedUserAlerts.clear();
|
|
1869
1928
|
firedSessionAlerts.clear();
|
|
1929
|
+
firedAnomalyKeys.clear();
|
|
1870
1930
|
}
|
|
1871
1931
|
async function resetSession(sessionId) {
|
|
1872
1932
|
await Promise.resolve(storage.clearSession(sessionId));
|