@aria-framework/ai 0.9.0 → 0.10.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/error.js CHANGED
@@ -18,13 +18,18 @@ class AiError extends Error {
18
18
  /**
19
19
  * @param {AiErrorKind} kind
20
20
  * @param {string} message written for the admin screen, not the log
21
- * @param {{cause?: Error, status?: number}} [opts]
21
+ * @param {{cause?: Error, status?: number, usage?: object}} [opts]
22
22
  */
23
23
  constructor(kind, message, opts = {}) {
24
24
  super(message);
25
25
  this.name = 'AiError';
26
26
  this.kind = kind;
27
27
  this.status = opts.status || null;
28
+ // WHAT IT COST BEFORE IT FAILED, when the caller knows. A model that spends its whole reply
29
+ // budget on internal reasoning produces no answer — but it generated tokens, and something
30
+ // measuring throughput should not have to treat that as zero work. Optional everywhere: most
31
+ // failures happen before a single token is spent.
32
+ if (opts.usage) this.usage = opts.usage;
28
33
  if (opts.cause) this.cause = opts.cause;
29
34
  }
30
35
 
package/index.js CHANGED
@@ -176,6 +176,15 @@ function createAiClient(deps = {}) {
176
176
  }
177
177
  }
178
178
 
179
+ /**
180
+ * A fixed-workload speed test for one endpoint. See benchmark.js for why the workload is fixed
181
+ * rather than the prompt, and why the warm-up is reported rather than discarded.
182
+ */
183
+ async function benchmarkEndpoint(cfgOverride, benchOpts) {
184
+ const cfg = cfgOverride || await resolveConfig();
185
+ return require('./benchmark').benchmark(complete, cfg, benchOpts || {});
186
+ }
187
+
179
188
  // The writing-assist engines are bound to this client's complete() so a caller gets config +
180
189
  // budget + retry for free. Prompt framing is supplied per call by the app (content).
181
190
  const boundPolish = (opts) => polish(complete, opts);
@@ -183,6 +192,7 @@ function createAiClient(deps = {}) {
183
192
 
184
193
  return {
185
194
  complete, isEnabled, test, listModels, listModelsResult, withOneRetry,
195
+ benchmark: benchmarkEndpoint,
186
196
  polish: boundPolish, generate: boundGenerate,
187
197
  facts, AiError, PROVIDERS, DEFAULTS
188
198
  };
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.9.0",
4
+ "version": "0.10.0",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -25,6 +25,6 @@
25
25
  "@aria-framework/db-worker": { "optional": true }
26
26
  },
27
27
  "scripts": {
28
- "test": "node test/smoke.js && node test/usageStore.js && node test/providerStore.js && node test/health.js && node test/listModels.js && node test/views.js"
28
+ "test": "node test/smoke.js && node test/usageStore.js && node test/providerStore.js && node test/health.js && node test/listModels.js && node test/benchmark.js && node test/views.js"
29
29
  }
30
30
  }
@@ -209,11 +209,13 @@ function emptyCompletion({ label, finishReason, reasoning, maxTokens, usage }) {
209
209
  return new AiError('bad_response',
210
210
  `${label} ran out of room before it answered — it used all ${maxTokens} reply tokens` +
211
211
  (reasoning ? ' on internal reasoning' : '') +
212
- '. This model thinks before it replies, so raise the reply limit (1024 is a sensible floor).');
212
+ '. This model thinks before it replies, so raise the reply limit (1024 is a sensible floor).',
213
+ { usage });
213
214
  }
214
215
  if (reasoning) {
215
216
  return new AiError('bad_response',
216
- `${label} returned only its internal reasoning and no answer. Raise the reply limit and try again.`);
217
+ `${label} returned only its internal reasoning and no answer. Raise the reply limit and try again.`,
218
+ { usage });
217
219
  }
218
220
  return new AiError('bad_response',
219
221
  `${label} returned an empty completion (finish reason: ${finishReason || 'none given'}). ` +
@@ -79,9 +79,23 @@
79
79
  <span class="font-monospace"><%= Number(usage.total_tokens || 0).toLocaleString() %></span></div>
80
80
  <% } %>
81
81
  <div>
82
- <%# LAST GOOD, not just a dot. "last good 3h ago" tells a story a green light cannot. %>
82
+ <%# LAST GOOD, not just a dot. "last good 3h ago" tells a story a green light cannot.
83
+ THREE STATES, NOT TWO. `ago(...) || 'never'` claimed an endpoint had never worked
84
+ whenever there was no timestamp — but health lives in process memory, so every
85
+ restart wiped the record and a perfectly healthy endpoint came back reading "never".
86
+ That is not a missing value, it is a DIFFERENT FACT, and stating the stronger one is
87
+ how an operator ends up debugging a server that was fine. The checker already tells
88
+ them apart: `status: 'unknown'` means it has not been seen in this process at all,
89
+ whereas a real entry with no lastGoodAt has genuinely been tried and never answered. %>
83
90
  <div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Last good</div>
84
- <span class="font-monospace"><%= ago(h.lastGoodAt) || 'never' %></span>
91
+ <% if (h.lastGoodAt) { %>
92
+ <span class="font-monospace"><%= ago(h.lastGoodAt) %></span>
93
+ <% } else if (h.status === 'unknown') { %>
94
+ <span class="font-monospace text-body-secondary"
95
+ title="Nothing recorded since this server started. Health is kept in memory, so a restart clears it — press Test to find out.">no record</span>
96
+ <% } else { %>
97
+ <span class="font-monospace">never</span>
98
+ <% } %>
85
99
  </div>
86
100
  </div>
87
101