@aria-framework/ai 0.7.0 → 0.8.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/health.js +34 -3
- package/package.json +1 -1
- package/views/ai/provider-card.ejs +8 -0
package/health.js
CHANGED
|
@@ -172,23 +172,38 @@ function createHealthChecker(opts = {}) {
|
|
|
172
172
|
const cooldownMs = Number(opts.cooldownMs) || 60000;
|
|
173
173
|
const threshold = Math.max(1, Number(opts.failureThreshold) || 3);
|
|
174
174
|
|
|
175
|
-
/** id -> { failures, downUntil, lastGoodAt, lastError, lastMs, lastCheckedAt, modelPresent } */
|
|
176
175
|
const state = new Map();
|
|
177
176
|
const entry = (id) => {
|
|
178
177
|
if (!state.has(id)) {
|
|
179
178
|
state.set(id, {
|
|
180
179
|
failures: 0, downUntil: 0, lastGoodAt: null,
|
|
181
|
-
lastError: null, lastMs: null, lastCheckedAt: null, modelPresent: null
|
|
180
|
+
lastError: null, lastMs: null, lastCheckedAt: null, modelPresent: null,
|
|
181
|
+
// The last few completions, for throughput. A window rather than a single value because
|
|
182
|
+
// one reload spike would otherwise define the number an operator plans capacity from.
|
|
183
|
+
samples: []
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
186
|
return state.get(id);
|
|
185
187
|
};
|
|
186
188
|
|
|
189
|
+
/** How many completions to average throughput over. Small enough to still track a real change. */
|
|
190
|
+
const WINDOW = 10;
|
|
191
|
+
|
|
187
192
|
/** Fold one outcome into the breaker. Exposed because a real CALL is better evidence than a probe. */
|
|
188
193
|
function report(id, ok, info = {}) {
|
|
189
194
|
const e = entry(id);
|
|
190
195
|
e.lastCheckedAt = now();
|
|
191
196
|
if (info.ms != null) e.lastMs = info.ms;
|
|
197
|
+
|
|
198
|
+
// THROUGHPUT, and what it honestly measures: completion tokens divided by the WHOLE call, so
|
|
199
|
+
// it includes prompt processing, queueing and the network. That is not the model's raw
|
|
200
|
+
// generation speed — separating those would need time-to-first-token, which no adapter
|
|
201
|
+
// reports — but it is what the caller actually experienced, which is the number worth planning
|
|
202
|
+
// against. Only completions count; a health probe generates no tokens and would drag it to 0.
|
|
203
|
+
if (ok && info.tokens > 0 && info.ms > 0) {
|
|
204
|
+
e.samples.push({ tokens: info.tokens, ms: info.ms });
|
|
205
|
+
if (e.samples.length > WINDOW) e.samples.shift();
|
|
206
|
+
}
|
|
192
207
|
if (ok) {
|
|
193
208
|
e.failures = 0;
|
|
194
209
|
e.downUntil = 0;
|
|
@@ -267,7 +282,15 @@ function createHealthChecker(opts = {}) {
|
|
|
267
282
|
/** Everything the UI needs for one provider, without probing. */
|
|
268
283
|
status(id) {
|
|
269
284
|
const e = state.get(id);
|
|
270
|
-
|
|
285
|
+
// The same SHAPE for an unseen provider, so a caller never has to tell `undefined` (this key
|
|
286
|
+
// does not exist here) from `null` (we do not know yet) — they mean the same thing to a view.
|
|
287
|
+
if (!e) {
|
|
288
|
+
return {
|
|
289
|
+
id, status: 'unknown', failures: 0, lastGoodAt: null, lastError: null, lastMs: null,
|
|
290
|
+
lastCheckedAt: null, modelPresent: null, tokensPerSec: null, samples: 0,
|
|
291
|
+
cooldownRemainingMs: 0
|
|
292
|
+
};
|
|
293
|
+
}
|
|
271
294
|
const down = !!(e.downUntil && now() < e.downUntil);
|
|
272
295
|
return {
|
|
273
296
|
id,
|
|
@@ -278,6 +301,14 @@ function createHealthChecker(opts = {}) {
|
|
|
278
301
|
lastMs: e.lastMs,
|
|
279
302
|
lastCheckedAt: e.lastCheckedAt,
|
|
280
303
|
modelPresent: e.modelPresent,
|
|
304
|
+
// NULL until something has actually generated tokens. Reporting 0 tok/s for a provider
|
|
305
|
+
// nobody has used yet reads as "it is slow" rather than "we do not know".
|
|
306
|
+
tokensPerSec: e.samples.length
|
|
307
|
+
? Math.round(
|
|
308
|
+
e.samples.reduce((n, x) => n + x.tokens, 0)
|
|
309
|
+
/ (e.samples.reduce((n, x) => n + x.ms, 0) / 1000))
|
|
310
|
+
: null,
|
|
311
|
+
samples: e.samples.length,
|
|
281
312
|
cooldownRemainingMs: down ? e.downUntil - now() : 0
|
|
282
313
|
};
|
|
283
314
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aria-framework/ai",
|
|
3
3
|
"description": "Aria App Framework — AI module. A dependency-injected model seam (createAiClient) over several providers (LM Studio / OpenAI-compatible / Anthropic), with a fact-preservation guard, generic Polish and Generate writing engines, and a browser polish widget. Prompts and config stay in the consuming app.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"publishConfig": {
|
|
@@ -64,6 +64,14 @@
|
|
|
64
64
|
<div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Latency</div>
|
|
65
65
|
<span class="font-monospace"><%= h.lastMs %> ms</span></div>
|
|
66
66
|
<% } %>
|
|
67
|
+
<% if (h.tokensPerSec != null) { %>
|
|
68
|
+
<%# Completion tokens over the WHOLE call, so it includes prompt processing and the
|
|
69
|
+
network — what the caller experienced, not the model's raw generation speed. The
|
|
70
|
+
title says so, because an unqualified "tok/s" invites comparing it to a benchmark. %>
|
|
71
|
+
<div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Throughput</div>
|
|
72
|
+
<span class="font-monospace"
|
|
73
|
+
title="Completion tokens per second of total call time, averaged over the last <%= h.samples %> call<%= h.samples === 1 ? '' : 's' %>. Includes prompt processing and network."><%= h.tokensPerSec %> tok/s</span></div>
|
|
74
|
+
<% } %>
|
|
67
75
|
<% if (typeof usage !== 'undefined' && usage) { %>
|
|
68
76
|
<div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Calls</div>
|
|
69
77
|
<span class="font-monospace"><%= Number(usage.calls || 0).toLocaleString() %></span></div>
|