@sohqureshi/tokenwise 1.0.3 → 1.0.7

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
@@ -3,8 +3,8 @@
3
3
  TokenWise is a lightweight utility for preparing JSON before sending it to AI models. It helps reduce payload noise, shrink token usage, and turn structured data into formats that are easier for LLMs to consume.
4
4
 
5
5
 
6
- ![npm version](https://img.shields.io/npm/v/tokenwise)
7
- ![downloads](https://img.shields.io/npm/dw/tokenwise)
6
+ [![npm version](https://img.shields.io/npm/v/%40sohqureshi%2Ftokenwise)](https://www.npmjs.com/package/@sohqureshi/tokenwise)
7
+ [![downloads](https://img.shields.io/npm/dw/%40sohqureshi%2Ftokenwise)](https://www.npmjs.com/package/@sohqureshi/tokenwise)
8
8
  ![license](https://img.shields.io/github/license/sohqureshi/tokenwise)
9
9
  ![stars](https://img.shields.io/github/stars/sohqureshi/tokenwise?style=social)
10
10
  [![Demo](https://img.shields.io/badge/Live%20Demo-Visit-brightgreen)](https://sohqureshi.github.io/tokenwise/)
@@ -36,10 +36,12 @@ Raw JSON is:
36
36
  npm install @sohqureshi/tokenwise
37
37
  ```
38
38
 
39
+ Available on [npm](https://www.npmjs.com/package/@sohqureshi/tokenwise).
40
+
39
41
  ## Quick Usage
40
42
 
41
43
  ```js
42
- import ai, { compact, flatten, toNatural, toTOON } from "tokenwise";
44
+ import ai, { compact, flatten, toNatural, toTOON } from "@sohqureshi/tokenwise";
43
45
 
44
46
  const product = {
45
47
  product: {
@@ -68,7 +70,7 @@ console.log(ai(product).compact().value());
68
70
  Removes fields you do not want to send to the model. By default it also removes `null`, `undefined`, and empty objects.
69
71
 
70
72
  ```js
71
- import { prune } from "tokenwise";
73
+ import { prune } from "@sohqureshi/tokenwise";
72
74
 
73
75
  const input = {
74
76
  user: { name: "John", age: 28 },
@@ -208,14 +210,17 @@ LLMs charge and reason over tokens. Sending raw JSON often includes repeated key
208
210
  node demo.js
209
211
  ```
210
212
 
211
- You can visualize token optimization results using planned CLI/Web visual tools.
213
+ Use `--analyze` to compare serialized input and output with TokenWise's
214
+ four-characters-per-token heuristic. For model-accurate counts, pass
215
+ `exact: true` and a `model` name to `estimateTokens()` or `analyze()`.
216
+ This is still only as exact as the tokenizer implementation you use.
212
217
 
213
218
  ---
214
219
 
215
220
  ## 🚀 Roadmap
216
221
 
217
- * [x] CLI support *(Coming Soon)*
218
- * [ ] NPM Support
222
+ * [x] CLI support
223
+ * [x] NPM Support
219
224
  * [ ] Streaming support (GB+ data)
220
225
  * [ ] Schema-aware optimization
221
226
  * [ ] SaaS API
@@ -259,4 +264,4 @@ If it helps you, consider supporting its development:
259
264
 
260
265
  ## 💡 Vision
261
266
 
262
- Make AI cheaper and faster by optimizing data before it reaches the model.
267
+ Make AI cheaper and faster by optimizing data before it reaches the model.
@@ -14,11 +14,11 @@ function toTOON(data, indent = 0) {
14
14
  }
15
15
  return result;
16
16
  }
17
- return `${space}[${data.length}]: ${data.join(",")}`;
17
+ return `${space}[${data.length}]: ${data.map(formatValue).join(",")}`;
18
18
  }
19
19
  if (typeof data === "object" && data !== null) {
20
20
  let result = "";
21
- for (const key in data) {
21
+ for (const key of Object.keys(data)) {
22
22
  const value = data[key];
23
23
  if (typeof value === "object" && value !== null) {
24
24
  result += `${space}${key}:
@@ -37,7 +37,9 @@ ${toTOON(value, indent + 1)}
37
37
  }
38
38
  function formatValue(val) {
39
39
  if (val === null || val === void 0) return "";
40
- if (typeof val === "string") return val;
40
+ if (typeof val === "string") {
41
+ return val === "" || /[,\n\r]/.test(val) ? JSON.stringify(val) : val;
42
+ }
41
43
  return String(val);
42
44
  }
43
45
  function isPlainObject(value) {
@@ -1,21 +1,27 @@
1
1
  import {
2
2
  toTOON
3
- } from "./chunk-3ECVBELU.js";
3
+ } from "./chunk-6VRLDRJK.js";
4
4
  import {
5
5
  compact
6
- } from "./chunk-EUFLW42L.js";
6
+ } from "./chunk-XE36GLJP.js";
7
7
  import {
8
8
  flatten
9
- } from "./chunk-YMOSWQGE.js";
9
+ } from "./chunk-L7BC62MT.js";
10
10
  import {
11
11
  prune
12
- } from "./chunk-IAV75SZA.js";
12
+ } from "./chunk-ZD536GZF.js";
13
13
  import {
14
- estimateTokens
15
- } from "./chunk-TM6L7YF2.js";
14
+ estimateTokensWithMeta,
15
+ serializeForTokenEstimate
16
+ } from "./chunk-U433WUUT.js";
16
17
 
17
18
  // src/core/analyze.ts
18
19
  function analyze(input, options = {}) {
20
+ const {
21
+ exact = false,
22
+ model = "gpt-4o-mini",
23
+ fallbackToHeuristic = true
24
+ } = options;
19
25
  if (!input || typeof input === "object" && input !== null && Object.keys(input).length === 0) {
20
26
  return {
21
27
  originalTokens: 0,
@@ -23,11 +29,18 @@ function analyze(input, options = {}) {
23
29
  savings: 0,
24
30
  savingsPercent: 0,
25
31
  optimizedData: null,
26
- reductionRatio: 1
32
+ reductionRatio: 1,
33
+ originalCharacters: 0,
34
+ optimizedCharacters: 0,
35
+ estimator: exact ? `exact tokenizer: model=${model}` : "heuristic: 1 token \u2248 4 characters"
27
36
  };
28
37
  }
29
- let originalTokens = estimateTokens(input);
30
- if (isNaN(originalTokens) || !isFinite(originalTokens)) originalTokens = 0;
38
+ const originalMeta = estimateTokensWithMeta(input, {
39
+ exact,
40
+ model,
41
+ fallbackToHeuristic
42
+ });
43
+ const originalTokens = originalMeta.count;
31
44
  let optimizedData = input;
32
45
  if (options.prune && Array.isArray(options.prune)) {
33
46
  optimizedData = prune(optimizedData, options.prune);
@@ -41,18 +54,26 @@ function analyze(input, options = {}) {
41
54
  if (options.toTOON === true || options.toon === true) {
42
55
  optimizedData = toTOON(optimizedData);
43
56
  }
44
- let optimizedTokens = estimateTokens(optimizedData);
45
- if (isNaN(optimizedTokens) || !isFinite(optimizedTokens)) optimizedTokens = 0;
57
+ const optimizedMeta = estimateTokensWithMeta(optimizedData, {
58
+ exact,
59
+ model,
60
+ fallbackToHeuristic
61
+ });
62
+ const optimizedTokens = optimizedMeta.count;
46
63
  const savings = Math.max(0, originalTokens - optimizedTokens);
47
64
  const savingsPercent = originalTokens > 0 ? Math.round(savings / originalTokens * 100) : 0;
48
65
  const reductionRatio = originalTokens > 0 ? optimizedTokens / originalTokens : 1;
66
+ const estimatorLabel = originalMeta && originalMeta.estimator ? originalMeta.estimator : optimizedMeta.estimator;
49
67
  return {
50
68
  originalTokens,
51
69
  optimizedTokens,
52
70
  savings,
53
71
  savingsPercent,
54
72
  optimizedData,
55
- reductionRatio
73
+ reductionRatio,
74
+ originalCharacters: serializeForTokenEstimate(input).length,
75
+ optimizedCharacters: serializeForTokenEstimate(optimizedData).length,
76
+ estimator: estimatorLabel
56
77
  };
57
78
  }
58
79
 
@@ -2,20 +2,28 @@
2
2
  function flatten(obj, prefix = "", res = {}) {
3
3
  if (obj === null || obj === void 0) return res;
4
4
  if (typeof obj !== "object") {
5
- res[prefix] = obj;
5
+ setValue(res, prefix, obj);
6
6
  return res;
7
7
  }
8
- for (const key in obj) {
8
+ for (const key of Object.keys(obj)) {
9
9
  const value = obj[key];
10
10
  const newKey = prefix ? `${prefix}.${key}` : key;
11
11
  if (typeof value === "object" && value !== null) {
12
12
  flatten(value, newKey, res);
13
13
  } else {
14
- res[newKey] = value;
14
+ setValue(res, newKey, value);
15
15
  }
16
16
  }
17
17
  return res;
18
18
  }
19
+ function setValue(res, key, value) {
20
+ if (Object.prototype.hasOwnProperty.call(res, key)) {
21
+ throw new Error(
22
+ `Cannot flatten input: the path "${key}" collides with an existing key. Use keys without dots or rename one of the conflicting properties.`
23
+ );
24
+ }
25
+ res[key] = value;
26
+ }
19
27
 
20
28
  export {
21
29
  flatten
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  analyze
3
- } from "./chunk-GQ62V6ZK.js";
3
+ } from "./chunk-IOMOVRC7.js";
4
4
  import {
5
5
  toTOON
6
- } from "./chunk-3ECVBELU.js";
6
+ } from "./chunk-6VRLDRJK.js";
7
7
  import {
8
8
  compact
9
- } from "./chunk-EUFLW42L.js";
9
+ } from "./chunk-XE36GLJP.js";
10
10
  import {
11
11
  flatten
12
- } from "./chunk-YMOSWQGE.js";
12
+ } from "./chunk-L7BC62MT.js";
13
13
  import {
14
14
  toNatural
15
15
  } from "./chunk-D4CFTFM3.js";
16
16
  import {
17
17
  prune
18
- } from "./chunk-IAV75SZA.js";
18
+ } from "./chunk-ZD536GZF.js";
19
19
 
20
20
  // src/chain.ts
21
21
  var AIChain = class {
@@ -41,8 +41,8 @@ var AIChain = class {
41
41
  toNatural() {
42
42
  return toNatural(this.data);
43
43
  }
44
- analyze() {
45
- return analyze(this.data);
44
+ analyze(options) {
45
+ return analyze(this.data, options);
46
46
  }
47
47
  value() {
48
48
  return this.data;
@@ -0,0 +1,55 @@
1
+ // src/core/token.ts
2
+ import { createRequire } from "module";
3
+ var require2 = createRequire(import.meta.url);
4
+ function serializeForTokenEstimate(value) {
5
+ if (typeof value === "string") return value;
6
+ const serialized = JSON.stringify(value);
7
+ return serialized ?? "";
8
+ }
9
+ function estimateTokensHeuristic(value) {
10
+ return Math.ceil(serializeForTokenEstimate(value).length / 4);
11
+ }
12
+ function estimateTokens(value, options = {}) {
13
+ return estimateTokensWithMeta(value, options).count;
14
+ }
15
+ function estimateTokensWithMeta(value, options = {}) {
16
+ const { exact = false, model = "gpt-4o-mini", fallbackToHeuristic = true } = options;
17
+ const heuristic = estimateTokensHeuristic(value);
18
+ const heuristicEstimator = "heuristic: 1 token \u2248 4 characters";
19
+ if (!exact) {
20
+ return { count: heuristic, estimator: heuristicEstimator };
21
+ }
22
+ try {
23
+ const tiktoken = require2("tiktoken");
24
+ const encoder = tiktoken.encoding_for_model(model);
25
+ const text = serializeForTokenEstimate(value);
26
+ const count = encoder.encode(text).length;
27
+ let encodingName = null;
28
+ if (encoder.name) encodingName = encoder.name;
29
+ if (!encodingName && typeof tiktoken.model_to_encoding === "function") {
30
+ try {
31
+ encodingName = tiktoken.model_to_encoding(model);
32
+ } catch (e) {
33
+ encodingName = null;
34
+ }
35
+ }
36
+ if (!encodingName) {
37
+ const m = String(model || "").toLowerCase();
38
+ if (m.includes("davinci") || m.startsWith("text-")) encodingName = "r50k_base";
39
+ else encodingName = "cl100k_base";
40
+ }
41
+ const estimator = `exact tokenizer: model=${model} encoding=${encodingName}`;
42
+ return { count, estimator };
43
+ } catch (e) {
44
+ if (!fallbackToHeuristic) {
45
+ return { count: heuristic, estimator: "exact requested but tokenizer unavailable" };
46
+ }
47
+ return { count: heuristic, estimator: heuristicEstimator };
48
+ }
49
+ }
50
+
51
+ export {
52
+ serializeForTokenEstimate,
53
+ estimateTokens,
54
+ estimateTokensWithMeta
55
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  prune
3
- } from "./chunk-IAV75SZA.js";
3
+ } from "./chunk-ZD536GZF.js";
4
4
 
5
5
  // src/core/compact.ts
6
6
  function compact(obj) {
@@ -19,7 +19,7 @@ function prune(obj, options = {}) {
19
19
  return arr;
20
20
  }
21
21
  const result = {};
22
- for (const key in obj) {
22
+ for (const key of Object.keys(obj)) {
23
23
  if (removeKeys.includes(key)) continue;
24
24
  const value = prune(obj[key], normalizedOptions);
25
25
  if (value === void 0) continue;
package/dist/cli.cjs CHANGED
@@ -48,7 +48,7 @@ function prune(obj, options = {}) {
48
48
  return arr;
49
49
  }
50
50
  const result = {};
51
- for (const key in obj) {
51
+ for (const key of Object.keys(obj)) {
52
52
  if (removeKeys.includes(key)) continue;
53
53
  const value = prune(obj[key], normalizedOptions);
54
54
  if (value === void 0) continue;
@@ -64,20 +64,28 @@ function prune(obj, options = {}) {
64
64
  function flatten(obj, prefix = "", res = {}) {
65
65
  if (obj === null || obj === void 0) return res;
66
66
  if (typeof obj !== "object") {
67
- res[prefix] = obj;
67
+ setValue(res, prefix, obj);
68
68
  return res;
69
69
  }
70
- for (const key in obj) {
70
+ for (const key of Object.keys(obj)) {
71
71
  const value = obj[key];
72
72
  const newKey = prefix ? `${prefix}.${key}` : key;
73
73
  if (typeof value === "object" && value !== null) {
74
74
  flatten(value, newKey, res);
75
75
  } else {
76
- res[newKey] = value;
76
+ setValue(res, newKey, value);
77
77
  }
78
78
  }
79
79
  return res;
80
80
  }
81
+ function setValue(res, key, value) {
82
+ if (Object.prototype.hasOwnProperty.call(res, key)) {
83
+ throw new Error(
84
+ `Cannot flatten input: the path "${key}" collides with an existing key. Use keys without dots or rename one of the conflicting properties.`
85
+ );
86
+ }
87
+ res[key] = value;
88
+ }
81
89
 
82
90
  // src/core/compact.ts
83
91
  function compact(obj) {
@@ -101,11 +109,11 @@ function toTOON(data, indent = 0) {
101
109
  }
102
110
  return result;
103
111
  }
104
- return `${space}[${data.length}]: ${data.join(",")}`;
112
+ return `${space}[${data.length}]: ${data.map(formatValue).join(",")}`;
105
113
  }
106
114
  if (typeof data === "object" && data !== null) {
107
115
  let result = "";
108
- for (const key in data) {
116
+ for (const key of Object.keys(data)) {
109
117
  const value = data[key];
110
118
  if (typeof value === "object" && value !== null) {
111
119
  result += `${space}${key}:
@@ -124,7 +132,9 @@ ${toTOON(value, indent + 1)}
124
132
  }
125
133
  function formatValue(val) {
126
134
  if (val === null || val === void 0) return "";
127
- if (typeof val === "string") return val;
135
+ if (typeof val === "string") {
136
+ return val === "" || /[,\n\r]/.test(val) ? JSON.stringify(val) : val;
137
+ }
128
138
  return String(val);
129
139
  }
130
140
  function isPlainObject(value) {
@@ -132,13 +142,60 @@ function isPlainObject(value) {
132
142
  }
133
143
 
134
144
  // src/core/token.ts
135
- function estimateTokens(text) {
136
- if (!text) return 0;
137
- return Math.ceil(text.length / 4);
145
+ var import_node_module = require("module");
146
+ var import_meta = {};
147
+ var require2 = (0, import_node_module.createRequire)(import_meta.url);
148
+ function serializeForTokenEstimate(value) {
149
+ if (typeof value === "string") return value;
150
+ const serialized = JSON.stringify(value);
151
+ return serialized ?? "";
152
+ }
153
+ function estimateTokensHeuristic(value) {
154
+ return Math.ceil(serializeForTokenEstimate(value).length / 4);
155
+ }
156
+ function estimateTokensWithMeta(value, options = {}) {
157
+ const { exact = false, model = "gpt-4o-mini", fallbackToHeuristic = true } = options;
158
+ const heuristic = estimateTokensHeuristic(value);
159
+ const heuristicEstimator = "heuristic: 1 token \u2248 4 characters";
160
+ if (!exact) {
161
+ return { count: heuristic, estimator: heuristicEstimator };
162
+ }
163
+ try {
164
+ const tiktoken = require2("tiktoken");
165
+ const encoder = tiktoken.encoding_for_model(model);
166
+ const text = serializeForTokenEstimate(value);
167
+ const count = encoder.encode(text).length;
168
+ let encodingName = null;
169
+ if (encoder.name) encodingName = encoder.name;
170
+ if (!encodingName && typeof tiktoken.model_to_encoding === "function") {
171
+ try {
172
+ encodingName = tiktoken.model_to_encoding(model);
173
+ } catch (e) {
174
+ encodingName = null;
175
+ }
176
+ }
177
+ if (!encodingName) {
178
+ const m = String(model || "").toLowerCase();
179
+ if (m.includes("davinci") || m.startsWith("text-")) encodingName = "r50k_base";
180
+ else encodingName = "cl100k_base";
181
+ }
182
+ const estimator = `exact tokenizer: model=${model} encoding=${encodingName}`;
183
+ return { count, estimator };
184
+ } catch (e) {
185
+ if (!fallbackToHeuristic) {
186
+ return { count: heuristic, estimator: "exact requested but tokenizer unavailable" };
187
+ }
188
+ return { count: heuristic, estimator: heuristicEstimator };
189
+ }
138
190
  }
139
191
 
140
192
  // src/core/analyze.ts
141
193
  function analyze(input, options = {}) {
194
+ const {
195
+ exact = false,
196
+ model = "gpt-4o-mini",
197
+ fallbackToHeuristic = true
198
+ } = options;
142
199
  if (!input || typeof input === "object" && input !== null && Object.keys(input).length === 0) {
143
200
  return {
144
201
  originalTokens: 0,
@@ -146,11 +203,18 @@ function analyze(input, options = {}) {
146
203
  savings: 0,
147
204
  savingsPercent: 0,
148
205
  optimizedData: null,
149
- reductionRatio: 1
206
+ reductionRatio: 1,
207
+ originalCharacters: 0,
208
+ optimizedCharacters: 0,
209
+ estimator: exact ? `exact tokenizer: model=${model}` : "heuristic: 1 token \u2248 4 characters"
150
210
  };
151
211
  }
152
- let originalTokens = estimateTokens(input);
153
- if (isNaN(originalTokens) || !isFinite(originalTokens)) originalTokens = 0;
212
+ const originalMeta = estimateTokensWithMeta(input, {
213
+ exact,
214
+ model,
215
+ fallbackToHeuristic
216
+ });
217
+ const originalTokens = originalMeta.count;
154
218
  let optimizedData = input;
155
219
  if (options.prune && Array.isArray(options.prune)) {
156
220
  optimizedData = prune(optimizedData, options.prune);
@@ -164,18 +228,26 @@ function analyze(input, options = {}) {
164
228
  if (options.toTOON === true || options.toon === true) {
165
229
  optimizedData = toTOON(optimizedData);
166
230
  }
167
- let optimizedTokens = estimateTokens(optimizedData);
168
- if (isNaN(optimizedTokens) || !isFinite(optimizedTokens)) optimizedTokens = 0;
231
+ const optimizedMeta = estimateTokensWithMeta(optimizedData, {
232
+ exact,
233
+ model,
234
+ fallbackToHeuristic
235
+ });
236
+ const optimizedTokens = optimizedMeta.count;
169
237
  const savings = Math.max(0, originalTokens - optimizedTokens);
170
238
  const savingsPercent = originalTokens > 0 ? Math.round(savings / originalTokens * 100) : 0;
171
239
  const reductionRatio = originalTokens > 0 ? optimizedTokens / originalTokens : 1;
240
+ const estimatorLabel = originalMeta && originalMeta.estimator ? originalMeta.estimator : optimizedMeta.estimator;
172
241
  return {
173
242
  originalTokens,
174
243
  optimizedTokens,
175
244
  savings,
176
245
  savingsPercent,
177
246
  optimizedData,
178
- reductionRatio
247
+ reductionRatio,
248
+ originalCharacters: serializeForTokenEstimate(input).length,
249
+ optimizedCharacters: serializeForTokenEstimate(optimizedData).length,
250
+ estimator: estimatorLabel
179
251
  };
180
252
  }
181
253
 
@@ -305,8 +377,8 @@ var AIChain = class {
305
377
  toNatural() {
306
378
  return toNatural(this.data);
307
379
  }
308
- analyze() {
309
- return analyze(this.data);
380
+ analyze(options) {
381
+ return analyze(this.data, options);
310
382
  }
311
383
  value() {
312
384
  return this.data;
@@ -323,7 +395,7 @@ Usage:
323
395
  Options:
324
396
  --toon Convert JSON to TOON format
325
397
  --compact Convert JSON to compact format
326
- --analyze Show token analysis
398
+ --analyze Show token analysis (heuristic estimate)
327
399
  `);
328
400
  process.exit(0);
329
401
  }
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AIChain
4
- } from "./chunk-2IQAGI7Q.js";
5
- import "./chunk-GQ62V6ZK.js";
6
- import "./chunk-3ECVBELU.js";
7
- import "./chunk-EUFLW42L.js";
8
- import "./chunk-YMOSWQGE.js";
4
+ } from "./chunk-P6KXISNT.js";
5
+ import "./chunk-IOMOVRC7.js";
6
+ import "./chunk-6VRLDRJK.js";
7
+ import "./chunk-XE36GLJP.js";
8
+ import "./chunk-L7BC62MT.js";
9
9
  import "./chunk-D4CFTFM3.js";
10
- import "./chunk-IAV75SZA.js";
11
- import "./chunk-TM6L7YF2.js";
10
+ import "./chunk-ZD536GZF.js";
11
+ import "./chunk-U433WUUT.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import fs from "fs";
@@ -22,7 +22,7 @@ Usage:
22
22
  Options:
23
23
  --toon Convert JSON to TOON format
24
24
  --compact Convert JSON to compact format
25
- --analyze Show token analysis
25
+ --analyze Show token analysis (heuristic estimate)
26
26
  `);
27
27
  process.exit(0);
28
28
  }