@sisu-ai/mw-usage-tracker 1.0.0 → 2.0.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 CHANGED
@@ -2,14 +2,33 @@
2
2
 
3
3
  Track token usage across your pipeline and estimate cost.
4
4
 
5
+ ## Setup
6
+ ```bash
7
+ npm i @sisu-ai/mw-usage-tracker
8
+ ```
9
+
10
+ ## Documentation
11
+ Discover what you can do through examples or documentation. Check it out at https://github.com/finger-gun/sisu
12
+
5
13
  ## Usage
6
14
  ```ts
7
15
  import { usageTracker } from '@sisu-ai/mw-usage-tracker';
8
16
 
9
17
  const app = new Agent()
10
18
  .use(usageTracker({
11
- 'openai:gpt-4o-mini': { inputPer1K: 0.15, outputPer1K: 0.6 },
12
- '*': { inputPer1K: 0.15, outputPer1K: 0.6 },
19
+ 'openai:gpt-4o-mini': {
20
+ // Preferred: prices per 1M tokens (matches provider docs)
21
+ inputPer1M: 0.15,
22
+ outputPer1M: 0.60,
23
+ // Optional vision pricing (choose one):
24
+ // a) Per 1K images (e.g., $0.217/K images)
25
+ imagePer1K: 0.217,
26
+ // b) Approximate per-1K "image tokens"
27
+ // imageInputPer1K: 0.217,
28
+ // imageTokenPerImage: 1000,
29
+ },
30
+ // Fallback default for other models
31
+ '*': { inputPer1M: 0.15, outputPer1M: 0.60 },
13
32
  }, { logPerCall: true }))
14
33
  ```
15
34
 
@@ -17,8 +36,16 @@ const app = new Agent()
17
36
  - Wraps `ctx.model.generate` for the duration of the pipeline.
18
37
  - Accumulates `promptTokens`, `completionTokens`, `totalTokens` from `ModelResponse.usage`.
19
38
  - If a price table is provided, computes `costUSD` per call and totals.
39
+ - Optional: for vision models
40
+ - Providers vary: some include image cost in native token usage; others bill separately per image.
41
+ - If billed per image batch, set `imagePer1K` (e.g., 0.217 USD per 1K images). The tracker converts this to per-image.
42
+ - Or configure `imageInputPer1K` + `imageTokenPerImage` to approximate per-1K image-token pricing. In that mode we split
43
+ prompt tokens into estimated `imageTokens = imageCount * imageTokenPerImage` (default 1000) and
44
+ `textPromptTokens = promptTokens - imageTokens`, then price them separately.
20
45
  - Writes totals to `ctx.state.usage` and logs them at the end.
46
+ - Cost is rounded to 6 decimals to avoid showing small calls as 0.00.
21
47
 
22
48
  ## Notes
23
49
  - Each adapter should map its native usage fields to `ModelResponse.usage`.
24
50
  - If a provider doesn’t return usage, you’ll get counts of calls only (no cost).
51
+ - Image cost estimation is an approximation unless your adapter returns precise image token usage.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import type { Middleware } from '@sisu-ai/core';
2
2
  export type PriceTable = Record<string, {
3
- inputPer1K: number;
4
- outputPer1K: number;
3
+ inputPer1M?: number;
4
+ outputPer1M?: number;
5
+ inputPer1K?: number;
6
+ outputPer1K?: number;
7
+ imagePerImage?: number;
8
+ imagePer1K?: number;
9
+ imageInputPer1K?: number;
10
+ imageTokenPerImage?: number;
5
11
  }>;
6
12
  export interface UsageTotals {
7
13
  promptTokens: number;
8
14
  completionTokens: number;
9
15
  totalTokens: number;
10
16
  costUSD?: number;
17
+ imageTokens?: number;
18
+ imageCount?: number;
11
19
  }
