loren-code 0.1.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/src/metrics.js ADDED
@@ -0,0 +1,210 @@
1
+ import logger from './logger.js';
2
+
3
+ // Metriche raccolte
4
+ const metrics = {
5
+ // Contatori
6
+ requests: {
7
+ total: 0,
8
+ byEndpoint: {
9
+ '/api/usage': 0,
10
+ '/dashboard': 0,
11
+ '/v1/models': 0,
12
+ '/v1/messages': 0,
13
+ '/v1/messages/count_tokens': 0,
14
+ '/health': 0,
15
+ '/metrics': 0
16
+ },
17
+ byStatus: {
18
+ '2xx': 0,
19
+ '4xx': 0,
20
+ '5xx': 0
21
+ }
22
+ },
23
+ errors: {
24
+ total: 0,
25
+ byType: {
26
+ validation: 0,
27
+ upstream: 0,
28
+ network: 0,
29
+ other: 0
30
+ }
31
+ },
32
+ // Tempi di risposta
33
+ responseTimes: [],
34
+ // Token usage
35
+ tokens: {
36
+ input: 0,
37
+ output: 0,
38
+ byModel: {}
39
+ },
40
+ // Uptime
41
+ startTime: Date.now(),
42
+ // Connessioni attive
43
+ activeConnections: 0,
44
+ // Cache stats
45
+ cacheStats: {}
46
+ };
47
+
48
+ // Funzioni per incrementare metriche
49
+ export function incrementRequest(endpoint, statusCode) {
50
+ metrics.requests.total++;
51
+ if (metrics.requests.byEndpoint[endpoint] === undefined) {
52
+ metrics.requests.byEndpoint[endpoint] = 0;
53
+ }
54
+ metrics.requests.byEndpoint[endpoint]++;
55
+
56
+ const statusRange = Math.floor(statusCode / 100) + 'xx';
57
+ if (metrics.requests.byStatus[statusRange]) {
58
+ metrics.requests.byStatus[statusRange]++;
59
+ }
60
+ }
61
+
62
+ export function incrementError(type = 'other') {
63
+ metrics.errors.total++;
64
+ if (metrics.errors.byType[type] !== undefined) {
65
+ metrics.errors.byType[type]++;
66
+ }
67
+ }
68
+
69
+ export function recordResponseTime(duration) {
70
+ metrics.responseTimes.push(duration);
71
+ // Mantieni solo gli ultimi 1000 tempi
72
+ if (metrics.responseTimes.length > 1000) {
73
+ metrics.responseTimes.shift();
74
+ }
75
+ }
76
+
77
+ export function recordTokenUsage(model, inputTokens, outputTokens) {
78
+ metrics.tokens.input += inputTokens || 0;
79
+ metrics.tokens.output += outputTokens || 0;
80
+
81
+ if (!metrics.tokens.byModel[model]) {
82
+ metrics.tokens.byModel[model] = { input: 0, output: 0 };
83
+ }
84
+ metrics.tokens.byModel[model].input += inputTokens || 0;
85
+ metrics.tokens.byModel[model].output += outputTokens || 0;
86
+ }
87
+
88
+ export function setActiveConnections(count) {
89
+ metrics.activeConnections = count;
90
+ }
91
+
92
+ export function setCacheStats(stats) {
93
+ metrics.cacheStats = stats;
94
+ }
95
+
96
+ // Calcola statistiche sui tempi di risposta
97
+ function getResponseTimeStats() {
98
+ if (metrics.responseTimes.length === 0) {
99
+ return { avg: 0, min: 0, max: 0, p95: 0 };
100
+ }
101
+
102
+ const sorted = [...metrics.responseTimes].sort((a, b) => a - b);
103
+ const avg = sorted.reduce((a, b) => a + b, 0) / sorted.length;
104
+ const min = sorted[0];
105
+ const max = sorted[sorted.length - 1];
106
+ const p95Index = Math.floor(sorted.length * 0.95);
107
+ const p95 = sorted[p95Index];
108
+
109
+ return { avg: avg.toFixed(2), min, max, p95 };
110
+ }
111
+
112
+ // Ottieni tutte le metriche
113
+ export function getMetrics() {
114
+ const uptime = Date.now() - metrics.startTime;
115
+ const memoryUsage = process.memoryUsage();
116
+ const modelRequests =
117
+ (metrics.requests.byEndpoint['/v1/messages'] || 0) +
118
+ (metrics.requests.byEndpoint['/v1/messages/count_tokens'] || 0) +
119
+ (metrics.requests.byEndpoint['/v1/models'] || 0);
120
+ const internalRequests =
121
+ (metrics.requests.byEndpoint['/health'] || 0) +
122
+ (metrics.requests.byEndpoint['/metrics'] || 0) +
123
+ (metrics.requests.byEndpoint['/api/usage'] || 0) +
124
+ (metrics.requests.byEndpoint['/dashboard'] || 0);
125
+
126
+ return {
127
+ uptime: {
128
+ seconds: Math.floor(uptime / 1000),
129
+ human: formatUptime(uptime)
130
+ },
131
+ process: {
132
+ pid: process.pid,
133
+ version: process.version,
134
+ memory: {
135
+ rss: formatBytes(memoryUsage.rss),
136
+ heapTotal: formatBytes(memoryUsage.heapTotal),
137
+ heapUsed: formatBytes(memoryUsage.heapUsed),
138
+ external: formatBytes(memoryUsage.external)
139
+ }
140
+ },
141
+ requests: {
142
+ ...metrics.requests,
143
+ modelTotal: modelRequests,
144
+ internalTotal: internalRequests,
145
+ rate: {
146
+ perSecond: (metrics.requests.total / (uptime / 1000)).toFixed(2),
147
+ perMinute: ((metrics.requests.total / (uptime / 1000)) * 60).toFixed(2)
148
+ }
149
+ },
150
+ errors: metrics.errors,
151
+ responseTime: getResponseTimeStats(),
152
+ tokens: metrics.tokens,
153
+ activeConnections: metrics.activeConnections,
154
+ cache: metrics.cacheStats
155
+ };
156
+ }
157
+
158
+ // Funzioni di utilità
159
+ function formatUptime(ms) {
160
+ const seconds = Math.floor(ms / 1000);
161
+ const minutes = Math.floor(seconds / 60);
162
+ const hours = Math.floor(minutes / 60);
163
+ const days = Math.floor(hours / 24);
164
+
165
+ if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
166
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
167
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
168
+ return `${seconds}s`;
169
+ }
170
+
171
+ function formatBytes(bytes) {
172
+ if (bytes === 0) return '0 B';
173
+ const k = 1024;
174
+ const sizes = ['B', 'KB', 'MB', 'GB'];
175
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
176
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
177
+ }
178
+
179
+ // Middleware per tracciare richieste
180
+ export function metricsMiddleware(req, res, next) {
181
+ const start = Date.now();
182
+ const endpoint = (() => {
183
+ try {
184
+ return new URL(req.url || '/', 'http://localhost').pathname;
185
+ } catch {
186
+ return req.url || '/';
187
+ }
188
+ })();
189
+
190
+ // Override res.end per catturare la risposta
191
+ const originalEnd = res.end;
192
+ res.end = function(chunk, encoding) {
193
+ const duration = Date.now() - start;
194
+
195
+ // Registra metriche
196
+ incrementRequest(endpoint, res.statusCode);
197
+ recordResponseTime(duration);
198
+
199
+ if (res.statusCode >= 400) {
200
+ const errorType = res.statusCode >= 500 ? 'network' : 'validation';
201
+ incrementError(errorType);
202
+ }
203
+
204
+ // Ripristina il metodo originale
205
+ res.end = originalEnd;
206
+ res.end(chunk, encoding);
207
+ };
208
+
209
+ next();
210
+ }
package/src/schemas.js ADDED
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+
3
+ // Schema per /v1/messages
4
+ export const MessageSchema = z.object({
5
+ model: z.string().min(1, 'Model is required'),
6
+ messages: z.array(z.object({
7
+ role: z.enum(['user', 'assistant'], {
8
+ errorMap: () => ({ message: 'Role must be user or assistant' })
9
+ }),
10
+ content: z.union([
11
+ z.string(),
12
+ z.array(z.object({
13
+ type: z.string(),
14
+ text: z.string().optional(),
15
+ name: z.string().optional(),
16
+ input: z.any().optional(),
17
+ tool_use_id: z.string().optional(),
18
+ content: z.any().optional()
19
+ }))
20
+ ])
21
+ })).min(1, 'At least one message is required'),
22
+ max_tokens: z.number().int().positive().optional(),
23
+ stream: z.boolean().optional(),
24
+ system: z.union([z.string(), z.array(z.any())]).optional(),
25
+ tools: z.array(z.object({
26
+ name: z.string(),
27
+ description: z.string().optional(),
28
+ input_schema: z.any().optional(),
29
+ type: z.string().optional()
30
+ })).optional(),
31
+ thinking: z.any().optional()
32
+ });
33
+
34
+ // Schema per /v1/messages/count_tokens
35
+ export const CountTokensSchema = z.object({
36
+ model: z.string().optional(),
37
+ messages: z.array(z.any()).optional(),
38
+ system: z.union([z.string(), z.array(z.any())]).optional()
39
+ });
40
+
41
+ // Schema per il config
42
+ export const ConfigSchema = z.object({
43
+ host: z.string().default('127.0.0.1'),
44
+ port: z.number().int().min(1).max(65535).default(8788),
45
+ upstreamBaseUrl: z.string().url().default('https://ollama.com'),
46
+ apiKeys: z.array(z.string().min(1)).min(1, 'At least one API key is required'),
47
+ aliases: z.record(z.string()).default({}),
48
+ defaultModel: z.string().default('ollama-free-auto')
49
+ });
50
+
51
+ // Funzione helper per la validazione
52
+ export function validateInput(schema, data) {
53
+ try {
54
+ return schema.parse(data);
55
+ } catch (error) {
56
+ if (error instanceof z.ZodError) {
57
+ const errors = error.issues || error.errors || [];
58
+ const formattedErrors = errors.map(err => ({
59
+ field: err.path ? err.path.join('.') : 'unknown',
60
+ message: err.message
61
+ }));
62
+ throw new Error(`Validation failed: ${formattedErrors.map(e => `${e.field}: ${e.message}`).join(', ')}`);
63
+ }
64
+ throw error;
65
+ }
66
+ }