@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 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) return [];
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 PROVIDERS[cfg.provider].listModels(cfg);
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.8.1",
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
  }
@@ -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
- /** What the server has loaded — used by the admin screen so nobody has to type a model name blind. */
288
- async function listModels(cfg) {
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) return [];
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
- // The 200-with-an-error-body case needs nothing special here: an error body has no `data`, so
298
- // it already yields no models. A guard for it was written and then deleted a control proved
299
- // it could not fail, which is the definition of code that is not doing anything.
300
- //
301
- // The admin screen says both possibilities out loud ("nothing loaded, or the address may be
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
- // A provider that cannot list models can still complete; this is a convenience, not a check.
306
- return [];
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 };