wolverine-ai 3.5.0 → 3.6.1
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 +5 -0
- package/package.json +1 -1
- package/server/config/settings.json +18 -1
- package/server/lib/gpu-fleet.js +313 -0
- package/server/routes/fleet.js +167 -0
- package/server/routes/inference.js +329 -0
- package/src/agent/agent-engine.js +113 -4
- package/src/brain/brain.js +1 -1
- package/src/brain/embedder.js +1 -1
- package/src/brain/function-map.js +15 -1
- package/src/core/ai-client.js +22 -1
- package/src/core/error-parser.js +2 -2
- package/src/core/models.js +8 -1
- package/src/core/runner.js +29 -3
- package/src/dashboard/server.js +2 -2
- package/src/logger/pricing.js +8 -0
- package/src/logger/token-tracker.js +47 -5
- package/src/monitor/perf-monitor.js +1 -1
- package/src/notifications/notifier.js +1 -1
- package/src/platform/telemetry.js +2 -1
- package/src/security/injection-detector.js +1 -1
package/src/core/runner.js
CHANGED
|
@@ -590,11 +590,37 @@ class WolverineRunner {
|
|
|
590
590
|
this._healStatus = { active: true, route: routePath, error: errorDetails?.message?.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
591
591
|
this.logger.info("heal.error_monitor", `Healing caught 500 on ${routePath}`, { route: routePath });
|
|
592
592
|
|
|
593
|
-
// Build
|
|
593
|
+
// Build synthetic stderr that matches the error parser's expected format
|
|
594
|
+
// If IPC didn't include a file, try to resolve from the route path or stack
|
|
595
|
+
let file = errorDetails.file;
|
|
596
|
+
let line = errorDetails.line || 1;
|
|
597
|
+
if (!file && errorDetails.stack) {
|
|
598
|
+
// Try to find user-land file in stack (not node_modules, not node:)
|
|
599
|
+
const frames = (errorDetails.stack || "").split("\n");
|
|
600
|
+
for (const frame of frames) {
|
|
601
|
+
const m = frame.match(/\(([^)]+):(\d+):(\d+)\)/) || frame.match(/at\s+([^\s(]+):(\d+):(\d+)/);
|
|
602
|
+
if (m && !m[1].includes("node_modules") && !m[1].includes("node:")) {
|
|
603
|
+
file = m[1]; line = parseInt(m[2], 10); break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!file && routePath) {
|
|
608
|
+
// Last resort: map route path to likely file (e.g., /breakable → server/routes/breakable.js)
|
|
609
|
+
const routeName = routePath.split("/").filter(Boolean).pop();
|
|
610
|
+
if (routeName) {
|
|
611
|
+
const path = require("path");
|
|
612
|
+
const guess = path.join(this.cwd, "server", "routes", routeName + ".js");
|
|
613
|
+
if (require("fs").existsSync(guess)) { file = guess; line = 1; }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const msg = errorDetails.message || "Unknown error";
|
|
618
|
+
const hasErrorPrefix = /^\w*Error:/.test(msg);
|
|
594
619
|
const stderr = [
|
|
595
|
-
|
|
620
|
+
file ? `${file}:${line}` : "",
|
|
621
|
+
hasErrorPrefix ? msg : `Error: ${msg}`,
|
|
596
622
|
errorDetails.stack || "",
|
|
597
|
-
|
|
623
|
+
file ? ` at ${file}:${line}:1` : "",
|
|
598
624
|
].filter(Boolean).join("\n");
|
|
599
625
|
|
|
600
626
|
try {
|
package/src/dashboard/server.js
CHANGED
|
@@ -336,7 +336,7 @@ class DashboardServer {
|
|
|
336
336
|
systemPrompt: "Route a command. Respond with two words: ROUTE SIZE.\nROUTE: SIMPLE (general knowledge/explanation, no live data needed), TOOLS (needs live server data, file contents, or endpoint calls), AGENT (create/modify/fix code).\nSIZE: SMALL, MEDIUM, LARGE.\nExamples: 'what is wolverine' → SIMPLE SMALL. 'what time is it' → TOOLS SMALL. 'show me index.js' → TOOLS SMALL. 'add endpoint' → AGENT SMALL. 'build auth' → AGENT LARGE.",
|
|
337
337
|
userPrompt: command,
|
|
338
338
|
maxTokens: 10,
|
|
339
|
-
category: "
|
|
339
|
+
category: "classifier",
|
|
340
340
|
});
|
|
341
341
|
|
|
342
342
|
const raw = (result.content || "").trim().toUpperCase();
|
|
@@ -424,7 +424,7 @@ ${indexContent}
|
|
|
424
424
|
Existing route files:
|
|
425
425
|
${existingRoutes || "(none)"}`,
|
|
426
426
|
maxTokens: 2048,
|
|
427
|
-
category: "
|
|
427
|
+
category: "tool",
|
|
428
428
|
});
|
|
429
429
|
|
|
430
430
|
const raw = (result.content || "").trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
package/src/logger/pricing.js
CHANGED
|
@@ -53,6 +53,14 @@ const DEFAULT_PRICING = {
|
|
|
53
53
|
"claude-3-sonnet": { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
|
|
54
54
|
"claude-3-haiku": { input: 0.25, output: 1.25, cache_write: 0.3125, cache_read: 0.025 },
|
|
55
55
|
|
|
56
|
+
// ── Wolverine Self-Hosted (Gemma 4 via api.wolverinenode.xyz) ──
|
|
57
|
+
// Priced between Anthropic and OpenAI — cheaper than both
|
|
58
|
+
"wolverine-test-1": { input: 0.10, output: 0.40 },
|
|
59
|
+
"wolverine-gemma-26b": { input: 0.25, output: 1.00 },
|
|
60
|
+
"wolverine-gemma-8b": { input: 0.10, output: 0.40 },
|
|
61
|
+
"wolverine-coding": { input: 0.10, output: 0.40 },
|
|
62
|
+
"wolverine-reasoning": { input: 0.25, output: 1.00 },
|
|
63
|
+
|
|
56
64
|
// ── Fallback ──
|
|
57
65
|
"_default": { input: 1.00, output: 4.00 },
|
|
58
66
|
};
|
|
@@ -33,6 +33,8 @@ class TokenTracker {
|
|
|
33
33
|
this._byModel = {};
|
|
34
34
|
// Per-category totals
|
|
35
35
|
this._byCategory = {};
|
|
36
|
+
// Per-model-per-category cross-reference (model::category → stats)
|
|
37
|
+
this._byModelCategory = {};
|
|
36
38
|
// Per-tool totals
|
|
37
39
|
this._byTool = {};
|
|
38
40
|
// Timeline: recent entries for charts (in-memory)
|
|
@@ -87,7 +89,7 @@ class TokenTracker {
|
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
// Accumulate by model
|
|
90
|
-
if (!this._byModel[model]) this._byModel[model] = { input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0, minLatencyMs: Infinity, maxLatencyMs: 0, cacheCreation: 0, cacheRead: 0, cacheSavings: 0 };
|
|
92
|
+
if (!this._byModel[model]) this._byModel[model] = { input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0, totalLatencyTokens: 0, timedCalls: 0, minLatencyMs: Infinity, maxLatencyMs: 0, cacheCreation: 0, cacheRead: 0, cacheSavings: 0 };
|
|
91
93
|
const m = this._byModel[model];
|
|
92
94
|
m.input += entry.input;
|
|
93
95
|
m.output += entry.output;
|
|
@@ -100,6 +102,8 @@ class TokenTracker {
|
|
|
100
102
|
if (entry.success) m.successes++; else m.failures++;
|
|
101
103
|
if (latencyMs > 0) {
|
|
102
104
|
m.totalLatencyMs += latencyMs;
|
|
105
|
+
m.totalLatencyTokens += total;
|
|
106
|
+
m.timedCalls++;
|
|
103
107
|
if (latencyMs < m.minLatencyMs) m.minLatencyMs = latencyMs;
|
|
104
108
|
if (latencyMs > m.maxLatencyMs) m.maxLatencyMs = latencyMs;
|
|
105
109
|
}
|
|
@@ -112,6 +116,18 @@ class TokenTracker {
|
|
|
112
116
|
this._byCategory[category].calls++;
|
|
113
117
|
this._byCategory[category].cost += cost.total;
|
|
114
118
|
|
|
119
|
+
// Accumulate by model+category cross-reference
|
|
120
|
+
const mcKey = `${model}::${category}`;
|
|
121
|
+
if (!this._byModelCategory[mcKey]) this._byModelCategory[mcKey] = { model, category, input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0 };
|
|
122
|
+
const mc = this._byModelCategory[mcKey];
|
|
123
|
+
mc.input += entry.input;
|
|
124
|
+
mc.output += entry.output;
|
|
125
|
+
mc.total += total;
|
|
126
|
+
mc.calls++;
|
|
127
|
+
mc.cost += cost.total;
|
|
128
|
+
if (entry.success) mc.successes++; else mc.failures++;
|
|
129
|
+
if (latencyMs > 0) mc.totalLatencyMs += latencyMs;
|
|
130
|
+
|
|
115
131
|
// Accumulate by tool
|
|
116
132
|
if (tool) {
|
|
117
133
|
const toolKey = tool.split(" ")[0];
|
|
@@ -158,6 +174,7 @@ class TokenTracker {
|
|
|
158
174
|
},
|
|
159
175
|
byModel: this._formatModelStats(),
|
|
160
176
|
byCategory: this._byCategory,
|
|
177
|
+
byModelCategory: this._formatModelCategoryStats(),
|
|
161
178
|
byTool: this._byTool,
|
|
162
179
|
// Recent in-memory timeline
|
|
163
180
|
timeline: this._timeline.slice(-100).map(e => ({
|
|
@@ -188,19 +205,42 @@ class TokenTracker {
|
|
|
188
205
|
cacheCreation: m.cacheCreation || 0,
|
|
189
206
|
cacheRead: m.cacheRead || 0,
|
|
190
207
|
cacheSavings: Math.round((m.cacheSavings || 0) * 1000000) / 1000000,
|
|
191
|
-
successes: m.successes
|
|
208
|
+
successes: m.successes != null ? m.successes : m.calls - (m.failures || 0),
|
|
192
209
|
failures: m.failures || 0,
|
|
193
|
-
successRate: m.calls > 0 ?
|
|
194
|
-
|
|
210
|
+
successRate: m.calls > 0 ? parseFloat((((m.calls - (m.failures || 0)) / m.calls) * 100).toFixed(2)) : 0,
|
|
211
|
+
// Latency normalized by token count
|
|
212
|
+
avgLatencyMs: (m.timedCalls || 0) > 0 ? Math.round(m.totalLatencyMs / m.timedCalls) : 0,
|
|
213
|
+
msPerKToken: (m.totalLatencyTokens || 0) > 0 ? Math.round((m.totalLatencyMs / m.totalLatencyTokens) * 1000) : 0,
|
|
214
|
+
tokensPerSecond: m.totalLatencyMs > 0 ? Math.round((m.totalLatencyTokens || m.total) / (m.totalLatencyMs / 1000) * 10) / 10 : 0,
|
|
215
|
+
outputTokPerSecond: m.totalLatencyMs > 0 && m.output > 0 ? Math.round((m.output / (m.totalLatencyMs / 1000)) * 10) / 10 : 0,
|
|
216
|
+
timedCalls: m.timedCalls || 0,
|
|
195
217
|
minLatencyMs: m.minLatencyMs === Infinity ? 0 : (m.minLatencyMs || 0),
|
|
196
218
|
maxLatencyMs: m.maxLatencyMs || 0,
|
|
197
|
-
tokensPerSecond: m.totalLatencyMs > 0 ? Math.round((m.total / (m.totalLatencyMs / 1000)) * 10) / 10 : 0,
|
|
198
219
|
costPerCall: m.calls > 0 ? Math.round((m.cost / m.calls) * 1000000) / 1000000 : 0,
|
|
199
220
|
};
|
|
200
221
|
}
|
|
201
222
|
return result;
|
|
202
223
|
}
|
|
203
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Format model+category cross-reference for analytics.
|
|
227
|
+
* Returns array of { model, category, calls, cost, tokens, successRate, avgLatencyMs }
|
|
228
|
+
*/
|
|
229
|
+
_formatModelCategoryStats() {
|
|
230
|
+
return Object.values(this._byModelCategory).map(mc => ({
|
|
231
|
+
model: mc.model,
|
|
232
|
+
category: mc.category,
|
|
233
|
+
calls: mc.calls,
|
|
234
|
+
cost: Math.round(mc.cost * 1000000) / 1000000,
|
|
235
|
+
tokens: mc.total,
|
|
236
|
+
input: mc.input,
|
|
237
|
+
output: mc.output,
|
|
238
|
+
successRate: mc.calls > 0 ? parseFloat((((mc.calls - (mc.failures || 0)) / mc.calls) * 100).toFixed(2)) : 100,
|
|
239
|
+
avgLatencyMs: mc.calls > 0 && mc.totalLatencyMs > 0 ? Math.round(mc.totalLatencyMs / mc.calls) : 0,
|
|
240
|
+
tokensPerSecond: mc.totalLatencyMs > 0 ? Math.round((mc.total / (mc.totalLatencyMs / 1000)) * 10) / 10 : 0,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
204
244
|
/**
|
|
205
245
|
* Load full history from JSONL file. For dashboard charts across sessions.
|
|
206
246
|
* @param {number} limit — max entries to return (default: 500)
|
|
@@ -253,6 +293,7 @@ class TokenTracker {
|
|
|
253
293
|
lastSaved: Date.now(),
|
|
254
294
|
byModel: this._byModel,
|
|
255
295
|
byCategory: this._byCategory,
|
|
296
|
+
byModelCategory: this._byModelCategory,
|
|
256
297
|
byTool: this._byTool,
|
|
257
298
|
totalTokens: this._totalTokens,
|
|
258
299
|
totalCalls: this._totalCalls,
|
|
@@ -275,6 +316,7 @@ class TokenTracker {
|
|
|
275
316
|
const data = JSON.parse(fs.readFileSync(this.usagePath, "utf-8"));
|
|
276
317
|
this._byModel = data.byModel || {};
|
|
277
318
|
this._byCategory = data.byCategory || {};
|
|
319
|
+
this._byModelCategory = data.byModelCategory || {};
|
|
278
320
|
this._byTool = data.byTool || {};
|
|
279
321
|
this._totalTokens = data.totalTokens || 0;
|
|
280
322
|
this._totalCalls = data.totalCalls || 0;
|
|
@@ -236,7 +236,7 @@ Provide a brief analysis and actionable suggestions. Focus on:
|
|
|
236
236
|
|
|
237
237
|
Keep your response under 300 words. Be specific and actionable.`,
|
|
238
238
|
maxTokens: 512,
|
|
239
|
-
category: "
|
|
239
|
+
category: "audit",
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
const analysis = result.content;
|
|
@@ -172,7 +172,7 @@ class Notifier {
|
|
|
172
172
|
systemPrompt: "You summarize server errors for developers. Write 1-2 short sentences. Be direct and actionable. Do not include any secrets, passwords, or API key values — only refer to them by name (e.g. 'the OPENAI_API_KEY').",
|
|
173
173
|
userPrompt: `Summarize this error for a developer notification:\n\nCategory: ${classification.category}\nError: ${safeError}\n\nStack (first 300 chars): ${safeStack.slice(0, 300)}`,
|
|
174
174
|
maxTokens: 100,
|
|
175
|
-
category: "
|
|
175
|
+
category: "audit",
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
// Double-sanitize the AI response (in case the AI echoes something)
|
|
@@ -66,7 +66,8 @@ function collectHeartbeat(subsystems) {
|
|
|
66
66
|
totalCalls: tokenTracker?._totalCalls || usage?.session?.totalCalls || 0,
|
|
67
67
|
totalCacheSavings: _sumCacheSavings(usage?.byModel || {}),
|
|
68
68
|
byCategory: usage?.byCategory || {},
|
|
69
|
-
byModel: usage?.byModel || {},
|
|
69
|
+
byModel: usage?.byModel || {},
|
|
70
|
+
byModelCategory: usage?.byModelCategory || [],
|
|
70
71
|
byTool: usage?.byTool || {},
|
|
71
72
|
byProvider: _aggregateByProvider(usage?.byModel || {}),
|
|
72
73
|
},
|