12
20
  export declare function usageTracker(prices: PriceTable, opts?: {
13
21
  logPerCall?: boolean;
package/dist/index.js CHANGED
@@ -10,6 +10,10 @@ export function usageTracker(prices, opts = {}) {
10
10
  costUSD: Number(ctx.state?.usage?.costUSD ?? 0),
11
11
  };
12
12
  async function withUsage(...args) {
13
+ // Inspect request messages to estimate image inputs
14
+ const reqMessages = args?.[0];
15
+ const imageCount = countImageInputs(reqMessages);
16
+ const imageTokens = imageCount * Number(price?.imageTokenPerImage ?? 1000);
13
17
  const out = await orig(...args);
14
18
  const u = out.usage;
15
19
  if (u) {
@@ -20,11 +24,41 @@ export function usageTracker(prices, opts = {}) {
20
24
  totals.completionTokens += c;
21
25
  totals.totalTokens += t;
22
26
  if (price) {
23
- const cost = (p / 1000) * price.inputPer1K + (c / 1000) * price.outputPer1K;
27
+ // Resolve effective per-1K pricing from provided per-1M or per-1K fields
28
+ const inPer1K = price.inputPer1K ?? (price.inputPer1M != null ? price.inputPer1M / 1000 : undefined) ?? 0;
29
+ const outPer1K = price.outputPer1K ?? (price.outputPer1M != null ? price.outputPer1M / 1000 : undefined) ?? 0;
30
+ // Base text+completion token cost
31
+ const textPromptTokens = Math.max(0, p - (price.imageInputPer1K ? imageTokens : 0));
32
+ const tokenCost = (textPromptTokens / 1000) * inPer1K + (c / 1000) * outPer1K;
33
+ // Image cost (prefer per-image if provided)
34
+ const perImage = price.imagePerImage != null ? price.imagePerImage : (price.imagePer1K != null ? price.imagePer1K / 1000 : undefined);
35
+ const imageCost = perImage != null
36
+ ? imageCount * perImage
37
+ : price.imageInputPer1K
38
+ ? (imageTokens / 1000) * price.imageInputPer1K
39
+ : 0;
40
+ const cost = tokenCost + imageCost;
24
41
  totals.costUSD = Number((totals.costUSD ?? 0) + cost);
42
+ if (imageCount > 0 && price.imageInputPer1K) {
43
+ totals.imageTokens = Number((totals.imageTokens ?? 0) + imageTokens);
44
+ totals.imageCount = Number((totals.imageCount ?? 0) + imageCount);
45
+ }
25
46
  }
26
47
  if (opts.logPerCall)
27
- ctx.log.info?.('[usage] call', { promptTokens: p, completionTokens: c, totalTokens: t, estCostUSD: price ? round2((p / 1000) * price.inputPer1K + (c / 1000) * price.outputPer1K) : undefined });
48
+ ctx.log.info?.('[usage] call', {
49
+ promptTokens: p, completionTokens: c, totalTokens: t,
50
+ imageTokens: (imageCount > 0 && price?.imageInputPer1K) ? imageTokens : undefined,
51
+ imageCount: imageCount > 0 ? imageCount : undefined,
52
+ estCostUSD: price ? (() => {
53
+ const inPer1K = price.inputPer1K ?? (price.inputPer1M != null ? price.inputPer1M / 1000 : 0);
54
+ const outPer1K = price.outputPer1K ?? (price.outputPer1M != null ? price.outputPer1M / 1000 : 0);
55
+ const textPromptTokens = Math.max(0, p - (price?.imageInputPer1K ? imageTokens : 0));
56
+ const perImage = price?.imagePerImage != null ? price.imagePerImage : (price?.imagePer1K != null ? price.imagePer1K / 1000 : undefined);
57
+ const tokenCost = (textPromptTokens / 1000) * inPer1K + (c / 1000) * outPer1K;
58
+ const imageCost = perImage != null ? (imageCount * perImage) : (price?.imageInputPer1K ? (imageTokens / 1000) * price.imageInputPer1K : 0);
59
+ return roundUSD(tokenCost + imageCost);
60
+ })() : undefined,
61
+ });
28
62
  }
29
63
  return out;
30
64
  }
@@ -39,8 +73,34 @@ export function usageTracker(prices, opts = {}) {
39
73
  ctx.state.usage.completionTokens = totals.completionTokens;
40
74
  ctx.state.usage.totalTokens = totals.totalTokens;
41
75
  if (price)
42
- ctx.state.usage.costUSD = round2(totals.costUSD ?? 0);
76
+ ctx.state.usage.costUSD = roundUSD(totals.costUSD ?? 0);
77
+ if (totals.imageTokens)
78
+ ctx.state.usage.imageTokens = totals.imageTokens;
79
+ if (totals.imageCount)
80
+ ctx.state.usage.imageCount = totals.imageCount;
43
81
  ctx.log.info?.('[usage] totals', ctx.state.usage);
44
82
  };
45
83
  }
46
- function round2(n) { return Math.round(n * 100) / 100; }
84
+ function roundUSD(n) { return Math.round(n * 1e6) / 1e6; }
85
+ function countImageInputs(msgs) {
86
+ if (!Array.isArray(msgs))
87
+ return 0;
88
+ let count = 0;
89
+ for (const m of msgs) {
90
+ const c = m?.content;
91
+ if (Array.isArray(c)) {
92
+ for (const part of c) {
93
+ if (part && typeof part === 'object' && (part.type === 'image_url' || part.type === 'image'))
94
+ count += 1;
95
+ }
96
+ }
97
+ // Convenience shapes supported by adapters (count regardless of content presence)
98
+ if (Array.isArray(m?.images))
99
+ count += m.images.length;
100
+ if (typeof m?.image_url === 'string')
101
+ count += 1;
102
+ if (typeof m?.image === 'string')
103
+ count += 1;
104
+ }
105
+ return count;
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisu-ai/mw-usage-tracker",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,15 @@
14
14
  "build": "tsc -b"
15
15
  },
16
16
  "peerDependencies": {
17
- "@sisu-ai/core": "0.2.0"
17
+ "@sisu-ai/core": "0.3.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/finger-gun/sisu",
22
+ "directory": "packages/middleware/usage-tracker"
23
+ },
24
+ "homepage": "https://github.com/finger-gun/sisu#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/finger-gun/sisu/issues"
18
27
  }
19
28
  }