@meller/tokentalos 1.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.
@@ -0,0 +1,117 @@
1
+ import { getCostCalculator, PRICING_DATA } from './pricing.js';
2
+
3
+ export function runHeuristicAnalysis(usageRecord, variables) {
4
+ const issues = [];
5
+ const suggestions = [];
6
+ const variableAnalysis = [];
7
+ let estimatedSavingsPct = 0;
8
+ let estimatedSavingsUsd = 0;
9
+
10
+ const totalTokens = usageRecord.total_tokens;
11
+ if (totalTokens === 0) return null;
12
+
13
+ // 1. Model Migration Insight (MCE - Model Cascading Efficiency)
14
+ const calculator = getCostCalculator();
15
+ const provider = usageRecord.provider;
16
+ const model = usageRecord.model;
17
+ // Use input/output tokens if available, otherwise estimate 80/20 split
18
+ const inputTokens = usageRecord.input_tokens || Math.floor(totalTokens * 0.8);
19
+ const outputTokens = usageRecord.output_tokens || Math.floor(totalTokens * 0.2);
20
+
21
+ const bestAlt = calculator.getBestAlternative(provider, model, inputTokens, outputTokens);
22
+
23
+ // Check if current model is deprecated
24
+ const currentPricing = PRICING_DATA[provider.toLowerCase()]?.[model.toLowerCase()];
25
+ if (currentPricing?.deprecated) {
26
+ issues.push(`Model Retirement: ${model} is deprecated or retiring soon.`);
27
+ suggestions.push(`Migrate to a current stable model (e.g., Gemini 2.0 Flash) to ensure service continuity.`);
28
+ }
29
+
30
+ let mceResult = {};
31
+ if (bestAlt) {
32
+ const currentCost = usageRecord.total_cost || 0;
33
+ const savingsPct = currentCost > 0 ? ((currentCost - bestAlt.cost) / currentCost) * 100 : 0;
34
+
35
+ if (savingsPct > 10) { // Only suggest if savings are > 10%
36
+ mceResult = {
37
+ mce_best_alternative_model: bestAlt.model,
38
+ mce_best_alternative_provider: bestAlt.provider,
39
+ mce_best_alternative_cost: bestAlt.cost,
40
+ mce_savings_pct: savingsPct
41
+ };
42
+ suggestions.push(`Potential Migration: Switching to ${bestAlt.provider}/${bestAlt.model} could reduce this prompt's cost by ${savingsPct.toFixed(0)}%.`);
43
+ }
44
+ }
45
+
46
+ for (const v of variables) {
47
+ const rawPct = (v.token_count / totalTokens) * 100;
48
+ const pct = Math.min(rawPct, 100); // Cap at 100% for display sanity
49
+
50
+ const vAnalysis = {
51
+ variable_name: v.name,
52
+ token_count: v.token_count,
53
+ percentage: pct,
54
+ waste_reason: null,
55
+ recommendation: null
56
+ };
57
+
58
+ // Parameter Dominance (>75%)
59
+ if (pct > 75) {
60
+ issues.push(`Parameter Dominance: '${v.name}' uses ${pct.toFixed(1)}% of total prompt weight.`);
61
+ vAnalysis.waste_reason = 'Extreme dominance in prompt composition.';
62
+ vAnalysis.recommendation = 'Implement semantic summarization (Action: Dev required).';
63
+ suggestions.push(`Apply semantic compression to '${v.name}'. Summarizing this part could be ~70% effective in reducing total input tokens without context loss.`);
64
+ estimatedSavingsPct += 20;
65
+ }
66
+
67
+ // Tokenizer Discrepancy (e.g. Hebrew/CJK efficiencies)
68
+ if (rawPct > 120) {
69
+ issues.push(`Tokenizer Discrepancy: Local estimate for '${v.name}' is ${rawPct.toFixed(0)}% of reported total.`);
70
+ suggestions.push(`The LLM provider's tokenizer is significantly more efficient for this language than the local fallback. Trust the provider's 'Input Tokens' for billing, but use this breakdown for relative weighting.`);
71
+ }
72
+
73
+ // Context Bloat (>60%)
74
+ if (v.name === 'context' && pct > 60) {
75
+ issues.push(`Context Bloat: RAG/Context takes ${pct.toFixed(1)}% of tokens.`);
76
+ vAnalysis.waste_reason = 'Context size may exceed actual relevance thresholds.';
77
+ vAnalysis.recommendation = 'Implement re-ranking or top-k reduction.';
78
+ suggestions.push(`Use more selective retrieval or re-rank results to reduce context size.`);
79
+ estimatedSavingsPct += 15;
80
+ }
81
+
82
+ // History Bloat (>40%)
83
+ if (v.name.startsWith('history') && pct > 40) {
84
+ issues.push(`History Bloat: Conversation history takes ${pct.toFixed(1)}% of tokens.`);
85
+ vAnalysis.waste_reason = 'Long conversation history causing token pressure.';
86
+ vAnalysis.recommendation = 'Use sliding window or turn summarization.';
87
+ suggestions.push(`Implement a sliding window or summarize old conversation turns.`);
88
+ estimatedSavingsPct += 10;
89
+ }
90
+
91
+ // Whitespace Waste (Heuristic)
92
+ if (v.content && v.content.includes(' ')) {
93
+ const originalLen = v.content.length;
94
+ const cleanedLen = v.content.replace(/\s+/g, ' ').length;
95
+ const wastePct = ((originalLen - cleanedLen) / (originalLen || 1)) * 100;
96
+ if (wastePct > 5) {
97
+ issues.push(`Whitespace Waste: '${v.name}' has ${wastePct.toFixed(1)}% redundant whitespace.`);
98
+ vAnalysis.waste_reason = vAnalysis.waste_reason || 'Excessive whitespace/formatting bloat.';
99
+ vAnalysis.recommendation = vAnalysis.recommendation || 'Enable lossless compression for this project.';
100
+ suggestions.push(`Normalize whitespace in '${v.name}' before sending to LLM.`);
101
+ }
102
+ }
103
+
104
+ variableAnalysis.push(vAnalysis);
105
+ }
106
+
107
+ estimatedSavingsUsd = (usageRecord.total_cost * estimatedSavingsPct) / 100;
108
+
109
+ return {
110
+ ...mceResult,
111
+ detected_issues: issues,
112
+ optimization_suggestions: suggestions,
113
+ variable_analysis: variableAnalysis,
114
+ estimated_savings_pct: Math.min(estimatedSavingsPct, 90),
115
+ estimated_savings_usd: estimatedSavingsUsd
116
+ };
117
+ }
@@ -0,0 +1,30 @@
1
+ import crypto from 'crypto';
2
+
3
+ export class PromptCache {
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+
8
+ generateHash(promptString) {
9
+ return crypto.createHash('sha256').update(promptString).digest('hex');
10
+ }
11
+
12
+ async get(hash) {
13
+ return await this.db.get(`
14
+ SELECT response_content, usage_id, timestamp
15
+ FROM cache_entries
16
+ WHERE prompt_hash = ?
17
+ `, [hash]);
18
+ }
19
+
20
+ async set(hash, responseContent, usageId) {
21
+ return await this.db.run(`
22
+ INSERT INTO cache_entries (prompt_hash, response_content, usage_id)
23
+ VALUES (?, ?, ?)
24
+ ON CONFLICT(prompt_hash) DO UPDATE SET
25
+ response_content = excluded.response_content,
26
+ usage_id = excluded.usage_id,
27
+ timestamp = CURRENT_TIMESTAMP
28
+ `, [hash, responseContent, usageId]);
29
+ }
30
+ }
@@ -0,0 +1,307 @@
1
+ import sqlite3 from 'sqlite3';
2
+ import { open } from 'sqlite';
3
+ import pg from 'pg';
4
+ import chalk from 'chalk';
5
+
6
+ let db;
7
+ let dbType;
8
+ let dbConfig;
9
+
10
+ export async function initDb(config) {
11
+ dbConfig = config;
12
+ dbType = config.databaseType || 'sqlite';
13
+
14
+ if (dbType === 'sqlite') {
15
+ db = await open({
16
+ filename: config.sqlitePath,
17
+ driver: sqlite3.Database,
18
+ });
19
+
20
+ await db.exec(getSchema('sqlite'));
21
+ await runMigrations('sqlite', db);
22
+ console.log(chalk.green(`\nSQLite initialized at ${config.sqlitePath}\n`));
23
+ } else {
24
+ const { Pool } = pg;
25
+ db = new Pool({
26
+ host: config.pgHost,
27
+ port: config.pgPort,
28
+ user: config.pgUser,
29
+ password: config.pgPassword,
30
+ database: config.pgDatabase,
31
+ });
32
+
33
+ const schemaName = config.databaseSchema || 'tokentalos';
34
+ await db.query(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
35
+ await db.query(`SET search_path TO ${schemaName}`);
36
+
37
+ await db.query(getSchema('postgres', schemaName));
38
+ await runMigrations('postgres', db, schemaName);
39
+
40
+ db.on('connect', (client) => {
41
+ client.query(`SET search_path TO ${schemaName}`).catch(err => {
42
+ console.error('[TokenTalos] Error setting search_path on connect:', err);
43
+ });
44
+ });
45
+
46
+ console.log(chalk.green(`\nPostgreSQL initialized at ${config.pgHost}:${config.pgPort} (schema: ${schemaName})\n`));
47
+ }
48
+
49
+ return db;
50
+ }
51
+
52
+ /**
53
+ * Basic migration helper to ensure schema evolution
54
+ */
55
+ async function runMigrations(type, database, schemaName = '') {
56
+ const prefix = (type === 'postgres' && schemaName) ? `${schemaName}.` : '';
57
+
58
+ if (type === 'sqlite') {
59
+ // 1. Add org_id and project_id to usage_data if they don't exist
60
+ const columns = await database.all(`PRAGMA table_info(usage_data)`);
61
+ const hasOrgId = columns.some(c => c.name === 'org_id');
62
+ const hasProjectId = columns.some(c => c.name === 'project_id');
63
+ const hasSavedCost = columns.some(c => c.name === 'saved_cost');
64
+ const hasSavedTokens = columns.some(c => c.name === 'saved_tokens');
65
+ const hasFullPrompt = columns.some(c => c.name === 'full_prompt');
66
+ const hasResponseContent = columns.some(c => c.name === 'response_content');
67
+ const hasType = columns.some(c => c.name === 'type');
68
+
69
+ if (!hasOrgId) await database.exec(`ALTER TABLE usage_data ADD COLUMN org_id TEXT DEFAULT 'default_org'`);
70
+ if (!hasProjectId) await database.exec(`ALTER TABLE usage_data ADD COLUMN project_id TEXT DEFAULT 'default'`);
71
+ if (!hasSavedCost) await database.exec(`ALTER TABLE usage_data ADD COLUMN saved_cost REAL DEFAULT 0`);
72
+ if (!hasSavedTokens) await database.exec(`ALTER TABLE usage_data ADD COLUMN saved_tokens INTEGER DEFAULT 0`);
73
+ if (!hasFullPrompt) await database.exec(`ALTER TABLE usage_data ADD COLUMN full_prompt TEXT`);
74
+ if (!hasResponseContent) await database.exec(`ALTER TABLE usage_data ADD COLUMN response_content TEXT`);
75
+ if (!hasType) await database.exec(`ALTER TABLE usage_data ADD COLUMN type TEXT DEFAULT 'execution'`);
76
+
77
+ // Prompt Variables columns
78
+ const varColumns = await database.all(`PRAGMA table_info(prompt_variables)`);
79
+ const hasOriginalContent = varColumns.some(c => c.name === 'original_content');
80
+ if (!hasOriginalContent) await database.exec(`ALTER TABLE prompt_variables ADD COLUMN original_content TEXT`);
81
+ } else {
82
+ // Postgres migration check
83
+ try {
84
+ // Check if columns exist in usage_data
85
+ const checkSql = `
86
+ SELECT column_name
87
+ FROM information_schema.columns
88
+ WHERE table_schema = '${schemaName || 'tokentalos'}'
89
+ AND table_name = 'usage_data'
90
+ `;
91
+ const { rows } = await database.query(checkSql);
92
+ const colNames = rows.map(r => r.column_name);
93
+
94
+ if (!colNames.includes('org_id')) {
95
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN org_id TEXT DEFAULT 'default_org'`);
96
+ }
97
+ if (!colNames.includes('project_id')) {
98
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN project_id TEXT DEFAULT 'default'`);
99
+ }
100
+ if (!colNames.includes('saved_cost')) {
101
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN saved_cost REAL DEFAULT 0`);
102
+ }
103
+ if (!colNames.includes('saved_tokens')) {
104
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN saved_tokens INTEGER DEFAULT 0`);
105
+ }
106
+ if (!colNames.includes('full_prompt')) {
107
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN full_prompt TEXT`);
108
+ }
109
+ if (!colNames.includes('response_content')) {
110
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN response_content TEXT`);
111
+ }
112
+ if (!colNames.includes('type')) {
113
+ await database.query(`ALTER TABLE ${prefix}usage_data ADD COLUMN type TEXT DEFAULT 'execution'`);
114
+ }
115
+
116
+ // Check prompt_variables
117
+ const varRes = await database.query(`
118
+ SELECT column_name FROM information_schema.columns
119
+ WHERE table_schema = '${schemaName || 'tokentalos'}' AND table_name = 'prompt_variables'
120
+ `);
121
+ const varCols = varRes.rows.map(r => r.column_name);
122
+ if (!varCols.includes('original_content')) {
123
+ await database.query(`ALTER TABLE ${prefix}prompt_variables ADD COLUMN original_content TEXT`);
124
+ }
125
+
126
+ // Check explain_plans
127
+ const planRes = await database.query(`
128
+ SELECT column_name FROM information_schema.columns
129
+ WHERE table_schema = '${schemaName || 'tokentalos'}' AND table_name = 'explain_plans'
130
+ `);
131
+ const planCols = planRes.rows.map(r => r.column_name);
132
+ if (!planCols.includes('variable_analysis')) {
133
+ await database.query(`ALTER TABLE ${prefix}explain_plans ADD COLUMN variable_analysis TEXT`);
134
+ }
135
+ } catch (e) {
136
+ console.warn('[TokenTalos] Migration check failed (Postgres):', e.message);
137
+ }
138
+ }
139
+ }
140
+
141
+ export function getDb() {
142
+ if (!db) throw new Error('Database not initialized');
143
+
144
+ if (dbType === 'sqlite') {
145
+ db.type = 'sqlite';
146
+ return db;
147
+ } else {
148
+ return {
149
+ type: 'postgres',
150
+ run: async (sql, params) => {
151
+ let i = 0;
152
+ const pgSql = sql.replace(/\?/g, () => `$${++i}`);
153
+ return await db.query(pgSql, params);
154
+ },
155
+ get: async (sql, params) => {
156
+ let i = 0;
157
+ const pgSql = sql.replace(/\?/g, () => `$${++i}`);
158
+ const res = await db.query(pgSql, params);
159
+ return res.rows[0];
160
+ },
161
+ all: async (sql, params) => {
162
+ let i = 0;
163
+ const pgSql = sql.replace(/\?/g, () => `$${++i}`);
164
+ const res = await db.query(pgSql, params);
165
+ return res.rows;
166
+ },
167
+ exec: async (sql) => await db.query(sql)
168
+ };
169
+ }
170
+ }
171
+
172
+ function getSchema(type, schemaName = '') {
173
+ const autoInc = type === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'SERIAL PRIMARY KEY';
174
+ const timestamp = type === 'sqlite' ? "TEXT DEFAULT (datetime('now', 'utc'))" : "TIMESTAMP DEFAULT CURRENT_TIMESTAMP";
175
+ const prefix = (type === 'postgres' && schemaName) ? `${schemaName}.` : '';
176
+
177
+ const booleanType = type === 'sqlite' ? 'BOOLEAN' : 'BOOLEAN';
178
+ const booleanDefault0 = type === 'sqlite' ? '0' : 'FALSE';
179
+
180
+ return `
181
+ CREATE TABLE IF NOT EXISTS ${prefix}usage_data (
182
+ id TEXT PRIMARY KEY,
183
+ org_id TEXT DEFAULT 'default_org',
184
+ project_id TEXT DEFAULT 'default',
185
+ type TEXT DEFAULT 'execution', -- 'execution', 'cache_hit'
186
+ provider TEXT NOT NULL,
187
+ model TEXT NOT NULL,
188
+ full_prompt TEXT,
189
+ response_content TEXT,
190
+ input_tokens INTEGER DEFAULT 0,
191
+ output_tokens INTEGER DEFAULT 0,
192
+ total_tokens INTEGER DEFAULT 0,
193
+ saved_tokens INTEGER DEFAULT 0,
194
+ saved_cost REAL DEFAULT 0,
195
+ input_cost REAL DEFAULT 0,
196
+ output_cost REAL DEFAULT 0,
197
+ total_cost REAL DEFAULT 0,
198
+ endpoint TEXT,
199
+ latency_ms REAL,
200
+ token_limit_exceeded ${booleanType} DEFAULT ${booleanDefault0},
201
+ timestamp ${timestamp}
202
+ );
203
+
204
+ CREATE TABLE IF NOT EXISTS ${prefix}organizations (
205
+ id TEXT PRIMARY KEY,
206
+ name TEXT NOT NULL,
207
+ created_at ${timestamp}
208
+ );
209
+
210
+ CREATE TABLE IF NOT EXISTS ${prefix}api_keys (
211
+ key_hash TEXT PRIMARY KEY,
212
+ org_id TEXT NOT NULL,
213
+ name TEXT NOT NULL,
214
+ last_used_at ${timestamp},
215
+ FOREIGN KEY (org_id) REFERENCES ${prefix}organizations (id) ON DELETE CASCADE
216
+ );
217
+
218
+ CREATE TABLE IF NOT EXISTS ${prefix}users (
219
+ id TEXT PRIMARY KEY,
220
+ email TEXT UNIQUE NOT NULL,
221
+ name TEXT,
222
+ created_at ${timestamp}
223
+ );
224
+
225
+ CREATE TABLE IF NOT EXISTS ${prefix}organization_members (
226
+ id ${autoInc},
227
+ org_id TEXT NOT NULL,
228
+ user_id TEXT NOT NULL,
229
+ role TEXT DEFAULT 'viewer',
230
+ created_at ${timestamp},
231
+ FOREIGN KEY (org_id) REFERENCES ${prefix}organizations (id) ON DELETE CASCADE,
232
+ FOREIGN KEY (user_id) REFERENCES ${prefix}users (id) ON DELETE CASCADE
233
+ );
234
+
235
+ CREATE TABLE IF NOT EXISTS ${prefix}prompt_variables (
236
+ id ${autoInc},
237
+ usage_id TEXT NOT NULL,
238
+ name TEXT NOT NULL,
239
+ content TEXT,
240
+ original_content TEXT,
241
+ token_count INTEGER DEFAULT 0,
242
+ char_count INTEGER DEFAULT 0,
243
+ position INTEGER DEFAULT 0${type === 'sqlite' ? '' : `, FOREIGN KEY (usage_id) REFERENCES ${prefix}usage_data (id) ON DELETE CASCADE`}
244
+ );
245
+ ${type === 'sqlite' ? 'CREATE INDEX IF NOT EXISTS idx_variables_usage ON prompt_variables(usage_id);' : ''}
246
+
247
+ CREATE TABLE IF NOT EXISTS ${prefix}security_alerts (
248
+ id ${autoInc},
249
+ usage_id TEXT NOT NULL,
250
+ type TEXT NOT NULL, -- 'injection', 'secret', 'malicious_code'
251
+ description TEXT NOT NULL,
252
+ severity TEXT DEFAULT 'medium', -- 'low', 'medium', 'high', 'critical'
253
+ action_taken TEXT NOT NULL, -- 'warn', 'reject', 'mask'
254
+ timestamp ${timestamp}
255
+ );
256
+
257
+ CREATE TABLE IF NOT EXISTS ${prefix}explain_plans (
258
+ id TEXT PRIMARY KEY,
259
+ usage_id TEXT NOT NULL,
260
+ variable_analysis TEXT, -- JSON array of per-variable insights
261
+ detected_issues TEXT,
262
+ optimization_suggestions TEXT,
263
+ estimated_savings_pct REAL DEFAULT 0,
264
+ estimated_savings_usd REAL DEFAULT 0,
265
+ mce_best_alternative_model TEXT,
266
+ mce_best_alternative_provider TEXT,
267
+ mce_best_alternative_cost REAL,
268
+ mce_savings_pct REAL
269
+ );
270
+
271
+ CREATE TABLE IF NOT EXISTS ${prefix}opv_results (
272
+ id TEXT PRIMARY KEY,
273
+ usage_id TEXT NOT NULL,
274
+ status TEXT,
275
+ confidence REAL,
276
+ reasoning TEXT,
277
+ should_continue ${booleanType},
278
+ verified_at ${timestamp}
279
+ );
280
+
281
+ CREATE TABLE IF NOT EXISTS ${prefix}pii_hits (
282
+ id ${autoInc},
283
+ usage_id TEXT NOT NULL,
284
+ variable_name TEXT NOT NULL,
285
+ pii_type TEXT NOT NULL,
286
+ action_taken TEXT NOT NULL,
287
+ timestamp ${timestamp}
288
+ );
289
+
290
+ CREATE TABLE IF NOT EXISTS ${prefix}variable_actions (
291
+ id ${autoInc},
292
+ usage_id TEXT NOT NULL,
293
+ variable_name TEXT NOT NULL,
294
+ action_type TEXT NOT NULL, -- 'compress', 'pii', 'neutralize', 'owasp'
295
+ action_method TEXT, -- 'mask', 'warn', 'xml_wrap', etc.
296
+ details TEXT, -- JSON string for specific metadata (e.g. saved_chars)
297
+ timestamp ${timestamp}
298
+ );
299
+
300
+ CREATE TABLE IF NOT EXISTS ${prefix}cache_entries (
301
+ prompt_hash TEXT PRIMARY KEY,
302
+ response_content TEXT NOT NULL,
303
+ usage_id TEXT NOT NULL,
304
+ timestamp ${timestamp}
305
+ );
306
+ `;
307
+ }