@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 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
- if (!e) return { id, status: 'unknown', failures: 0 };
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.7.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>