ctxman 3.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/.ctxman/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
- package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Counting and Formatting Utilities
|
|
3
|
+
* Supports multiple tokenizers: OpenAI (tiktoken), Anthropic, Gemini
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { getTokenizerManager } from './tokenizer-adapter.js';
|
|
8
|
+
|
|
9
|
+
// Load tiktoken for backward compatibility (dynamic import for optional dep)
|
|
10
|
+
let tiktoken = null;
|
|
11
|
+
try {
|
|
12
|
+
const tiktokenModule = await import('tiktoken');
|
|
13
|
+
tiktoken = tiktokenModule.default || tiktokenModule;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
// tiktoken is optional
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Tokenizer manager singleton
|
|
19
|
+
let tokenizerManager = null;
|
|
20
|
+
|
|
21
|
+
export default class TokenUtils {
|
|
22
|
+
/**
|
|
23
|
+
* Calculate tokens using model-specific tokenizer
|
|
24
|
+
* @param {string} content - File content
|
|
25
|
+
* @param {string} modelName - Model name (e.g., 'gpt-5', 'claude-sonnet-4.5', 'gemini-pro')
|
|
26
|
+
* @returns {Promise<number>} Token count
|
|
27
|
+
*/
|
|
28
|
+
static async calculateForModel(content, modelName) {
|
|
29
|
+
if (!tokenizerManager) {
|
|
30
|
+
tokenizerManager = await getTokenizerManager();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return tokenizerManager.countWithTelemetry(content, modelName);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Fallback to estimation
|
|
37
|
+
return this.estimate(content, 'default.txt');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate exact or estimated tokens for content (backward compatible)
|
|
43
|
+
* Uses tiktoken if available, otherwise estimation
|
|
44
|
+
* @param {string} content - File content
|
|
45
|
+
* @param {string} filePath - File path for extension-based estimation
|
|
46
|
+
* @returns {number} Token count
|
|
47
|
+
*/
|
|
48
|
+
static calculate(content, filePath) {
|
|
49
|
+
if (tiktoken) {
|
|
50
|
+
try {
|
|
51
|
+
const encoding = tiktoken.get_encoding('cl100k_base');
|
|
52
|
+
const tokens = encoding.encode(content);
|
|
53
|
+
encoding.free();
|
|
54
|
+
return tokens.length;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return this.estimate(content, filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return this.estimate(content, filePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Estimate tokens based on file type and content length
|
|
64
|
+
* @param {string} content - File content
|
|
65
|
+
* @param {string} filePath - File path for extension detection
|
|
66
|
+
* @returns {number} Estimated token count
|
|
67
|
+
*/
|
|
68
|
+
static estimate(content, filePath) {
|
|
69
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
70
|
+
|
|
71
|
+
const charsPerToken = {
|
|
72
|
+
'.json': 2.5,
|
|
73
|
+
'.js': 3.2,
|
|
74
|
+
'.ts': 3.2,
|
|
75
|
+
'.jsx': 3.2,
|
|
76
|
+
'.tsx': 3.2,
|
|
77
|
+
'.py': 3.0,
|
|
78
|
+
'.java': 3.5,
|
|
79
|
+
'.go': 3.3,
|
|
80
|
+
'.rs': 3.4,
|
|
81
|
+
'.php': 3.1,
|
|
82
|
+
'.rb': 3.0,
|
|
83
|
+
'.swift': 3.3,
|
|
84
|
+
'.kt': 3.5,
|
|
85
|
+
'.cs': 3.5,
|
|
86
|
+
'.cpp': 3.6,
|
|
87
|
+
'.c': 3.6,
|
|
88
|
+
'.h': 3.6,
|
|
89
|
+
'.hpp': 3.6,
|
|
90
|
+
'.scala': 3.5,
|
|
91
|
+
'.md': 3.5,
|
|
92
|
+
'.txt': 4.0,
|
|
93
|
+
'default': 3.5
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const ratio = charsPerToken[ext] || charsPerToken['default'];
|
|
97
|
+
return Math.round(content.length / ratio);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Format token count with K/M suffixes
|
|
102
|
+
* @param {number} tokens - Token count
|
|
103
|
+
* @returns {string} Formatted string
|
|
104
|
+
*/
|
|
105
|
+
static format(tokens) {
|
|
106
|
+
if (tokens >= 1000000) {
|
|
107
|
+
return `${(tokens / 1000000).toFixed(1)}M`;
|
|
108
|
+
} else if (tokens >= 1000) {
|
|
109
|
+
return `${(tokens / 1000).toFixed(1)}K`;
|
|
110
|
+
}
|
|
111
|
+
return tokens.toString();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get token counting method description for a specific model
|
|
116
|
+
* @param {string} modelName - Model name
|
|
117
|
+
* @returns {Promise<string>} Description
|
|
118
|
+
*/
|
|
119
|
+
static async getMethodForModel(modelName) {
|
|
120
|
+
if (!tokenizerManager) {
|
|
121
|
+
tokenizerManager = await getTokenizerManager();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const tokenizer = tokenizerManager.getTokenizerForModel(modelName);
|
|
126
|
+
return `✅ Exact (using ${tokenizer.getName()})`;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return '⚠️ Estimated (~95% accurate)';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get token counting method description (backward compatible)
|
|
134
|
+
* @returns {string} Description
|
|
135
|
+
*/
|
|
136
|
+
static getMethod() {
|
|
137
|
+
return tiktoken ? '✅ Exact (using tiktoken)' : '⚠️ Estimated (~95% accurate)';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if exact token counting is available
|
|
142
|
+
* @returns {boolean} True if tiktoken is available
|
|
143
|
+
*/
|
|
144
|
+
static isExact() {
|
|
145
|
+
return tiktoken !== null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Alias for isExact() for backward compatibility
|
|
150
|
+
* @returns {boolean} True if tiktoken is available
|
|
151
|
+
*/
|
|
152
|
+
static hasExactCounting() {
|
|
153
|
+
return this.isExact();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get list of available tokenizers
|
|
158
|
+
* @returns {Promise<Array>} List of available tokenizers
|
|
159
|
+
*/
|
|
160
|
+
static async getAvailableTokenizers() {
|
|
161
|
+
if (!tokenizerManager) {
|
|
162
|
+
tokenizerManager = await getTokenizerManager();
|
|
163
|
+
}
|
|
164
|
+
return tokenizerManager.getAvailableTokenizers();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detect which tokenizer would be used for a model
|
|
169
|
+
* @param {string} modelName - Model name
|
|
170
|
+
* @returns {Promise<string>} Tokenizer name
|
|
171
|
+
*/
|
|
172
|
+
static async detectTokenizer(modelName) {
|
|
173
|
+
if (!tokenizerManager) {
|
|
174
|
+
tokenizerManager = await getTokenizerManager();
|
|
175
|
+
}
|
|
176
|
+
const tokenizer = tokenizerManager.getTokenizerForModel(modelName);
|
|
177
|
+
return tokenizer.getName();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get telemetry statistics
|
|
182
|
+
* @returns {Promise<Object>} Telemetry data
|
|
183
|
+
*/
|
|
184
|
+
static async getTelemetry() {
|
|
185
|
+
if (!tokenizerManager) {
|
|
186
|
+
tokenizerManager = await getTokenizerManager();
|
|
187
|
+
}
|
|
188
|
+
return tokenizerManager.getTelemetry();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reset telemetry statistics
|
|
193
|
+
*/
|
|
194
|
+
static async resetTelemetry() {
|
|
195
|
+
if (!tokenizerManager) {
|
|
196
|
+
tokenizerManager = await getTokenizerManager();
|
|
197
|
+
}
|
|
198
|
+
tokenizerManager.resetTelemetry();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenizer Adapter System
|
|
3
|
+
* Provides unified interface for different LLM tokenizers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base adapter interface for all tokenizers
|
|
8
|
+
*/
|
|
9
|
+
export class TokenizerAdapter {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.available = false;
|
|
12
|
+
this.name = 'Base';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the tokenizer (load libraries, etc.)
|
|
17
|
+
* @returns {Promise<boolean>} True if initialized successfully
|
|
18
|
+
*/
|
|
19
|
+
async initialize() {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Count tokens in content
|
|
25
|
+
* @param {string} content - Text to count tokens for
|
|
26
|
+
* @returns {number} Token count
|
|
27
|
+
*/
|
|
28
|
+
count(content) {
|
|
29
|
+
throw new Error('count() must be implemented by subclass');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if tokenizer is available
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
isAvailable() {
|
|
37
|
+
return this.available;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get tokenizer name
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
getName() {
|
|
45
|
+
return this.name;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* OpenAI Tokenizer (tiktoken)
|
|
51
|
+
* For GPT-4, GPT-5, o1 models
|
|
52
|
+
*/
|
|
53
|
+
export class TiktokenAdapter extends TokenizerAdapter {
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this.name = 'Tiktoken (OpenAI)';
|
|
57
|
+
this.tiktoken = null;
|
|
58
|
+
this.encoding = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async initialize() {
|
|
62
|
+
try {
|
|
63
|
+
const tiktokenModule = await import('tiktoken');
|
|
64
|
+
this.tiktoken = tiktokenModule.default || tiktokenModule;
|
|
65
|
+
this.available = true;
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
this.available = false;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
count(content) {
|
|
74
|
+
if (!this.available || !this.tiktoken) {
|
|
75
|
+
throw new Error('Tiktoken not available');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Use cl100k_base for GPT-4, GPT-4o, GPT-5
|
|
80
|
+
// Use o200k_base for newer models if needed
|
|
81
|
+
const encoding = this.tiktoken.get_encoding('cl100k_base');
|
|
82
|
+
const tokens = encoding.encode(content);
|
|
83
|
+
encoding.free();
|
|
84
|
+
return tokens.length;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Tiktoken encoding failed: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Anthropic Tokenizer
|
|
93
|
+
* For Claude models (Claude 3, Claude Sonnet, etc.)
|
|
94
|
+
*/
|
|
95
|
+
export class AnthropicAdapter extends TokenizerAdapter {
|
|
96
|
+
constructor() {
|
|
97
|
+
super();
|
|
98
|
+
this.name = 'Anthropic (Claude)';
|
|
99
|
+
this.tokenizer = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async initialize() {
|
|
103
|
+
try {
|
|
104
|
+
const anthropicModule = await import('@anthropic-ai/tokenizer');
|
|
105
|
+
this.tokenizer = anthropicModule.default || anthropicModule;
|
|
106
|
+
this.available = true;
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.available = false;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
count(content) {
|
|
115
|
+
if (!this.available || !this.tokenizer) {
|
|
116
|
+
throw new Error('Anthropic tokenizer not available');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Anthropic tokenizer API
|
|
121
|
+
return this.tokenizer.countTokens(content);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`Anthropic tokenizer failed: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Google Gemini Tokenizer
|
|
130
|
+
* Currently uses estimation (no official tokenizer package)
|
|
131
|
+
*/
|
|
132
|
+
export class GeminiAdapter extends TokenizerAdapter {
|
|
133
|
+
constructor() {
|
|
134
|
+
super();
|
|
135
|
+
this.name = 'Gemini (Estimation)';
|
|
136
|
+
this.available = true; // Always available (estimation)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async initialize() {
|
|
140
|
+
this.available = true;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
count(content) {
|
|
145
|
+
// Gemini uses similar tokenization to GPT models
|
|
146
|
+
// Estimation: ~3.5 chars per token for English text
|
|
147
|
+
return Math.round(content.length / 3.5);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* DeepSeek Tokenizer
|
|
153
|
+
* Uses tiktoken-compatible encoding (OpenAI-compatible API)
|
|
154
|
+
*/
|
|
155
|
+
export class DeepSeekAdapter extends TokenizerAdapter {
|
|
156
|
+
constructor() {
|
|
157
|
+
super();
|
|
158
|
+
this.name = 'DeepSeek (tiktoken)';
|
|
159
|
+
this.tiktokenAdapter = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async initialize() {
|
|
163
|
+
try {
|
|
164
|
+
this.tiktokenAdapter = new TiktokenAdapter();
|
|
165
|
+
await this.tiktokenAdapter.initialize();
|
|
166
|
+
this.available = this.tiktokenAdapter.isAvailable();
|
|
167
|
+
return this.available;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.available = false;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
count(content) {
|
|
175
|
+
if (!this.available || !this.tiktokenAdapter) {
|
|
176
|
+
// Fallback to estimation
|
|
177
|
+
return Math.round(content.length / 3.5);
|
|
178
|
+
}
|
|
179
|
+
return this.tiktokenAdapter.count(content);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Llama Tokenizer
|
|
185
|
+
* For Llama 3.x models
|
|
186
|
+
*/
|
|
187
|
+
export class LlamaAdapter extends TokenizerAdapter {
|
|
188
|
+
constructor() {
|
|
189
|
+
super();
|
|
190
|
+
this.name = 'Llama 3 (llama3-tokenizer-js)';
|
|
191
|
+
this.tokenizer = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async initialize() {
|
|
195
|
+
try {
|
|
196
|
+
const llamaModule = await import('llama3-tokenizer-js');
|
|
197
|
+
this.tokenizer = llamaModule.default || llamaModule;
|
|
198
|
+
this.available = true;
|
|
199
|
+
return true;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.available = false;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
count(content) {
|
|
207
|
+
if (!this.available || !this.tokenizer) {
|
|
208
|
+
throw new Error('Llama tokenizer not available');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
// llama3-tokenizer-js API
|
|
213
|
+
const tokens = this.tokenizer.encode(content);
|
|
214
|
+
return tokens.length;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(`Llama tokenizer failed: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Estimation-based tokenizer (fallback)
|
|
223
|
+
* Uses character count heuristics
|
|
224
|
+
*/
|
|
225
|
+
export class EstimationAdapter extends TokenizerAdapter {
|
|
226
|
+
constructor() {
|
|
227
|
+
super();
|
|
228
|
+
this.name = 'Estimation (Fallback)';
|
|
229
|
+
this.available = true; // Always available
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async initialize() {
|
|
233
|
+
this.available = true;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
count(content) {
|
|
238
|
+
// Conservative estimation: ~3.5 chars per token
|
|
239
|
+
// Works reasonably well for most languages
|
|
240
|
+
return Math.round(content.length / 3.5);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Telemetry tracker for tokenizer usage
|
|
246
|
+
*/
|
|
247
|
+
class TokenizerTelemetry {
|
|
248
|
+
constructor() {
|
|
249
|
+
this.stats = {
|
|
250
|
+
totalCalls: 0,
|
|
251
|
+
byTokenizer: {},
|
|
252
|
+
byModel: {},
|
|
253
|
+
totalTokens: 0,
|
|
254
|
+
errors: 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
track(tokenizerName, modelName, tokens, error = null) {
|
|
259
|
+
this.stats.totalCalls++;
|
|
260
|
+
|
|
261
|
+
if (error) {
|
|
262
|
+
this.stats.errors++;
|
|
263
|
+
} else {
|
|
264
|
+
// Track by tokenizer
|
|
265
|
+
if (!this.stats.byTokenizer[tokenizerName]) {
|
|
266
|
+
this.stats.byTokenizer[tokenizerName] = {
|
|
267
|
+
calls: 0,
|
|
268
|
+
tokens: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
this.stats.byTokenizer[tokenizerName].calls++;
|
|
272
|
+
this.stats.byTokenizer[tokenizerName].tokens += tokens;
|
|
273
|
+
|
|
274
|
+
// Track by model
|
|
275
|
+
if (!this.stats.byModel[modelName]) {
|
|
276
|
+
this.stats.byModel[modelName] = {
|
|
277
|
+
calls: 0,
|
|
278
|
+
tokens: 0,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
this.stats.byModel[modelName].calls++;
|
|
282
|
+
this.stats.byModel[modelName].tokens += tokens;
|
|
283
|
+
|
|
284
|
+
this.stats.totalTokens += tokens;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getStats() {
|
|
289
|
+
return { ...this.stats };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
reset() {
|
|
293
|
+
this.stats = {
|
|
294
|
+
totalCalls: 0,
|
|
295
|
+
byTokenizer: {},
|
|
296
|
+
byModel: {},
|
|
297
|
+
totalTokens: 0,
|
|
298
|
+
errors: 0,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Tokenizer factory and manager with telemetry
|
|
305
|
+
*/
|
|
306
|
+
export class TokenizerManager {
|
|
307
|
+
constructor(options = {}) {
|
|
308
|
+
this.adapters = new Map();
|
|
309
|
+
this.initialized = false;
|
|
310
|
+
this.telemetry = new TokenizerTelemetry();
|
|
311
|
+
this.enableTelemetry = options.enableTelemetry !== false; // Default: enabled
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Initialize all available tokenizers
|
|
316
|
+
*/
|
|
317
|
+
async initialize() {
|
|
318
|
+
if (this.initialized) return;
|
|
319
|
+
|
|
320
|
+
const adapters = [
|
|
321
|
+
{ key: 'tiktoken', adapter: new TiktokenAdapter() },
|
|
322
|
+
{ key: 'anthropic', adapter: new AnthropicAdapter() },
|
|
323
|
+
{ key: 'gemini', adapter: new GeminiAdapter() },
|
|
324
|
+
{ key: 'deepseek', adapter: new DeepSeekAdapter() },
|
|
325
|
+
{ key: 'llama', adapter: new LlamaAdapter() },
|
|
326
|
+
{ key: 'estimation', adapter: new EstimationAdapter() },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
for (const { key, adapter } of adapters) {
|
|
330
|
+
await adapter.initialize();
|
|
331
|
+
this.adapters.set(key, adapter);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.initialized = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get tokenizer for specific model with telemetry
|
|
339
|
+
* @param {string} modelName - Model name
|
|
340
|
+
* @returns {TokenizerAdapter}
|
|
341
|
+
*/
|
|
342
|
+
getTokenizerForModel(modelName) {
|
|
343
|
+
if (!this.initialized) {
|
|
344
|
+
throw new Error('TokenizerManager not initialized. Call initialize() first.');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const lowerModel = modelName.toLowerCase();
|
|
348
|
+
|
|
349
|
+
// OpenAI models
|
|
350
|
+
if (lowerModel.includes('gpt') || lowerModel.includes('o1') || lowerModel.includes('o3')) {
|
|
351
|
+
const tiktoken = this.adapters.get('tiktoken');
|
|
352
|
+
if (tiktoken.isAvailable()) return tiktoken;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Anthropic models
|
|
356
|
+
if (lowerModel.includes('claude')) {
|
|
357
|
+
const anthropic = this.adapters.get('anthropic');
|
|
358
|
+
if (anthropic.isAvailable()) return anthropic;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Google models
|
|
362
|
+
if (lowerModel.includes('gemini')) {
|
|
363
|
+
return this.adapters.get('gemini');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// DeepSeek models
|
|
367
|
+
if (lowerModel.includes('deepseek')) {
|
|
368
|
+
const deepseek = this.adapters.get('deepseek');
|
|
369
|
+
if (deepseek.isAvailable()) return deepseek;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Llama models
|
|
373
|
+
if (lowerModel.includes('llama')) {
|
|
374
|
+
const llama = this.adapters.get('llama');
|
|
375
|
+
if (llama.isAvailable()) return llama;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Fallback to estimation
|
|
379
|
+
return this.adapters.get('estimation');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Count tokens with telemetry tracking
|
|
384
|
+
* @param {string} content - Content to tokenize
|
|
385
|
+
* @param {string} modelName - Model name
|
|
386
|
+
* @returns {number} Token count
|
|
387
|
+
*/
|
|
388
|
+
countWithTelemetry(content, modelName) {
|
|
389
|
+
const tokenizer = this.getTokenizerForModel(modelName);
|
|
390
|
+
let tokens = 0;
|
|
391
|
+
let error = null;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
tokens = tokenizer.count(content);
|
|
395
|
+
if (this.enableTelemetry) {
|
|
396
|
+
this.telemetry.track(tokenizer.getName(), modelName, tokens);
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
error = err;
|
|
400
|
+
if (this.enableTelemetry) {
|
|
401
|
+
this.telemetry.track(tokenizer.getName(), modelName, 0, err);
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return tokens;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get telemetry stats
|
|
411
|
+
* @returns {Object} Telemetry statistics
|
|
412
|
+
*/
|
|
413
|
+
getTelemetry() {
|
|
414
|
+
return this.telemetry.getStats();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Reset telemetry
|
|
419
|
+
*/
|
|
420
|
+
resetTelemetry() {
|
|
421
|
+
this.telemetry.reset();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get all available tokenizers
|
|
426
|
+
* @returns {Array<{key: string, name: string, available: boolean}>}
|
|
427
|
+
*/
|
|
428
|
+
getAvailableTokenizers() {
|
|
429
|
+
const result = [];
|
|
430
|
+
for (const [key, adapter] of this.adapters.entries()) {
|
|
431
|
+
result.push({
|
|
432
|
+
key,
|
|
433
|
+
name: adapter.getName(),
|
|
434
|
+
available: adapter.isAvailable(),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Singleton instance
|
|
442
|
+
let managerInstance = null;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get singleton tokenizer manager instance
|
|
446
|
+
* @returns {Promise<TokenizerManager>}
|
|
447
|
+
*/
|
|
448
|
+
export async function getTokenizerManager() {
|
|
449
|
+
if (!managerInstance) {
|
|
450
|
+
managerInstance = new TokenizerManager();
|
|
451
|
+
await managerInstance.initialize();
|
|
452
|
+
}
|
|
453
|
+
return managerInstance;
|
|
454
|
+
}
|