@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.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/api/api/v1/analytics.js +153 -0
- package/api/api/v1/opv.js +36 -0
- package/api/api/v1/usage.js +318 -0
- package/api/index.js +111 -0
- package/api/middleware/auth.js +45 -0
- package/api/package.json +38 -0
- package/bin/tokentalos.js +221 -0
- package/index.js +151 -0
- package/lib/engine/ai_analyzer.js +66 -0
- package/lib/engine/analyzer.js +117 -0
- package/lib/engine/cache.js +30 -0
- package/lib/engine/db.js +307 -0
- package/lib/engine/index.js +320 -0
- package/lib/engine/llm_clients.js +255 -0
- package/lib/engine/opv.js +96 -0
- package/lib/engine/parameterizer.js +68 -0
- package/lib/engine/pii_detector.js +73 -0
- package/lib/engine/pricing.js +106 -0
- package/lib/engine/processor.js +157 -0
- package/lib/engine/security.js +101 -0
- package/lib/engine/tokenizers.js +40 -0
- package/package.json +63 -0
package/api/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { initDb } from '../lib/engine/db.js';
|
|
7
|
+
import usageRouter, { setConfig as setUsageConfig } from './api/v1/usage.js';
|
|
8
|
+
import analyticsRouter from './api/v1/analytics.js';
|
|
9
|
+
import opvRouter, { setConfig as setOpvConfig } from './api/v1/opv.js';
|
|
10
|
+
import net from 'net';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
async function isPortInUse(port) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const server = net.createServer()
|
|
18
|
+
.once('error', (err) => {
|
|
19
|
+
if (err.code === 'EADDRINUSE') resolve(true);
|
|
20
|
+
else resolve(false);
|
|
21
|
+
})
|
|
22
|
+
.once('listening', () => {
|
|
23
|
+
server.close();
|
|
24
|
+
resolve(false);
|
|
25
|
+
})
|
|
26
|
+
.listen(port, '0.0.0.0');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function startServer(config) {
|
|
31
|
+
await initDb(config);
|
|
32
|
+
setUsageConfig(config);
|
|
33
|
+
setOpvConfig(config);
|
|
34
|
+
const app = express();
|
|
35
|
+
return createApp(app, config);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createApp(app, config) {
|
|
39
|
+
const isCollectorOnly = config._service === 'collector';
|
|
40
|
+
const isDashboardOnly = config._service === 'dashboard';
|
|
41
|
+
|
|
42
|
+
const PORT = isDashboardOnly ? (config.dashboardPort || 8060) : (config.gatewayPort || 8060);
|
|
43
|
+
const publicPath = path.join(__dirname, 'public');
|
|
44
|
+
|
|
45
|
+
// Store config for middleware access
|
|
46
|
+
app.set('tokentalosConfig', config);
|
|
47
|
+
|
|
48
|
+
app.use(cors());
|
|
49
|
+
app.use(express.json());
|
|
50
|
+
|
|
51
|
+
// --- API Router ---
|
|
52
|
+
// The Dashboard ALWAYS needs the Analytics and Recent Usage APIs to function.
|
|
53
|
+
if (config.enableCollector !== false || config.enableDashboard !== false) {
|
|
54
|
+
const apiRouter = express.Router();
|
|
55
|
+
|
|
56
|
+
// Shared / Analytics Routes (Needed by Dashboard)
|
|
57
|
+
apiRouter.use('/v1/analytics', analyticsRouter);
|
|
58
|
+
|
|
59
|
+
// Usage Router has both Read (recent) and Write (ingest/execute).
|
|
60
|
+
// We mount it for both, as the dashboard needs the 'recent' logs.
|
|
61
|
+
apiRouter.use('/v1/usage', usageRouter);
|
|
62
|
+
apiRouter.use('/v1/opv', opvRouter);
|
|
63
|
+
|
|
64
|
+
apiRouter.get('/', (req, res) => {
|
|
65
|
+
res.json({
|
|
66
|
+
name: isDashboardOnly ? 'TokenTalos Dashboard API' : 'TokenTalos Collector API',
|
|
67
|
+
version: '0.1.0',
|
|
68
|
+
services: {
|
|
69
|
+
collector: !isDashboardOnly && (config.enableCollector !== false),
|
|
70
|
+
dashboard: !isCollectorOnly && (config.enableDashboard !== false)
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
apiRouter.get('/health', (req, res) => {
|
|
76
|
+
res.json({ status: 'ok', service: isDashboardOnly ? 'dashboard' : 'collector' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
app.use('/api', apiRouter);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Dashboard Static Assets ---
|
|
83
|
+
if (config.enableDashboard !== false && !isCollectorOnly) {
|
|
84
|
+
app.use(express.static(publicPath));
|
|
85
|
+
|
|
86
|
+
// Catch-all for SPA (excluding /api)
|
|
87
|
+
app.use((req, res, next) => {
|
|
88
|
+
if (req.path.startsWith('/api')) return next();
|
|
89
|
+
res.sendFile(path.join(publicPath, 'index.html'), (err) => {
|
|
90
|
+
if (err) res.status(404).send('TokenTalos Dashboard build not found.');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
96
|
+
(async () => {
|
|
97
|
+
if (await isPortInUse(PORT)) {
|
|
98
|
+
console.error(chalk.red.bold(`\n❌ Error: Port ${PORT} is already in use.`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
103
|
+
const serviceName = isCollectorOnly ? 'Collector' : (isDashboardOnly ? 'Dashboard' : 'TokenTalos');
|
|
104
|
+
console.log(chalk.cyan.bold(`\n🚀 ${serviceName} running at http://localhost:${PORT}`));
|
|
105
|
+
if (!isDashboardOnly && (config.enableCollector !== false)) console.log(chalk.gray(` API Endpoint: http://localhost:${PORT}/api/v1`));
|
|
106
|
+
});
|
|
107
|
+
})();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return app;
|
|
111
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TokenTalosEngine } from '../../lib/engine/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication Middleware
|
|
5
|
+
*
|
|
6
|
+
* Secure ingestion and execution endpoints.
|
|
7
|
+
* In Managed Mode: Requires valid X-TokenTalos-Key.
|
|
8
|
+
* In Local Mode: Optional key, defaults to default_org.
|
|
9
|
+
*/
|
|
10
|
+
export async function authMiddleware(req, res, next) {
|
|
11
|
+
// Config is passed to the app during startServer
|
|
12
|
+
const config = req.app.get('tokentalosConfig');
|
|
13
|
+
const managedMode = config.managedMode || false;
|
|
14
|
+
const apiKey = req.headers['x-tokentalos-key'];
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const engine = new TokenTalosEngine(config);
|
|
18
|
+
await engine.init();
|
|
19
|
+
|
|
20
|
+
if (apiKey) {
|
|
21
|
+
const orgId = await engine.validateApiKey(apiKey);
|
|
22
|
+
if (orgId) {
|
|
23
|
+
req.orgId = orgId;
|
|
24
|
+
return next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If key provided but invalid
|
|
28
|
+
if (managedMode) {
|
|
29
|
+
return res.status(401).json({ error: 'Invalid API Key' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// No key provided
|
|
34
|
+
if (managedMode) {
|
|
35
|
+
return res.status(401).json({ error: 'API Key required (X-TokenTalos-Key)' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback for Local Mode
|
|
39
|
+
req.orgId = config.orgId || 'default_org';
|
|
40
|
+
next();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('[TokenTalos] Auth Error:', err);
|
|
43
|
+
res.status(500).json({ error: 'Authentication internal error' });
|
|
44
|
+
}
|
|
45
|
+
}
|
package/api/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tokentalos-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node index.js",
|
|
9
|
+
"dev": "nodemon index.js",
|
|
10
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"jest": "^29.0.0",
|
|
17
|
+
"nodemon": "^3.0.0",
|
|
18
|
+
"supertest": "^7.2.2"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@anthropic-ai/sdk": "^0.76.0",
|
|
22
|
+
"@google/generative-ai": "^0.24.1",
|
|
23
|
+
"anthropic": "^0.0.0",
|
|
24
|
+
"axios": "^1.13.5",
|
|
25
|
+
"cli-table3": "^0.6.5",
|
|
26
|
+
"cors": "^2.8.6",
|
|
27
|
+
"dotenv": "^17.3.1",
|
|
28
|
+
"express": "^5.2.1",
|
|
29
|
+
"fs-extra": "^11.3.3",
|
|
30
|
+
"js-tiktoken": "^1.0.21",
|
|
31
|
+
"openai": "^6.22.0",
|
|
32
|
+
"path": "^0.12.7",
|
|
33
|
+
"pg": "^8.18.0",
|
|
34
|
+
"sqlite": "^5.1.1",
|
|
35
|
+
"sqlite3": "^5.1.7",
|
|
36
|
+
"uuid": "^13.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { loadConfig, runSetup } from '../api/setup.js';
|
|
4
|
+
import { startServer } from '../api/index.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { exec, spawn } from 'child_process';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
const getPidFile = (service) => path.join(process.cwd(), `.tokentalos-${service || 'full'}.pid`);
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('tokentalos')
|
|
21
|
+
.description('Standalone LLM Token Usage Analyzer and Proxy')
|
|
22
|
+
.version('0.1.0');
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('setup')
|
|
26
|
+
.description('Run the interactive configuration wizard')
|
|
27
|
+
.action(async () => {
|
|
28
|
+
await runSetup();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('start [service]')
|
|
33
|
+
.description('Start Token Talos services (all, collector, or dashboard)')
|
|
34
|
+
.option('-d, --daemon', 'Run in background', false)
|
|
35
|
+
.action(async (service, options) => {
|
|
36
|
+
const validServices = ['collector', 'dashboard'];
|
|
37
|
+
if (service && !validServices.includes(service)) {
|
|
38
|
+
console.log(chalk.red(`Invalid service: ${service}. Use 'collector', 'dashboard', or leave empty for both.`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let config = await loadConfig();
|
|
43
|
+
if (!config) {
|
|
44
|
+
console.log(chalk.yellow('No configuration found. Starting setup...'));
|
|
45
|
+
config = await runSetup();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Inject service context into config
|
|
49
|
+
if (service) config._service = service;
|
|
50
|
+
|
|
51
|
+
const pidFile = getPidFile(service);
|
|
52
|
+
|
|
53
|
+
if (options.daemon) {
|
|
54
|
+
if (fs.existsSync(pidFile)) {
|
|
55
|
+
console.log(chalk.red(`Token Talos ${service || ''} is already running (PID file exists).`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const logFile = `./tokentalos-${service || 'all'}.log`;
|
|
60
|
+
const out = fs.openSync(logFile, 'a');
|
|
61
|
+
const err = fs.openSync(logFile, 'a');
|
|
62
|
+
|
|
63
|
+
const args = ['start'];
|
|
64
|
+
if (service) args.push(service);
|
|
65
|
+
|
|
66
|
+
const child = spawn('node', [path.join(__dirname, 'tokentalos.js'), ...args], {
|
|
67
|
+
detached: true,
|
|
68
|
+
stdio: ['ignore', out, err]
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(pidFile, child.pid.toString());
|
|
72
|
+
child.unref();
|
|
73
|
+
console.log(chalk.green(`Token Talos ${service || 'services'} started in background (PID: ${child.pid})`));
|
|
74
|
+
console.log(chalk.gray(`Logs: ${logFile}`));
|
|
75
|
+
process.exit(0);
|
|
76
|
+
} else {
|
|
77
|
+
await startServer(config);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('stop [service]')
|
|
83
|
+
.description('Stop Token Talos services (all, collector, or dashboard)')
|
|
84
|
+
.action(async (service) => {
|
|
85
|
+
const config = await loadConfig();
|
|
86
|
+
const pidFile = getPidFile(service);
|
|
87
|
+
|
|
88
|
+
// Determine port based on service
|
|
89
|
+
let port = config?.gatewayPort || 8060;
|
|
90
|
+
if (service === 'dashboard') port = config?.dashboardPort || 8060;
|
|
91
|
+
|
|
92
|
+
let stopped = false;
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(pidFile)) {
|
|
95
|
+
const pid = fs.readFileSync(pidFile, 'utf8');
|
|
96
|
+
try {
|
|
97
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
98
|
+
fs.removeSync(pidFile);
|
|
99
|
+
console.log(chalk.green(`Token Talos ${service || ''} (PID: ${pid}) stopped.`));
|
|
100
|
+
stopped = true;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.log(chalk.red(`Failed to stop Token Talos ${service || ''} via PID: ${err.message}`));
|
|
103
|
+
fs.removeSync(pidFile);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Secondary check: kill by port if PID file didn't work
|
|
108
|
+
if (!stopped) {
|
|
109
|
+
console.log(chalk.gray(`Checking for processes on port ${port}...`));
|
|
110
|
+
try {
|
|
111
|
+
const { stdout } = await new Promise((resolve) => {
|
|
112
|
+
exec(`lsof -t -i:${port}`, (err, stdout) => resolve({ stdout }));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (stdout && stdout.trim()) {
|
|
116
|
+
const pids = stdout.trim().split('\n');
|
|
117
|
+
for (const pidToKill of pids) {
|
|
118
|
+
process.kill(parseInt(pidToKill), 'SIGKILL');
|
|
119
|
+
console.log(chalk.green(`Process on port ${port} (PID: ${pidToKill}) terminated.`));
|
|
120
|
+
}
|
|
121
|
+
stopped = true;
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// Silently ignore if lsof fails (likely no process)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!stopped) {
|
|
129
|
+
console.log(chalk.yellow(`No running Token Talos ${service || 'service'} found.`));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
program
|
|
134
|
+
.command('dashboard')
|
|
135
|
+
.description('Launch the Token Talos Dashboard (Reader Mode)')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
let config = await loadConfig();
|
|
138
|
+
if (!config) {
|
|
139
|
+
console.log(chalk.yellow('No configuration found. Starting setup...'));
|
|
140
|
+
config = await runSetup();
|
|
141
|
+
}
|
|
142
|
+
// Start server in foreground for dashboard access
|
|
143
|
+
console.log(chalk.blue('Launching Dashboard...'));
|
|
144
|
+
await startServer(config);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command('stats')
|
|
149
|
+
.description('Show aggregate token and cost statistics')
|
|
150
|
+
.action(async () => {
|
|
151
|
+
const config = await loadConfig();
|
|
152
|
+
const port = config?.gatewayPort || 8060;
|
|
153
|
+
const API_URL = process.env.TOKENTALOS_API_URL || `http://localhost:${port}/api/v1`;
|
|
154
|
+
try {
|
|
155
|
+
const { data } = await axios.get(`${API_URL}/usage/stats`);
|
|
156
|
+
const table = new Table({ head: [chalk.blue('Metric'), chalk.blue('Value')] });
|
|
157
|
+
table.push(['Total Tokens', data.total_tokens.toLocaleString()]);
|
|
158
|
+
table.push(['Total Cost', `$${data.total_cost.toFixed(4)}`]);
|
|
159
|
+
table.push(['Total Requests', data.total_requests]);
|
|
160
|
+
console.log(table.toString());
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error(chalk.red('Error: Could not connect to API. Is Token Talos running?'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
program
|
|
167
|
+
.command('list')
|
|
168
|
+
.description('List recent prompt logs')
|
|
169
|
+
.action(async () => {
|
|
170
|
+
const config = await loadConfig();
|
|
171
|
+
const port = config?.gatewayPort || 8060;
|
|
172
|
+
const API_URL = process.env.TOKENTALOS_API_URL || `http://localhost:${port}/api/v1`;
|
|
173
|
+
try {
|
|
174
|
+
const { data } = await axios.get(`${API_URL}/usage/recent`);
|
|
175
|
+
const table = new Table({ head: [chalk.blue('ID'), chalk.blue('Model'), chalk.blue('Tokens'), chalk.blue('Cost')] });
|
|
176
|
+
data.forEach(r => table.push([r.id.substring(0, 8), r.model, r.total_tokens, `$${r.total_cost.toFixed(4)}`]));
|
|
177
|
+
console.log(table.toString());
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(chalk.red('Error: Could not connect to API.'));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
program
|
|
184
|
+
.command('export')
|
|
185
|
+
.description('Export usage logs to external formats')
|
|
186
|
+
.option('-f, --format <type>', 'Export format (jsonl, langsmith)', 'jsonl')
|
|
187
|
+
.option('-o, --output <path>', 'Output file path', './tokentalos_export.json')
|
|
188
|
+
.action(async (options) => {
|
|
189
|
+
const config = await loadConfig();
|
|
190
|
+
const port = config?.gatewayPort || 8060;
|
|
191
|
+
const API_URL = process.env.TOKENTALOS_API_URL || `http://localhost:${port}/api/v1`;
|
|
192
|
+
try {
|
|
193
|
+
const { data } = await axios.get(`${API_URL}/usage/recent?limit=1000`);
|
|
194
|
+
|
|
195
|
+
let outputData = '';
|
|
196
|
+
if (options.format === 'jsonl') {
|
|
197
|
+
outputData = data.map(r => JSON.stringify(r)).join('\n');
|
|
198
|
+
} else if (options.format === 'langsmith') {
|
|
199
|
+
// Simple mapping to LangSmith schema
|
|
200
|
+
outputData = JSON.stringify(data.map(r => ({
|
|
201
|
+
name: r.endpoint || 'tokentalos_gateway',
|
|
202
|
+
inputs: r.variables ? Object.fromEntries(r.variables.map(v => [v.name, v.content])) : {},
|
|
203
|
+
outputs: { content: '...' }, // Responses not stored in passive ingest
|
|
204
|
+
usage: { prompt_tokens: r.input_tokens, completion_tokens: r.output_tokens },
|
|
205
|
+
extra: { model: r.model, provider: r.provider }
|
|
206
|
+
})), null, 2);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(options.output, outputData);
|
|
210
|
+
console.log(chalk.green(`Successfully exported ${data.length} records to ${options.output} (${options.format})`));
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error(chalk.red(`Export failed: ${err.message}`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Handle default case
|
|
217
|
+
if (process.argv.length === 2) {
|
|
218
|
+
program.help();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
program.parse(process.argv);
|
package/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { TokenTalosEngine } from './lib/engine/index.js';
|
|
2
|
+
import { TokenTalosPrompt } from './lib/engine/parameterizer.js';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TokenTalos Client SDK
|
|
7
|
+
*
|
|
8
|
+
* Supports both Standalone (direct DB) and Proxy (HTTP) modes.
|
|
9
|
+
*/
|
|
10
|
+
export class TokenTalos {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} [options.mode='standalone'] - 'standalone' or 'proxy'
|
|
14
|
+
* @param {string} [options.apiUrl] - Base URL for proxy mode
|
|
15
|
+
* @param {Object} [options.config] - TokenTalos configuration for standalone mode
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.mode = options.mode || 'standalone';
|
|
19
|
+
this.apiUrl = options.apiUrl || 'http://localhost:8060/api/v1';
|
|
20
|
+
this.reportUrl = options.reportUrl || options.apiUrl || null; // URL to report usage to if in standalone
|
|
21
|
+
this.projectId = options.projectId || 'default';
|
|
22
|
+
this.apiKey = options.apiKey || null;
|
|
23
|
+
|
|
24
|
+
if (this.mode === 'standalone') {
|
|
25
|
+
this.engine = new TokenTalosEngine({
|
|
26
|
+
...options.config,
|
|
27
|
+
projectId: this.projectId
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Internal helper for reporting to collector
|
|
34
|
+
*/
|
|
35
|
+
async _report(usageId, result, params) {
|
|
36
|
+
if (!this.reportUrl || this.mode === 'proxy') return;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Re-map engine result to ingestion format
|
|
40
|
+
const reportData = {
|
|
41
|
+
id: usageId,
|
|
42
|
+
projectId: this.projectId,
|
|
43
|
+
provider: params.provider,
|
|
44
|
+
model: params.model,
|
|
45
|
+
full_prompt: result.full_prompt_string || result.full_prompt || null,
|
|
46
|
+
response_content: result.content || null,
|
|
47
|
+
input_tokens: result.usage?.input_tokens || result.input_tokens || 0,
|
|
48
|
+
output_tokens: result.usage?.output_tokens || result.output_tokens || 0,
|
|
49
|
+
latency_ms: result.metadata?.latency_ms || 0,
|
|
50
|
+
endpoint: params.endpoint || 'sdk_standalone',
|
|
51
|
+
variables: result.variables || [],
|
|
52
|
+
actions_taken: result.metadata?.actions_taken || [],
|
|
53
|
+
timestamp: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await axios.post(`${this.reportUrl}/usage/ingest`, reportData, {
|
|
57
|
+
headers: this._getHeaders()
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.warn('[TokenTalos] Failed to report usage to collector:', err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async init() {
|
|
65
|
+
if (this.mode === 'standalone') {
|
|
66
|
+
await this.engine.init();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Construct and process a prompt without executing
|
|
72
|
+
*/
|
|
73
|
+
async construct(params) {
|
|
74
|
+
if (this.mode === 'standalone') {
|
|
75
|
+
const { processedParts, metadata } = await this.engine.process(params.parts);
|
|
76
|
+
const prompt = this.engine.createPrompt(params.provider, params.model);
|
|
77
|
+
|
|
78
|
+
for (const key in processedParts) {
|
|
79
|
+
if (key === 'system') prompt.addSystem(processedParts[key], params.parts[key]);
|
|
80
|
+
else if (key === 'context') prompt.addContext(processedParts[key], params.parts[key]);
|
|
81
|
+
else if (key === 'history') prompt.addHistory(processedParts[key], params.parts[key]);
|
|
82
|
+
else if (key === 'user_query') prompt.addUserQuery(processedParts[key], params.parts[key]);
|
|
83
|
+
else prompt.add(key, processedParts[key], params.parts[key]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const trackingData = prompt.getTrackingData();
|
|
87
|
+
const result = {
|
|
88
|
+
id: trackingData.id,
|
|
89
|
+
messages: prompt.toMessages(),
|
|
90
|
+
full_prompt_string: prompt.toString(),
|
|
91
|
+
metadata,
|
|
92
|
+
variables: trackingData.variables
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Construction is "passive" ingestion - we report the intent
|
|
96
|
+
await this._report(trackingData.id, { ...result, input_tokens: trackingData.total_tokens }, params);
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
} else {
|
|
100
|
+
const { data } = await axios.post(`${this.apiUrl}/usage/prompt/construct`, {
|
|
101
|
+
projectId: this.projectId,
|
|
102
|
+
...params
|
|
103
|
+
}, { headers: this._getHeaders() });
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute a prompt through the Gateway
|
|
110
|
+
*/
|
|
111
|
+
async execute(params) {
|
|
112
|
+
if (this.mode === 'standalone') {
|
|
113
|
+
const result = await this.engine.execute({
|
|
114
|
+
projectId: this.projectId,
|
|
115
|
+
...params
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Report execution result
|
|
119
|
+
await this._report(result.id, result, params);
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
} else {
|
|
123
|
+
const { data } = await axios.post(`${this.apiUrl}/usage/prompt/execute`, {
|
|
124
|
+
projectId: this.projectId,
|
|
125
|
+
...params
|
|
126
|
+
}, { headers: this._getHeaders() });
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify reasoning chunks (OPV)
|
|
133
|
+
*/
|
|
134
|
+
async verifyReasoning(params) {
|
|
135
|
+
if (this.mode === 'standalone') {
|
|
136
|
+
return await this.engine.verifyReasoning({
|
|
137
|
+
projectId: this.projectId,
|
|
138
|
+
...params
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
const { data } = await axios.post(`${this.apiUrl}/opv/heartbeat`, {
|
|
142
|
+
projectId: this.projectId,
|
|
143
|
+
...params
|
|
144
|
+
}, { headers: this._getHeaders() });
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { TokenTalosPrompt };
|
|
151
|
+
export default TokenTalos;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { getCostCalculator } from './pricing.js';
|
|
3
|
+
|
|
4
|
+
export async function runAIAnalysis(config, usageRecord, variables) {
|
|
5
|
+
if (config.llmProvider !== 'gemini') return null;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
|
|
9
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
|
10
|
+
|
|
11
|
+
const variableInfo = variables.map(v => `${v.name} (${v.token_count} tokens): "${v.content.substring(0, 100)}..."`).join('\n');
|
|
12
|
+
|
|
13
|
+
const prompt = `
|
|
14
|
+
Analyze this LLM prompt structure and suggest optimizations to reduce costs while maintaining performance.
|
|
15
|
+
|
|
16
|
+
Usage Context:
|
|
17
|
+
- Provider: ${usageRecord.provider}
|
|
18
|
+
- Model: ${usageRecord.model}
|
|
19
|
+
- Total Tokens: ${usageRecord.total_tokens}
|
|
20
|
+
- Total Cost: $${usageRecord.total_cost}
|
|
21
|
+
|
|
22
|
+
Variables:
|
|
23
|
+
${variableInfo}
|
|
24
|
+
|
|
25
|
+
Return a JSON object with:
|
|
26
|
+
- detected_issues (array of strings)
|
|
27
|
+
- optimization_suggestions (array of strings)
|
|
28
|
+
- estimated_savings_pct (number)
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const result = await model.generateContent(prompt);
|
|
32
|
+
const response = await result.response;
|
|
33
|
+
const text = response.text();
|
|
34
|
+
|
|
35
|
+
// Attempt to parse JSON from response
|
|
36
|
+
try {
|
|
37
|
+
const jsonStart = text.indexOf('{');
|
|
38
|
+
const jsonEnd = text.lastIndexOf('}') + 1;
|
|
39
|
+
const analysis = JSON.parse(text.substring(jsonStart, jsonEnd));
|
|
40
|
+
|
|
41
|
+
const calculator = getCostCalculator();
|
|
42
|
+
const mceResult = calculator.getBestAlternative(
|
|
43
|
+
usageRecord.provider,
|
|
44
|
+
usageRecord.model,
|
|
45
|
+
usageRecord.input_tokens,
|
|
46
|
+
usageRecord.output_tokens,
|
|
47
|
+
config.comparisonProviders
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (mceResult) {
|
|
51
|
+
analysis.mce_best_alternative_model = mceResult.model;
|
|
52
|
+
analysis.mce_best_alternative_provider = mceResult.provider;
|
|
53
|
+
analysis.mce_best_alternative_cost = mceResult.cost;
|
|
54
|
+
analysis.mce_savings_pct = ((usageRecord.total_cost - mceResult.cost) / usageRecord.total_cost) * 100;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return analysis;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.warn('AI analysis JSON parsing failed:', err);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('AI analysis error:', err);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|