@aria-framework/ai 0.4.0 → 0.5.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
@@ -177,6 +177,13 @@ module.exports = {
177
177
  get createProviderStore() { return require('./providerStore').createProviderStore; },
178
178
  // No database behind health, so it loads eagerly like the rest of the seam.
179
179
  ...require('./health'),
180
+ /**
181
+ * Where this package's EJS partials live, for the consumer's view-roots list.
182
+ *
183
+ * Same contract as backup/server/notify/uploads: the package knows its own layout, the app
184
+ * puts its own views FIRST so a local file of the same name wins.
185
+ */
186
+ viewsDir: require('path').join(__dirname, 'views'),
180
187
  get providerSchemaFor() { return require('./providerStore').schemaFor; },
181
188
  get usageSchemaFor() { return require('./usageStore').schemaFor; },
182
189
  createAiClient,
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.0",
4
+ "version": "0.5.0",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -16,7 +16,7 @@
16
16
  "generate.js",
17
17
  "providers/openai-compatible.js",
18
18
  "providers/anthropic.js",
19
- "browser/ai-polish.js", "usageStore.js", "providerStore.js", "health.js"
19
+ "browser/ai-polish.js", "usageStore.js", "providerStore.js", "health.js", "views/"
20
20
  ],
21
21
  "peerDependencies": {
22
22
  "@aria-framework/db-worker": ">=0.7.0"
@@ -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"
28
+ "test": "node test/smoke.js && node test/usageStore.js && node test/providerStore.js && node test/health.js && node test/views.js"
29
29
  }
30
30
  }
