@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.
@@ -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, IStorage 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, ModelStats as d, PricesFile as e, ReportOptions as f, UserStats as g };
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, IStorage 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, ModelStats as d, PricesFile as e, ReportOptions as f, UserStats as g };
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-22",
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));