@diogonzafe/tokenwatch 0.5.0 → 0.7.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 +137 -2
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +467 -41
- 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 +69 -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 +69 -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 +7 -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-24",
|
|
273
273
|
source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
274
274
|
models: {
|
|
275
275
|
"gpt-4o": {
|
|
@@ -1586,6 +1586,12 @@ var prices_default = {
|
|
|
1586
1586
|
cachedInput: 0.3,
|
|
1587
1587
|
cacheCreationInput: 3.75,
|
|
1588
1588
|
maxInputTokens: 1e6
|
|
1589
|
+
},
|
|
1590
|
+
"gpt-5.5": {
|
|
1591
|
+
input: 5,
|
|
1592
|
+
output: 30,
|
|
1593
|
+
cachedInput: 0.5,
|
|
1594
|
+
maxInputTokens: 272e3
|
|
1589
1595
|
}
|
|
1590
1596
|
}
|
|
1591
1597
|
};
|
|
@@ -1618,7 +1624,14 @@ var TrackerConfigSchema = import_zod.z.object({
|
|
|
1618
1624
|
perUser: BudgetConfigSchema.optional(),
|
|
1619
1625
|
perSession: BudgetConfigSchema.optional()
|
|
1620
1626
|
}).optional(),
|
|
1621
|
-
suggestions: import_zod.z.boolean().optional().default(false)
|
|
1627
|
+
suggestions: import_zod.z.boolean().optional().default(false),
|
|
1628
|
+
anomalyDetection: import_zod.z.object({
|
|
1629
|
+
multiplierThreshold: import_zod.z.number().positive(),
|
|
1630
|
+
webhookUrl: import_zod.z.string().url(),
|
|
1631
|
+
windowHours: import_zod.z.number().positive().optional().default(24),
|
|
1632
|
+
mode: import_zod.z.enum(["once", "always"]).optional().default("once")
|
|
1633
|
+
}).optional(),
|
|
1634
|
+
exporter: import_zod.z.custom((v) => v !== null && typeof v === "object" && typeof v.export === "function").optional()
|
|
1622
1635
|
});
|
|
1623
1636
|
function createTracker(config = {}) {
|
|
1624
1637
|
const parsed = TrackerConfigSchema.safeParse(config);
|
|
@@ -1635,7 +1648,9 @@ ${issues}`);
|
|
|
1635
1648
|
customPrices,
|
|
1636
1649
|
warnIfStaleAfterHours,
|
|
1637
1650
|
budgets,
|
|
1638
|
-
suggestions
|
|
1651
|
+
suggestions,
|
|
1652
|
+
anomalyDetection,
|
|
1653
|
+
exporter
|
|
1639
1654
|
} = parsed.data;
|
|
1640
1655
|
const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
|
|
1641
1656
|
let remotePrices;
|
|
@@ -1668,6 +1683,7 @@ ${issues}`);
|
|
|
1668
1683
|
let alertFired = false;
|
|
1669
1684
|
const firedUserAlerts = /* @__PURE__ */ new Set();
|
|
1670
1685
|
const firedSessionAlerts = /* @__PURE__ */ new Set();
|
|
1686
|
+
const firedAnomalyKeys = /* @__PURE__ */ new Set();
|
|
1671
1687
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1672
1688
|
function resolveModelPrice(model) {
|
|
1673
1689
|
maybeWarnStaleness();
|
|
@@ -1692,7 +1708,12 @@ ${issues}`);
|
|
|
1692
1708
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1693
1709
|
};
|
|
1694
1710
|
storage.record(full);
|
|
1711
|
+
if (exporter) {
|
|
1712
|
+
Promise.resolve(exporter.export(full)).catch(() => {
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1695
1715
|
maybeFireAlerts(full);
|
|
1716
|
+
if (anomalyDetection) maybeDetectAnomaly(full);
|
|
1696
1717
|
if (suggestions) {
|
|
1697
1718
|
maybeSuggestCheaperModel(entry.model, costUSD, entry.inputTokens, entry.outputTokens, {
|
|
1698
1719
|
bundledPrices,
|
|
@@ -1862,11 +1883,56 @@ ${issues}`);
|
|
|
1862
1883
|
basedOnPeriod: { from: first, to: last }
|
|
1863
1884
|
};
|
|
1864
1885
|
}
|
|
1886
|
+
function maybeDetectAnomaly(entry) {
|
|
1887
|
+
if (entry.costUSD <= 0) return;
|
|
1888
|
+
const { multiplierThreshold, webhookUrl: aUrl, windowHours: wh, mode: modeRaw } = anomalyDetection;
|
|
1889
|
+
const wHours = wh ?? 24;
|
|
1890
|
+
const mode = modeRaw ?? "once";
|
|
1891
|
+
const windowStart = Date.now() - wHours * 60 * 60 * 1e3;
|
|
1892
|
+
const entryTs = new Date(entry.timestamp).getTime();
|
|
1893
|
+
function checkEntity(key, label, predicate) {
|
|
1894
|
+
if (mode !== "always" && firedAnomalyKeys.has(key)) return;
|
|
1895
|
+
if (mode !== "always") firedAnomalyKeys.add(key);
|
|
1896
|
+
Promise.resolve(storage.getAll()).then((all) => {
|
|
1897
|
+
const history = all.filter(
|
|
1898
|
+
(e) => predicate(e) && new Date(e.timestamp).getTime() >= windowStart && new Date(e.timestamp).getTime() !== entryTs
|
|
1899
|
+
);
|
|
1900
|
+
if (history.length === 0) {
|
|
1901
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
const avg = history.reduce((s, e) => s + e.costUSD, 0) / history.length;
|
|
1905
|
+
if (avg <= 0 || entry.costUSD <= avg * multiplierThreshold) {
|
|
1906
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const multiple = (entry.costUSD / avg).toFixed(1);
|
|
1910
|
+
fireWebhook(aUrl, {
|
|
1911
|
+
text: `[tokenwatch] Anomaly: ${label} call cost $${entry.costUSD.toFixed(4)} is ${multiple}x above ${wHours}h average ($${avg.toFixed(4)})`
|
|
1912
|
+
});
|
|
1913
|
+
}).catch(() => {
|
|
1914
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
if (entry.userId) {
|
|
1918
|
+
checkEntity(
|
|
1919
|
+
`user:${entry.userId}`,
|
|
1920
|
+
`user "${entry.userId}"`,
|
|
1921
|
+
(e) => e.userId === entry.userId
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
checkEntity(
|
|
1925
|
+
`model:${entry.model}`,
|
|
1926
|
+
`model "${entry.model}"`,
|
|
1927
|
+
(e) => e.model === entry.model
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1865
1930
|
async function reset() {
|
|
1866
1931
|
await Promise.resolve(storage.clearAll());
|
|
1867
1932
|
alertFired = false;
|
|
1868
1933
|
firedUserAlerts.clear();
|
|
1869
1934
|
firedSessionAlerts.clear();
|
|
1935
|
+
firedAnomalyKeys.clear();
|
|
1870
1936
|
}
|
|
1871
1937
|
async function resetSession(sessionId) {
|
|
1872
1938
|
await Promise.resolve(storage.clearSession(sessionId));
|