@@ -0,0 +1,138 @@
1
+ <%#
2
+ One provider: what it is, whether it works, and what it has cost.
3
+
4
+ LOCALS
5
+ p a row from createProviderStore.all()
6
+ health a status object from createHealthChecker.status(p.id) — may be undefined
7
+ usage { calls, total_tokens } for the window, or undefined
8
+ canEdit boolean — hides controls rather than disabling them, because a greyed-out
9
+ button still advertises a feature and invites a support ticket
10
+ routes [{ id, position }] this provider appears in, for the shared-fallback warning
11
+
12
+ WHY "reachable" AND "model loaded" ARE SHOWN SEPARATELY: a local server answers /models
13
+ perfectly while the model itself has been evicted, and the next call then pays a
14
+ multi-second reload. One green dot would hide the only fact worth knowing.
15
+ -%>
16
+ <%
17
+ const h = typeof health !== 'undefined' && health ? health : { status: 'unknown', failures: 0 };
18
+ const local = p.kind === 'lmstudio' || p.kind === 'openai-compatible';
19
+ const dot = !p.enabled ? 'secondary'
20
+ : h.status === 'down' ? 'danger'
21
+ : h.status === 'up' ? (h.modelPresent === false ? 'warning' : 'success')
22
+ : 'secondary';
23
+ const ago = (ts) => {
24
+ if (!ts) return null;
25
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
26
+ if (s < 60) return s + 's ago';
27
+ if (s < 3600) return Math.round(s / 60) + 'm ago';
28
+ if (s < 86400) return Math.round(s / 3600) + 'h ago';
29
+ return Math.round(s / 86400) + 'd ago';
30
+ };
31
+ const shared = (typeof routes !== 'undefined' && routes) ? routes : [];
32
+ -%>
33
+ <article class="card mb-3">
34
+ <div class="card-body d-flex flex-wrap gap-3 justify-content-between align-items-start">
35
+ <div class="flex-grow-1" style="min-width:18rem">
36
+
37
+ <div class="d-flex align-items-center gap-2 mb-1">
38
+ <span class="badge rounded-pill bg-<%= dot %>" style="width:.6rem;height:.6rem;padding:0"
39
+ aria-hidden="true"></span>
40
+ <strong class="fs-6"><%= p.id %></strong>
41
+ <span class="badge text-bg-light border"><%= p.kind %></span>
42
+ <% if (local) { %><span class="badge text-bg-light border">local</span><% } %>
43
+ <% if (!p.enabled) { %><span class="badge text-bg-secondary">disabled</span><% } %>
44
+ <% if (h.status === 'down') { %>
45
+ <span class="badge text-bg-danger">
46
+ down<%= h.cooldownRemainingMs ? ' · retrying in ' + Math.ceil(h.cooldownRemainingMs / 1000) + 's' : '' %>
47
+ </span>
48
+ <% } else if (h.modelPresent === false) { %>
49
+ <span class="badge text-bg-warning">model not loaded</span>
50
+ <% } %>
51
+ </div>
52
+
53
+ <div class="text-body-secondary small font-monospace mb-2">
54
+ <%= p.model || '(no chat model)' %><% if (p.embedding_model) { %> · <%= p.embedding_model %><% } %>
55
+ <% if (p.base_url) { %> · <%= p.base_url %><% } %>
56
+ </div>
57
+
58
+ <div class="d-flex flex-wrap gap-4 small">
59
+ <% if (p.context_tokens) { %>
60
+ <div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Context</div>
61
+ <span class="font-monospace"><%= Number(p.context_tokens).toLocaleString() %></span></div>
62
+ <% } %>
63
+ <% if (h.lastMs != null) { %>
64
+ <div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Latency</div>
65
+ <span class="font-monospace"><%= h.lastMs %> ms</span></div>
66
+ <% } %>
67
+ <% if (typeof usage !== 'undefined' && usage) { %>
68
+ <div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Calls</div>
69
+ <span class="font-monospace"><%= Number(usage.calls || 0).toLocaleString() %></span></div>
70
+ <div><div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Tokens</div>
71
+ <span class="font-monospace"><%= Number(usage.total_tokens || 0).toLocaleString() %></span></div>
72
+ <% } %>
73
+ <div>
74
+ <%# LAST GOOD, not just a dot. "last good 3h ago" tells a story a green light cannot. %>
75
+ <div class="text-body-secondary text-uppercase" style="font-size:.68rem;letter-spacing:.06em">Last good</div>
76
+ <span class="font-monospace"><%= ago(h.lastGoodAt) || 'never' %></span>
77
+ </div>
78
+ </div>
79
+
80
+ <% if (p.daily_token_cap) { %>
81
+ <%
82
+ const spent = (typeof usage !== 'undefined' && usage) ? Number(usage.total_tokens || 0) : 0;
83
+ const pct = Math.min(100, Math.round((spent / Number(p.daily_token_cap)) * 100));
84
+ -%>
85
+ <div class="mt-3" style="max-width:26rem">
86
+ <div class="d-flex justify-content-between small text-body-secondary">
87
+ <span>Daily token cap</span>
88
+ <span class="font-monospace"><%= spent.toLocaleString() %> / <%= Number(p.daily_token_cap).toLocaleString() %></span>
89
+ </div>
90
+ <div class="progress" style="height:.4rem" role="progressbar" aria-valuenow="<%= pct %>"
91
+ aria-valuemin="0" aria-valuemax="100">
92
+ <div class="progress-bar bg-<%= pct >= 100 ? 'danger' : pct >= 80 ? 'warning' : 'success' %>"
93
+ style="width:<%= pct %>%"></div>
94
+ </div>
95
+ </div>
96
+ <% } %>
97
+
98
+ <% if (h.status === 'down' && h.lastError) { %>
99
+ <div class="alert alert-danger d-flex gap-2 mt-3 mb-0 py-2">
100
+ <i class="bi bi-exclamation-triangle mt-1"></i>
101
+ <div class="small">
102
+ <strong>Marked down after <%= h.failures %> consecutive failure<%= h.failures === 1 ? '' : 's' %>.</strong>
103
+ Last error: <code><%= h.lastError %></code>. Calls skip this provider entirely rather than
104
+ paying its timeout, and it returns to service on the first success.
105
+ </div>
106
+ </div>
107
+ <% } else if (h.modelPresent === false) { %>
108
+ <div class="alert alert-warning d-flex gap-2 mt-3 mb-0 py-2">
109
+ <i class="bi bi-hourglass-split mt-1"></i>
110
+ <div class="small">
111
+ <strong>Reachable, but <code><%= p.model %></code> is not loaded.</strong>
112
+ The next call pays a reload before it does anything. This is not a failure and nothing
113
+ fails over — but if it keeps happening, turn off automatic model unloading on the server.
114
+ </div>
115
+ </div>
116
+ <% } %>
117
+
118
+ <% if (shared.length > 1) { %>
119
+ <div class="alert alert-warning d-flex gap-2 mt-3 mb-0 py-2">
120
+ <i class="bi bi-diagram-3 mt-1"></i>
121
+ <div class="small">
122
+ <strong>Serves <%= shared.length %> routes.</strong>
123
+ <%= shared.map((r) => r.id + ' (' + (r.position === 0 ? 'primary' : 'fallback') + ')').join(', ') %>.
124
+ If it goes down, one route fails over <em>and</em> another loses its safety net at the
125
+ same moment.
126
+ </div>
127
+ </div>
128
+ <% } %>
129
+ </div>
130
+
131
+ <div class="d-flex flex-column gap-2 align-items-end">
132
+ <% if (typeof canEdit !== 'undefined' && canEdit) { %>
133
+ <button class="btn btn-sm btn-outline-secondary" name="test_provider" value="<%= p.id %>">Test</button>
134
+ <a class="btn btn-sm btn-outline-secondary" href="?edit=<%= encodeURIComponent(p.id) %>">Edit</a>
135
+ <% } %>
136
+ </div>
137
+ </div>
138
+ </article>
@@ -0,0 +1,58 @@
1
+ <%#
2
+ Per-provider usage over a window.
3
+
4
+ LOCALS
5
+ rows from createUsageStore.byProvider(sinceDay) — [{provider, model, calls, ...}]
6
+ prices optional { [model]: costPerMillionTokens } supplied BY THE APP. Prices change, and
7
+ a stale hardcoded price silently produces confidently wrong cost reports, so the
8
+ package never carries one.
9
+ kinds optional { [providerId]: kind } so a local endpoint can be shown as having no cost
10
+ rather than a cost of zero — different facts.
11
+ -%>
12
+ <%
13
+ const priceFor = (m) => (typeof prices !== 'undefined' && prices && prices[m] != null) ? Number(prices[m]) : null;
14
+ const kindOf = (id) => (typeof kinds !== 'undefined' && kinds) ? kinds[id] : undefined;
15
+ const isLocal = (id) => ['lmstudio', 'openai-compatible'].includes(kindOf(id));
16
+ const n = (v) => Number(v || 0).toLocaleString();
17
+ -%>
18
+ <div class="table-responsive">
19
+ <table class="table table-sm align-middle mb-0">
20
+ <thead>
21
+ <tr class="small text-body-secondary text-uppercase">
22
+ <th>Provider</th><th>Model</th>
23
+ <th class="text-end">Calls</th><th class="text-end">Prompt</th>
24
+ <th class="text-end">Completion</th><th class="text-end">Total</th><th class="text-end">Cost</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% if (!rows || !rows.length) { %>
29
+ <tr><td colspan="7" class="text-body-secondary py-3">Nothing recorded in this window.</td></tr>
30
+ <% } %>
31
+ <% (rows || []).forEach(function (r) {
32
+ const price = priceFor(r.model);
33
+ const cost = price == null ? null : (Number(r.total_tokens || 0) / 1e6) * price;
34
+ -%>
35
+ <tr>
36
+ <td><%= r.provider %></td>
37
+ <td class="font-monospace small"><%= r.model %></td>
38
+ <td class="text-end font-monospace"><%= n(r.calls) %></td>
39
+ <td class="text-end font-monospace"><%= n(r.prompt_tokens) %></td>
40
+ <td class="text-end font-monospace"><%= n(r.completion_tokens) %></td>
41
+ <td class="text-end font-monospace"><%= n(r.total_tokens) %></td>
42
+ <td class="text-end font-monospace">
43
+ <%# A local GPU has NO cost, which is a different fact from a cost of zero. Showing
44
+ "0.00" would invite someone to compare it against a cloud row as if it were cheap. %>
45
+ <% if (isLocal(r.provider)) { %><span class="text-body-secondary">&mdash;</span>
46
+ <% } else if (cost == null) { %><span class="text-body-secondary" title="No price configured for this model">?</span>
47
+ <% } else { %><%= cost.toFixed(2) %><% } %>
48
+ </td>
49
+ </tr>
50
+ <% }); -%>
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ <p class="small text-body-secondary mt-2 mb-0">
55
+ Tokens are recorded for every call, including local models. Cost is derived from tokens and the
56
+ price table this app supplies &mdash; never stored &mdash; so a local endpoint still reports real
57
+ numbers, just different ones: throughput and utilisation rather than spend.
58
+ </p>