@aria-framework/ai 0.8.0 → 0.9.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 +14 -7
- package/index.js +23 -4
- package/package.json +2 -2
- package/providers/anthropic.js +11 -1
- package/providers/openai-compatible.js +33 -13
package/health.js
CHANGED
|
@@ -200,8 +200,12 @@ function createHealthChecker(opts = {}) {
|
|
|
200
200
|
// generation speed — separating those would need time-to-first-token, which no adapter
|
|
201
201
|
// reports — but it is what the caller actually experienced, which is the number worth planning
|
|
202
202
|
// against. Only completions count; a health probe generates no tokens and would drag it to 0.
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
// Guarded on TOKENS, not on duration. Requiring ms > 0 here silently dropped any call that
|
|
204
|
+
// completed inside a millisecond — which a fast local model or a stub genuinely does, so the
|
|
205
|
+
// sample count became timing-dependent. The division is where zero actually matters, so that
|
|
206
|
+
// is where it is handled.
|
|
207
|
+
if (ok && info.tokens > 0) {
|
|
208
|
+
e.samples.push({ tokens: info.tokens, ms: Math.max(0, Number(info.ms) || 0) });
|
|
205
209
|
if (e.samples.length > WINDOW) e.samples.shift();
|
|
206
210
|
}
|
|
207
211
|
if (ok) {
|
|
@@ -303,11 +307,14 @@ function createHealthChecker(opts = {}) {
|
|
|
303
307
|
modelPresent: e.modelPresent,
|
|
304
308
|
// NULL until something has actually generated tokens. Reporting 0 tok/s for a provider
|
|
305
309
|
// nobody has used yet reads as "it is slow" rather than "we do not know".
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
// Null rather than Infinity when every sample was too fast to measure: "we cannot tell"
|
|
311
|
+
// is the honest answer, and a card showing Infinity tok/s is worse than showing nothing.
|
|
312
|
+
tokensPerSec: (() => {
|
|
313
|
+
if (!e.samples.length) return null;
|
|
314
|
+
const ms = e.samples.reduce((n, x) => n + x.ms, 0);
|
|
315
|
+
if (ms <= 0) return null;
|
|
316
|
+
return Math.round(e.samples.reduce((n, x) => n + x.tokens, 0) / (ms / 1000));
|
|
317
|
+
})(),
|
|
311
318
|
samples: e.samples.length,
|
|
312
319
|
cooldownRemainingMs: down ? e.downUntil - now() : 0
|
|
313
320
|
};
|
package/index.js
CHANGED
|
@@ -148,12 +148,31 @@ function createAiClient(deps = {}) {
|
|
|
148
148
|
|
|
149
149
|
/** What the server has loaded, for an admin model picker. Empty when it cannot say. */
|
|
150
150
|
async function listModels(cfgOverride) {
|
|
151
|
+
return (await listModelsResult(cfgOverride)).models;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The model list and WHY it is the length it is.
|
|
156
|
+
*
|
|
157
|
+
* Callers that only want names should use listModels(). An admin screen wants this one: an empty
|
|
158
|
+
* array on its own cannot tell "the server has nothing loaded" from "that address is not an
|
|
159
|
+
* OpenAI-compatible API root", and those need different fixes.
|
|
160
|
+
*/
|
|
161
|
+
async function listModelsResult(cfgOverride) {
|
|
151
162
|
const cfg = cfgOverride || await resolveConfig();
|
|
152
|
-
if (!cfg.enabled)
|
|
163
|
+
if (!cfg.enabled) {
|
|
164
|
+
return { ok: false, models: [], url: null, status: 0, error: 'No provider is selected.' };
|
|
165
|
+
}
|
|
166
|
+
const adapter = PROVIDERS[cfg.provider];
|
|
167
|
+
if (!adapter) {
|
|
168
|
+
return { ok: false, models: [], url: null, status: 0, error: `Unknown provider “${cfg.provider}”.` };
|
|
169
|
+
}
|
|
153
170
|
try {
|
|
154
|
-
return await
|
|
171
|
+
if (adapter.listModelsResult) return await adapter.listModelsResult(cfg);
|
|
172
|
+
// An adapter that predates this contract still works; it simply cannot explain itself.
|
|
173
|
+
return { ok: true, models: await adapter.listModels(cfg), url: null, status: 0, error: null };
|
|
155
174
|
} catch (err) {
|
|
156
|
-
return [];
|
|
175
|
+
return { ok: false, models: [], url: null, status: 0, error: err.message };
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
|
|
@@ -163,7 +182,7 @@ function createAiClient(deps = {}) {
|
|
|
163
182
|
const boundGenerate = (opts) => generate(complete, opts);
|
|
164
183
|
|
|
165
184
|
return {
|
|
166
|
-
complete, isEnabled, test, listModels, withOneRetry,
|
|
185
|
+
complete, isEnabled, test, listModels, listModelsResult, withOneRetry,
|
|
167
186
|
polish: boundPolish, generate: boundGenerate,
|
|
168
187
|
facts, AiError, PROVIDERS, DEFAULTS
|
|
169
188
|
};
|
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.9.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/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/views.js"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/providers/anthropic.js
CHANGED
|
@@ -173,6 +173,16 @@ function normaliseUsage(u) {
|
|
|
173
173
|
/** No listing endpoint is used here — the model name is typed, and a wrong one fails loudly. */
|
|
174
174
|
async function listModels() { return []; }
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Anthropic has no models endpoint on this surface. `unsupported` lets the admin screen say that
|
|
178
|
+
* plainly instead of reporting an empty list, which reads as a broken server.
|
|
179
|
+
*/
|
|
180
|
+
async function listModelsResult() {
|
|
181
|
+
return {
|
|
182
|
+
ok: true, models: [], url: null, status: 0, error: null, unsupported: true
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
/**
|
|
177
187
|
* Anthropic does not offer an embeddings API. This is not a gap in the adapter.
|
|
178
188
|
*
|
|
@@ -189,4 +199,4 @@ async function embed() {
|
|
|
189
199
|
'with this provider. Everything else works; results are ordered by text search alone.');
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
module.exports = { complete, listModels, embed, API_VERSION, DEFAULT_BASE };
|
|
202
|
+
module.exports = { complete, listModels, listModelsResult, embed, API_VERSION, DEFAULT_BASE };
|
|
@@ -284,29 +284,49 @@ function normaliseUsage(u) {
|
|
|
284
284
|
return { prompt, completion, total: u.total_tokens || prompt + completion };
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
/**
|
|
288
|
-
|
|
287
|
+
/**
|
|
288
|
+
* What the server has loaded, WITH the diagnosis attached.
|
|
289
|
+
*
|
|
290
|
+
* The previous version returned a bare array and threw the rest away, so three quite different
|
|
291
|
+
* situations all arrived as `[]`: a server with nothing loaded, a base URL pointing at the wrong
|
|
292
|
+
* API surface, and a host that never answered. The admin screen could then only recite the
|
|
293
|
+
* possibilities and let the operator guess — which cost a real afternoon when an Ollama base URL
|
|
294
|
+
* was set to `/api` (its NATIVE surface) instead of `/v1` (its OpenAI-compatible one). The server
|
|
295
|
+
* answered `404 page not found` and the screen said "nothing loaded, or the address may be wrong".
|
|
296
|
+
*
|
|
297
|
+
* The URL and the status are what separate those cases, and both are free right here. THIS IS NOT
|
|
298
|
+
* URL CORRECTION: a wrong address still fails, it just says which address it tried.
|
|
299
|
+
*
|
|
300
|
+
* @returns {{ok: boolean, models: string[], url: string, status: number, error: string|null}}
|
|
301
|
+
*/
|
|
302
|
+
async function listModelsResult(cfg) {
|
|
289
303
|
const url = apiRoot(cfg.baseUrl) + '/models';
|
|
290
304
|
try {
|
|
291
305
|
const res = await fetch(url, {
|
|
292
306
|
headers: cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {},
|
|
293
307
|
signal: AbortSignal.timeout(cfg.timeoutMs || 10000)
|
|
294
308
|
});
|
|
295
|
-
if (!res.ok)
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
return { ok: false, models: [], url, status: res.status, error: `GET ${url} answered HTTP ${res.status}` };
|
|
311
|
+
}
|
|
296
312
|
const payload = await res.json();
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// wrong") because from here the two are genuinely indistinguishable.
|
|
303
|
-
return (payload.data || []).map((m) => m.id).filter(Boolean);
|
|
313
|
+
// A 200 WITH AN ERROR BODY still yields no models, because an error body has no `data` — but
|
|
314
|
+
// unlike the old code this reports it as an empty LIST rather than a failure, which is what it
|
|
315
|
+
// is. LM Studio does exactly this when the base URL is missing its `/v1`.
|
|
316
|
+
const models = (payload.data || []).map((m) => m.id).filter(Boolean);
|
|
317
|
+
return { ok: true, models, url, status: res.status, error: null };
|
|
304
318
|
} catch (err) {
|
|
305
|
-
//
|
|
306
|
-
|
|
319
|
+
// Never throws: a provider that cannot list models can still complete. This is a convenience,
|
|
320
|
+
// not a health check — but a convenience that says why it came back empty.
|
|
321
|
+
return { ok: false, models: [], url, status: 0, error: `GET ${url} failed: ${err.message}` };
|
|
307
322
|
}
|
|
308
323
|
}
|
|
309
324
|
|
|
325
|
+
/** The bare list, for callers that only want names. Kept so existing callers are unaffected. */
|
|
326
|
+
async function listModels(cfg) {
|
|
327
|
+
return (await listModelsResult(cfg)).models;
|
|
328
|
+
}
|
|
329
|
+
|
|
310
330
|
/**
|
|
311
331
|
* Embed one or more strings.
|
|
312
332
|
*
|
|
@@ -385,4 +405,4 @@ async function embed(cfg, texts) {
|
|
|
385
405
|
});
|
|
386
406
|
}
|
|
387
407
|
|
|
388
|
-
module.exports = { complete, listModels, embed, apiRoot };
|
|
408
|
+
module.exports = { complete, listModels, listModelsResult, embed, apiRoot };
|