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/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/package.json +70 -0
- package/scripts/ClaudeWrapperLauncher.cs +78 -0
- package/scripts/claude-wrapper.js +216 -0
- package/scripts/install-claude-ollama.ps1 +184 -0
- package/scripts/loren.js +515 -0
- package/scripts/uninstall-claude-ollama.ps1 +73 -0
- package/src/bootstrap.js +30 -0
- package/src/cache.js +64 -0
- package/src/config-watcher.js +73 -0
- package/src/config.js +98 -0
- package/src/http-agents.js +80 -0
- package/src/key-manager.js +69 -0
- package/src/logger.js +46 -0
- package/src/metrics.js +210 -0
- package/src/schemas.js +66 -0
- package/src/server.js +1238 -0
- package/src/usage-tracker.js +346 -0
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
|
+
}
|