@sisu-ai/mw-usage-tracker 9.0.1 → 9.0.3

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
@@ -1,3 +1,9 @@
1
+ <div align="right">
2
+ <a href="https://github.com/finger-gun/sisu"><img src="https://github.com/finger-gun/sisu/raw/main/sisu-light.svg" alt="ProjectSpecs" width="100" /></a>
3
+ </div>
4
+
5
+ ---
6
+
1
7
  # @sisu-ai/mw-usage-tracker
2
8
 
3
9
  Track token usage across your pipeline and estimate cost.
@@ -62,3 +68,78 @@ Discover what you can do through examples or documentation. Check it out at http
62
68
  - [License](https://github.com/finger-gun/sisu/blob/main/LICENSE)
63
69
  - [Report a Bug](https://github.com/finger-gun/sisu/issues/new?template=bug_report.md)
64
70
  - [Request a Feature](https://github.com/finger-gun/sisu/issues/new?template=feature_request.md)
71
+
72
+ ---
73
+
74
+ ## Documentation
75
+
76
+ **Core** — [Package docs](packages/core/README.md) · [Error types](packages/core/ERROR_TYPES.md)
77
+
78
+ **Adapters** — [OpenAI](packages/adapters/openai/README.md) · [Anthropic](packages/adapters/anthropic/README.md) · [Ollama](packages/adapters/ollama/README.md)
79
+
80
+ <details>
81
+ <summary>All middleware packages</summary>
82
+
83
+ - [@sisu-ai/mw-agent-run-api](packages/middleware/agent-run-api/README.md)
84
+ - [@sisu-ai/mw-context-compressor](packages/middleware/context-compressor/README.md)
85
+ - [@sisu-ai/mw-control-flow](packages/middleware/control-flow/README.md)
86
+ - [@sisu-ai/mw-conversation-buffer](packages/middleware/conversation-buffer/README.md)
87
+ - [@sisu-ai/mw-cors](packages/middleware/cors/README.md)
88
+ - [@sisu-ai/mw-error-boundary](packages/middleware/error-boundary/README.md)
89
+ - [@sisu-ai/mw-guardrails](packages/middleware/guardrails/README.md)
90
+ - [@sisu-ai/mw-invariants](packages/middleware/invariants/README.md)
91
+ - [@sisu-ai/mw-orchestration](packages/middleware/orchestration/README.md)
92
+ - [@sisu-ai/mw-rag](packages/middleware/rag/README.md)
93
+ - [@sisu-ai/mw-react-parser](packages/middleware/react-parser/README.md)
94
+ - [@sisu-ai/mw-register-tools](packages/middleware/register-tools/README.md)
95
+ - [@sisu-ai/mw-tool-calling](packages/middleware/tool-calling/README.md)
96
+ - [@sisu-ai/mw-trace-viewer](packages/middleware/trace-viewer/README.md)
97
+ - [@sisu-ai/mw-usage-tracker](packages/middleware/usage-tracker/README.md)
98
+ </details>
99
+
100
+ <details>
101
+ <summary>All tool packages</summary>
102
+
103
+ - [@sisu-ai/tool-aws-s3](packages/tools/aws-s3/README.md)
104
+ - [@sisu-ai/tool-azure-blob](packages/tools/azure-blob/README.md)
105
+ - [@sisu-ai/tool-extract-urls](packages/tools/extract-urls/README.md)
106
+ - [@sisu-ai/tool-github-projects](packages/tools/github-projects/README.md)
107
+ - [@sisu-ai/tool-summarize-text](packages/tools/summarize-text/README.md)
108
+ - [@sisu-ai/tool-terminal](packages/tools/terminal/README.md)
109
+ - [@sisu-ai/tool-vec-chroma](packages/tools/vec-chroma/README.md)
110
+ - [@sisu-ai/tool-web-fetch](packages/tools/web-fetch/README.md)
111
+ - [@sisu-ai/tool-web-search-duckduckgo](packages/tools/web-search-duckduckgo/README.md)
112
+ - [@sisu-ai/tool-web-search-google](packages/tools/web-search-google/README.md)
113
+ - [@sisu-ai/tool-web-search-openai](packages/tools/web-search-openai/README.md)
114
+ - [@sisu-ai/tool-wikipedia](packages/tools/wikipedia/README.md)
115
+ </details>
116
+
117
+ <details>
118
+ <summary>All examples</summary>
119
+
120
+ **Anthropic** — [hello](examples/anthropic-hello/README.md) · [control-flow](examples/anthropic-control-flow/README.md) · [stream](examples/anthropic-stream/README.md) · [weather](examples/anthropic-weather/README.md)
121
+
122
+ **Ollama** — [hello](examples/ollama-hello/README.md) · [stream](examples/ollama-stream/README.md) · [vision](examples/ollama-vision/README.md) · [weather](examples/ollama-weather/README.md) · [web-search](examples/ollama-web-search/README.md)
123
+
124
+ **OpenAI** — [hello](examples/openai-hello/README.md) · [weather](examples/openai-weather/README.md) · [stream](examples/openai-stream/README.md) · [vision](examples/openai-vision/README.md) · [reasoning](examples/openai-reasoning/README.md) · [react](examples/openai-react/README.md) · [control-flow](examples/openai-control-flow/README.md) · [branch](examples/openai-branch/README.md) · [parallel](examples/openai-parallel/README.md) · [graph](examples/openai-graph/README.md) · [orchestration](examples/openai-orchestration/README.md) · [orchestration-adaptive](examples/openai-orchestration-adaptive/README.md) · [guardrails](examples/openai-guardrails/README.md) · [error-handling](examples/openai-error-handling/README.md) · [rag-chroma](examples/openai-rag-chroma/README.md) · [web-search](examples/openai-web-search/README.md) · [web-fetch](examples/openai-web-fetch/README.md) · [wikipedia](examples/openai-wikipedia/README.md) · [terminal](examples/openai-terminal/README.md) · [github-projects](examples/openai-github-projects/README.md) · [server](examples/openai-server/README.md) · [aws-s3](examples/openai-aws-s3/README.md) · [azure-blob](examples/openai-azure-blob/README.md)
125
+ </details>
126
+
127
+ ---
128
+
129
+ ## Contributing
130
+
131
+ We build Sisu in the open. Contributions welcome.
132
+
133
+ [Contributing Guide](CONTRIBUTING.md) · [Report a Bug](https://github.com/finger-gun/sisu/issues/new?template=bug_report.md) · [Request a Feature](https://github.com/finger-gun/sisu/issues/new?template=feature_request.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
134
+
135
+ ---
136
+
137
+ <div align="center">
138
+
139
+ **[Star on GitHub](https://github.com/finger-gun/sisu)** if Sisu helps you build better agents.
140
+
141
+ *Quiet, determined, relentlessly useful.*
142
+
143
+ [Apache 2.0 License](LICENSE)
144
+
145
+ </div>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Middleware } from '@sisu-ai/core';
1
+ import type { Middleware } from "@sisu-ai/core";
2
2
  export type PriceTable = Record<string, {
3
3
  inputPer1M?: number;
4
4
  outputPer1M?: number;
package/dist/index.js CHANGED
@@ -2,36 +2,39 @@ export function usageTracker(prices, opts = {}) {
2
2
  return async (ctx, next) => {
3
3
  // Wrap model.generate to intercept responses
4
4
  const orig = ctx.model.generate.bind(ctx.model);
5
- const price = prices[ctx.model.name] ?? prices['*'];
5
+ const price = prices[ctx.model.name] ?? prices["*"];
6
+ const state = ctx.state;
6
7
  const totals = {
7
- promptTokens: Number(ctx.state?.usage?.promptTokens ?? 0),
8
- completionTokens: Number(ctx.state?.usage?.completionTokens ?? 0),
9
- totalTokens: Number(ctx.state?.usage?.totalTokens ?? 0),
10
- costUSD: Number(ctx.state?.usage?.costUSD ?? 0),
8
+ promptTokens: Number(state.usage?.promptTokens ?? 0),
9
+ completionTokens: Number(state.usage?.completionTokens ?? 0),
10
+ totalTokens: Number(state.usage?.totalTokens ?? 0),
11
+ costUSD: Number(state.usage?.costUSD ?? 0),
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);
17
- const out = await orig(...args);
13
+ function applyUsage(out, imageCount, imageTokens) {
18
14
  const u = out.usage;
19
15
  if (u) {
20
16
  const p = Number(u.promptTokens ?? 0);
21
17
  const c = Number(u.completionTokens ?? 0);
22
- const t = Number(u.totalTokens ?? (p + c));
18
+ const t = Number(u.totalTokens ?? p + c);
23
19
  totals.promptTokens += p;
24
20
  totals.completionTokens += c;
25
21
  totals.totalTokens += t;
26
22
  if (price) {
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
23
+ const inPer1K = price.inputPer1K ??
24
+ (price.inputPer1M != null ? price.inputPer1M / 1000 : undefined) ??
25
+ 0;
26
+ const outPer1K = price.outputPer1K ??
27
+ (price.outputPer1M != null
28
+ ? price.outputPer1M / 1000
29
+ : undefined) ??
30
+ 0;
31
31
  const textPromptTokens = Math.max(0, p - (price.imageInputPer1K ? imageTokens : 0));
32
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);
33
+ const perImage = price.imagePerImage != null
34
+ ? price.imagePerImage
35
+ : price.imagePer1K != null
36
+ ? price.imagePer1K / 1000
37
+ : undefined;
35
38
  const imageCost = perImage != null
36
39
  ? imageCount * perImage
37
40
  : price.imageInputPer1K
@@ -45,43 +48,95 @@ export function usageTracker(prices, opts = {}) {
45
48
  }
46
49
  }
47
50
  if (opts.logPerCall)
48
- ctx.log.info?.('[usage] call', {
49
- promptTokens: p, completionTokens: c, totalTokens: t,
50
- imageTokens: (imageCount > 0 && price?.imageInputPer1K) ? imageTokens : undefined,
51
+ ctx.log.info?.("[usage] call", {
52
+ promptTokens: p,
53
+ completionTokens: c,
54
+ totalTokens: t,
55
+ imageTokens: imageCount > 0 && price?.imageInputPer1K
56
+ ? imageTokens
57
+ : undefined,
51
58
  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,
59
+ estCostUSD: price
60
+ ? (() => {
61
+ const inPer1K = price.inputPer1K ??
62
+ (price.inputPer1M != null ? price.inputPer1M / 1000 : 0);
63
+ const outPer1K = price.outputPer1K ??
64
+ (price.outputPer1M != null ? price.outputPer1M / 1000 : 0);
65
+ const textPromptTokens = Math.max(0, p - (price?.imageInputPer1K ? imageTokens : 0));
66
+ const perImage = price?.imagePerImage != null
67
+ ? price.imagePerImage
68
+ : price?.imagePer1K != null
69
+ ? price.imagePer1K / 1000
70
+ : undefined;
71
+ const tokenCost = (textPromptTokens / 1000) * inPer1K + (c / 1000) * outPer1K;
72
+ const imageCost = perImage != null
73
+ ? imageCount * perImage
74
+ : price?.imageInputPer1K
75
+ ? (imageTokens / 1000) * price.imageInputPer1K
76
+ : 0;
77
+ return roundUSD(tokenCost + imageCost);
78
+ })()
79
+ : undefined,
61
80
  });
62
81
  }
63
- return out;
64
82
  }
65
- ctx.state._origGenerate = orig;
66
- ctx.model.generate = withUsage;
83
+ const isAsyncIterable = (val) => !!val &&
84
+ typeof val[Symbol.asyncIterator] ===
85
+ "function";
86
+ function withUsage(...args) {
87
+ const reqMessages = args?.[0];
88
+ const imageCount = countImageInputs(reqMessages);
89
+ const imageTokens = imageCount * Number(price?.imageTokenPerImage ?? 1000);
90
+ const out = orig(...args);
91
+ if (isAsyncIterable(out)) {
92
+ const iter = async function* () {
93
+ let final;
94
+ for await (const ev of out) {
95
+ if (ev.type === "assistant_message") {
96
+ final = { message: ev.message };
97
+ }
98
+ else if (ev.type === "usage") {
99
+ final = {
100
+ message: { role: "assistant", content: "" },
101
+ usage: ev.usage,
102
+ };
103
+ }
104
+ yield ev;
105
+ }
106
+ if (final)
107
+ applyUsage(final, imageCount, imageTokens);
108
+ };
109
+ return iter();
110
+ }
111
+ return (async () => {
112
+ const resolved = await out;
113
+ applyUsage(resolved, imageCount, imageTokens);
114
+ return resolved;
115
+ })();
116
+ }
117
+ state._origGenerate = orig;
118
+ ctx.model.generate =
119
+ withUsage;
67
120
  await next();
68
121
  // Restore
69
122
  ctx.model.generate = orig;
70
- if (!ctx.state.usage)
71
- ctx.state.usage = {};
72
- ctx.state.usage.promptTokens = totals.promptTokens;
73
- ctx.state.usage.completionTokens = totals.completionTokens;
74
- ctx.state.usage.totalTokens = totals.totalTokens;
123
+ if (!state.usage)
124
+ state.usage = {};
125
+ state.usage.promptTokens = totals.promptTokens;
126
+ state.usage.completionTokens = totals.completionTokens;
127
+ state.usage.totalTokens = totals.totalTokens;
75
128
  if (price)
76
- ctx.state.usage.costUSD = roundUSD(totals.costUSD ?? 0);
129
+ state.usage.costUSD = roundUSD(totals.costUSD ?? 0);
77
130
  if (totals.imageTokens)
78
- ctx.state.usage.imageTokens = totals.imageTokens;
131
+ state.usage.imageTokens = totals.imageTokens;
79
132
  if (totals.imageCount)
80
- ctx.state.usage.imageCount = totals.imageCount;
81
- ctx.log.info?.('[usage] totals', ctx.state.usage);
133
+ state.usage.imageCount = totals.imageCount;
134
+ ctx.log.info?.("[usage] totals", state.usage);
82
135
  };
83
136
  }
84
- function roundUSD(n) { return Math.round(n * 1e6) / 1e6; }
137
+ function roundUSD(n) {
138
+ return Math.round(n * 1e6) / 1e6;
139
+ }
85
140
  function countImageInputs(msgs) {
86
141
  if (!Array.isArray(msgs))
87
142
  return 0;
@@ -90,16 +145,20 @@ function countImageInputs(msgs) {
90
145
  const c = m?.content;
91
146
  if (Array.isArray(c)) {
92
147
  for (const part of c) {
93
- if (part && typeof part === 'object' && (part.type === 'image_url' || part.type === 'image'))
148
+ const partType = part?.type;
149
+ if (part &&
150
+ typeof part === "object" &&
151
+ (partType === "image_url" || partType === "image"))
94
152
  count += 1;
95
153
  }
96
154
  }
97
155
  // 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')
156
+ const anyMsg = m;
157
+ if (Array.isArray(anyMsg.images))
158
+ count += anyMsg.images.length;
159
+ if (typeof anyMsg.image_url === "string")
101
160
  count += 1;
102
- if (typeof m?.image === 'string')
161
+ if (typeof anyMsg.image === "string")
103
162
  count += 1;
104
163
  }
105
164
  return count;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisu-ai/mw-usage-tracker",
3
- "version": "9.0.1",
3
+ "version": "9.0.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "access": "public"
13
13
  },
14
14
  "peerDependencies": {
15
- "@sisu-ai/core": "^2.3.1"
15
+ "@sisu-ai/core": "^2.3.2"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",