wolverine-ai 2.0.1 → 2.1.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/package.json +1 -1
- package/server/config/settings.json +23 -11
- package/src/core/ai-client.js +36 -20
- package/src/core/config.js +13 -13
- package/src/logger/token-tracker.js +45 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"env": "development"
|
|
6
6
|
},
|
|
7
7
|
|
|
8
|
-
"provider": "
|
|
8
|
+
"provider": "hybrid",
|
|
9
9
|
|
|
10
|
-
"
|
|
10
|
+
"openai_settings": {
|
|
11
11
|
"reasoning": "gpt-5.4-mini",
|
|
12
12
|
"coding": "gpt-5.1-codex-mini",
|
|
13
13
|
"chat": "gpt-5-nano",
|
|
@@ -19,15 +19,27 @@
|
|
|
19
19
|
"embedding": "text-embedding-3-small"
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
"
|
|
23
|
-
"reasoning": "claude-sonnet-4-
|
|
24
|
-
"coding": "claude-
|
|
25
|
-
"chat": "claude-haiku-4-
|
|
26
|
-
"tool": "claude-
|
|
27
|
-
"classifier": "claude-haiku-4-
|
|
28
|
-
"audit": "claude-haiku-4-
|
|
29
|
-
"compacting": "claude-
|
|
30
|
-
"research": "claude-sonnet-4-
|
|
22
|
+
"anthropic_settings": {
|
|
23
|
+
"reasoning": "claude-sonnet-4-6",
|
|
24
|
+
"coding": "claude-opus-4-6",
|
|
25
|
+
"chat": "claude-haiku-4-5",
|
|
26
|
+
"tool": "claude-opus-4-6",
|
|
27
|
+
"classifier": "claude-haiku-4-5",
|
|
28
|
+
"audit": "claude-haiku-4-5",
|
|
29
|
+
"compacting": "claude-sonnet-4-6",
|
|
30
|
+
"research": "claude-sonnet-4-6",
|
|
31
|
+
"embedding": "text-embedding-3-small"
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"hybrid_settings": {
|
|
35
|
+
"reasoning": "claude-sonnet-4-6",
|
|
36
|
+
"coding": "claude-opus-4-6",
|
|
37
|
+
"chat": "claude-haiku-4-5",
|
|
38
|
+
"tool": "claude-opus-4-6",
|
|
39
|
+
"classifier": "gpt-4o-mini",
|
|
40
|
+
"audit": "gpt-4o-mini",
|
|
41
|
+
"compacting": "claude-sonnet-4-6",
|
|
42
|
+
"research": "claude-sonnet-4-6",
|
|
31
43
|
"embedding": "text-embedding-3-small"
|
|
32
44
|
},
|
|
33
45
|
|
package/src/core/ai-client.js
CHANGED
|
@@ -16,10 +16,10 @@ function _extractTokens(usage) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function _track(model, category, usage, tool) {
|
|
19
|
+
function _track(model, category, usage, tool, latencyMs, success) {
|
|
20
20
|
if (!_tracker) return;
|
|
21
21
|
const { input, output } = _extractTokens(usage);
|
|
22
|
-
_tracker.record(model, category, input, output, tool);
|
|
22
|
+
_tracker.record(model, category, input, output, tool, latencyMs, success);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ── Client Management ──
|
|
@@ -70,34 +70,50 @@ function tokenParam(model, limit) {
|
|
|
70
70
|
|
|
71
71
|
async function aiCall({ model, systemPrompt, userPrompt, maxTokens = 2048, tools, toolChoice, category = "chat", tool }) {
|
|
72
72
|
const provider = detectProvider(model);
|
|
73
|
+
const startMs = Date.now();
|
|
73
74
|
let result;
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
try {
|
|
77
|
+
if (provider === "anthropic") {
|
|
78
|
+
result = await _anthropicCall({ model, systemPrompt, userPrompt, maxTokens, tools, toolChoice });
|
|
79
|
+
} else if (isResponsesModel(model)) {
|
|
80
|
+
result = await _responsesCall(_getOpenAIClient(), { model, systemPrompt, userPrompt, maxTokens, tools });
|
|
81
|
+
} else {
|
|
82
|
+
result = await _chatCall(_getOpenAIClient(), { model, systemPrompt, userPrompt, maxTokens, tools, toolChoice });
|
|
83
|
+
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const latencyMs = Date.now() - startMs;
|
|
86
|
+
_track(model, category, result.usage, tool, latencyMs, true);
|
|
87
|
+
return result;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const latencyMs = Date.now() - startMs;
|
|
90
|
+
_track(model, category, {}, tool, latencyMs, false);
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
async function aiCallWithHistory({ model, messages, tools, maxTokens = 4096, category = "chat", tool }) {
|
|
88
96
|
const provider = detectProvider(model);
|
|
97
|
+
const startMs = Date.now();
|
|
89
98
|
let result;
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
try {
|
|
101
|
+
if (provider === "anthropic") {
|
|
102
|
+
result = await _anthropicCallWithHistory({ model, messages, tools, maxTokens });
|
|
103
|
+
} else if (isResponsesModel(model)) {
|
|
104
|
+
result = await _responsesCallWithHistory(_getOpenAIClient(), { model, messages, tools, maxTokens });
|
|
105
|
+
} else {
|
|
106
|
+
result = await _chatCallWithHistory(_getOpenAIClient(), { model, messages, tools, maxTokens });
|
|
107
|
+
}
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
const latencyMs = Date.now() - startMs;
|
|
110
|
+
_track(model, category, result.usage, tool, latencyMs, true);
|
|
111
|
+
return result;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const latencyMs = Date.now() - startMs;
|
|
114
|
+
_track(model, category, {}, tool, latencyMs, false);
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
// ── Anthropic Implementation ──
|
package/src/core/config.js
CHANGED
|
@@ -26,25 +26,25 @@ function loadConfig() {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Resolve
|
|
29
|
+
// Resolve provider and model set
|
|
30
|
+
// "openai" → openai_settings, "anthropic" → anthropic_settings, "hybrid" → hybrid_settings
|
|
30
31
|
const provider = process.env.WOLVERINE_PROVIDER || fileConfig.provider || "openai";
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
: fileConfig.models;
|
|
32
|
+
const settingsKey = `${provider}_settings`;
|
|
33
|
+
const modelSource = fileConfig[settingsKey] || fileConfig.openai_settings || fileConfig.models || {};
|
|
34
34
|
|
|
35
35
|
_config = {
|
|
36
36
|
provider,
|
|
37
37
|
|
|
38
38
|
models: {
|
|
39
|
-
reasoning: process.env.REASONING_MODEL || modelSource
|
|
40
|
-
coding: process.env.CODING_MODEL || modelSource
|
|
41
|
-
chat: process.env.CHAT_MODEL || modelSource
|
|
42
|
-
tool: process.env.TOOL_MODEL || modelSource
|
|
43
|
-
classifier: process.env.CLASSIFIER_MODEL || modelSource
|
|
44
|
-
audit: process.env.AUDIT_MODEL || modelSource
|
|
45
|
-
compacting: process.env.COMPACTING_MODEL || modelSource
|
|
46
|
-
research: process.env.RESEARCH_MODEL || modelSource
|
|
47
|
-
embedding: process.env.TEXT_EMBEDDING_MODEL || modelSource
|
|
39
|
+
reasoning: process.env.REASONING_MODEL || modelSource.reasoning || "gpt-4o",
|
|
40
|
+
coding: process.env.CODING_MODEL || modelSource.coding || "gpt-4o",
|
|
41
|
+
chat: process.env.CHAT_MODEL || modelSource.chat || "gpt-4o-mini",
|
|
42
|
+
tool: process.env.TOOL_MODEL || modelSource.tool || "gpt-4o-mini",
|
|
43
|
+
classifier: process.env.CLASSIFIER_MODEL || modelSource.classifier || "gpt-4o-mini",
|
|
44
|
+
audit: process.env.AUDIT_MODEL || modelSource.audit || "gpt-4o-mini",
|
|
45
|
+
compacting: process.env.COMPACTING_MODEL || modelSource.compacting || "gpt-4o-mini",
|
|
46
|
+
research: process.env.RESEARCH_MODEL || modelSource.research || "gpt-4o",
|
|
47
|
+
embedding: process.env.TEXT_EMBEDDING_MODEL || modelSource.embedding || "text-embedding-3-small",
|
|
48
48
|
},
|
|
49
49
|
|
|
50
50
|
server: {
|
|
@@ -64,9 +64,8 @@ class TokenTracker {
|
|
|
64
64
|
* @param {number} outputTokens - Completion/output tokens
|
|
65
65
|
* @param {string} tool - Optional tool name (e.g. "call_endpoint /time")
|
|
66
66
|
*/
|
|
67
|
-
record(model, category, inputTokens, outputTokens, tool) {
|
|
67
|
+
record(model, category, inputTokens, outputTokens, tool, latencyMs, success) {
|
|
68
68
|
const total = (inputTokens || 0) + (outputTokens || 0);
|
|
69
|
-
if (total === 0) return;
|
|
70
69
|
|
|
71
70
|
// Calculate USD cost
|
|
72
71
|
const cost = calculateCost(model, inputTokens || 0, outputTokens || 0);
|
|
@@ -78,17 +77,26 @@ class TokenTracker {
|
|
|
78
77
|
input: inputTokens || 0,
|
|
79
78
|
output: outputTokens || 0,
|
|
80
79
|
total,
|
|
81
|
-
cost: Math.round(cost.total * 1000000) / 1000000,
|
|
80
|
+
cost: Math.round(cost.total * 1000000) / 1000000,
|
|
82
81
|
tool: tool || null,
|
|
82
|
+
latencyMs: latencyMs || 0,
|
|
83
|
+
success: success !== false,
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
// Accumulate by model
|
|
86
|
-
if (!this._byModel[model]) this._byModel[model] = { input: 0, output: 0, total: 0, calls: 0, cost: 0 };
|
|
87
|
-
this._byModel[model]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
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 };
|
|
88
|
+
const m = this._byModel[model];
|
|
89
|
+
m.input += entry.input;
|
|
90
|
+
m.output += entry.output;
|
|
91
|
+
m.total += total;
|
|
92
|
+
m.calls++;
|
|
93
|
+
m.cost += cost.total;
|
|
94
|
+
if (entry.success) m.successes++; else m.failures++;
|
|
95
|
+
if (latencyMs > 0) {
|
|
96
|
+
m.totalLatencyMs += latencyMs;
|
|
97
|
+
if (latencyMs < m.minLatencyMs) m.minLatencyMs = latencyMs;
|
|
98
|
+
if (latencyMs > m.maxLatencyMs) m.maxLatencyMs = latencyMs;
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
// Accumulate by category
|
|
94
102
|
if (!this._byCategory[category]) this._byCategory[category] = { input: 0, output: 0, total: 0, calls: 0, cost: 0 };
|
|
@@ -142,7 +150,7 @@ class TokenTracker {
|
|
|
142
150
|
duration: sessionDuration,
|
|
143
151
|
tokensPerMinute,
|
|
144
152
|
},
|
|
145
|
-
byModel: this.
|
|
153
|
+
byModel: this._formatModelStats(),
|
|
146
154
|
byCategory: this._byCategory,
|
|
147
155
|
byTool: this._byTool,
|
|
148
156
|
// Recent in-memory timeline
|
|
@@ -153,10 +161,37 @@ class TokenTracker {
|
|
|
153
161
|
output: e.output,
|
|
154
162
|
cat: e.category,
|
|
155
163
|
model: e.model,
|
|
164
|
+
latencyMs: e.latencyMs || 0,
|
|
165
|
+
success: e.success !== false,
|
|
156
166
|
})),
|
|
157
167
|
};
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Format model stats with computed performance metrics.
|
|
172
|
+
*/
|
|
173
|
+
_formatModelStats() {
|
|
174
|
+
const result = {};
|
|
175
|
+
for (const [model, m] of Object.entries(this._byModel)) {
|
|
176
|
+
result[model] = {
|
|
177
|
+
input: m.input,
|
|
178
|
+
output: m.output,
|
|
179
|
+
total: m.total,
|
|
180
|
+
calls: m.calls,
|
|
181
|
+
cost: m.cost,
|
|
182
|
+
successes: m.successes || m.calls, // backwards compat
|
|
183
|
+
failures: m.failures || 0,
|
|
184
|
+
successRate: m.calls > 0 ? Math.round(((m.successes || m.calls) / m.calls) * 100) : 0,
|
|
185
|
+
avgLatencyMs: m.calls > 0 && m.totalLatencyMs ? Math.round(m.totalLatencyMs / m.calls) : 0,
|
|
186
|
+
minLatencyMs: m.minLatencyMs === Infinity ? 0 : (m.minLatencyMs || 0),
|
|
187
|
+
maxLatencyMs: m.maxLatencyMs || 0,
|
|
188
|
+
tokensPerSecond: m.totalLatencyMs > 0 ? Math.round((m.total / (m.totalLatencyMs / 1000)) * 10) / 10 : 0,
|
|
189
|
+
costPerCall: m.calls > 0 ? Math.round((m.cost / m.calls) * 1000000) / 1000000 : 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
/**
|
|
161
196
|
* Load full history from JSONL file. For dashboard charts across sessions.
|
|
162
197
|
* @param {number} limit — max entries to return (default: 500)
|