@aria-framework/ai 0.8.1 → 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/index.js +23 -4
- package/package.json +2 -2
- package/providers/anthropic.js +11 -1
- package/providers/openai-compatible.js +33 -13
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 };
|