@aria-framework/ai 0.26.0 → 0.26.1
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/README.md +6 -0
- package/browser/ai-panels.js +138 -138
- package/index.js +20 -1
- package/lmxVerify.js +402 -402
- package/package.json +2 -2
- package/providers/anthropic.js +11 -4
- package/providers/openai-compatible.js +18 -7
- package/views/ai/job-card.ejs +182 -182
- package/views/ai/lmx-stack.ejs +426 -426
package/README.md
CHANGED
|
@@ -44,6 +44,12 @@ Every failure is thrown as an `AiError` with a `kind`: `disabled`, `unconfigured
|
|
|
44
44
|
`timeout`, `auth`, `rate_limit`, `bad_response` or `refused`. The `message` is written for
|
|
45
45
|
an admin screen and never includes a URL or key.
|
|
46
46
|
|
|
47
|
+
**A failed call that spent tokens is still metered** (0.26.1). When the model answered but the answer
|
|
48
|
+
was unusable (it thought until the limit, or a structured reply was cut off), the error carries
|
|
49
|
+
`usage` (`{ prompt, completion, total }`) and `budget.record(cfg, { usage, model, ms, failed: true }, meta)`
|
|
50
|
+
is called before the error is thrown. A failure that spent nothing (refused, unreachable, timeout)
|
|
51
|
+
records nothing. If `record` throws, the original error still reaches the caller.
|
|
52
|
+
|
|
47
53
|
`providerStore`, `usageStore`, `speedStore` and `lmxStore` are optional db-worker-backed
|
|
48
54
|
stores. Each exports a `schemaFor(dialect)`, so the app's migration can be checked against it.
|
|
49
55
|
|
package/browser/ai-panels.js
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fold on the Inference list, and prefilling the supervisor form from a panel.
|
|
3
|
-
*
|
|
4
|
-
* WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
|
|
5
|
-
* and a verification report — worth having, not worth reading every time you open this page to do
|
|
6
|
-
* something else. Folded, a panel is one line that still carries the whole verdict: status, the
|
|
7
|
-
* count of what depends on it, all three credentials and when it was last checked. Folding hides
|
|
8
|
-
* detail; it must never hide the reason you would have opened it.
|
|
9
|
-
*
|
|
10
|
-
* A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
|
|
11
|
-
* server on anything with a missing engine, an expiring certificate, a failed check or a nearly
|
|
12
|
-
* spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
|
|
13
|
-
* quietly honours a fold you chose last week and hides the thing that broke yesterday.
|
|
14
|
-
*
|
|
15
|
-
* THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
|
|
16
|
-
* person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
|
|
17
|
-
* a browser set to block site data throws on read, and a settings page must not break because
|
|
18
|
-
* somebody tightened their privacy settings.
|
|
19
|
-
*/
|
|
20
|
-
(function () {
|
|
21
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
var KEY = 's101.ai.panels';
|
|
24
|
-
|
|
25
|
-
function readPrefs() {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
|
|
28
|
-
} catch (e) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writePref(id, open) {
|
|
34
|
-
try {
|
|
35
|
-
var prefs = readPrefs();
|
|
36
|
-
prefs[id] = !!open;
|
|
37
|
-
window.localStorage.setItem(KEY, JSON.stringify(prefs));
|
|
38
|
-
} catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function bodyOf(panel) {
|
|
42
|
-
var btn = panel.querySelector('[data-panel-toggle]');
|
|
43
|
-
if (!btn) return null;
|
|
44
|
-
return document.getElementById(btn.getAttribute('aria-controls'));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function setOpen(panel, open, remember) {
|
|
48
|
-
var btn = panel.querySelector('[data-panel-toggle]');
|
|
49
|
-
var body = bodyOf(panel);
|
|
50
|
-
if (!btn || !body) return;
|
|
51
|
-
// `hidden`, not a style: the server renders the closed state the same way, so a panel does not
|
|
52
|
-
// flicker open on load before this script runs.
|
|
53
|
-
body.hidden = !open;
|
|
54
|
-
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
55
|
-
panel.classList.toggle('panel-open', open);
|
|
56
|
-
if (remember) writePref(panel.getAttribute('data-panel'), open);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function restore() {
|
|
60
|
-
var prefs = readPrefs();
|
|
61
|
-
var panels = document.querySelectorAll('[data-panel]');
|
|
62
|
-
for (var i = 0; i < panels.length; i += 1) {
|
|
63
|
-
var panel = panels[i];
|
|
64
|
-
var id = panel.getAttribute('data-panel');
|
|
65
|
-
// The server already opened this one and means it. Do not consult the preference at all —
|
|
66
|
-
// reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
|
|
67
|
-
if (panel.hasAttribute('data-panel-attention')) {
|
|
68
|
-
setOpen(panel, true, false);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
document.addEventListener('click', function (ev) {
|
|
76
|
-
var toggle = ev.target.closest('[data-panel-toggle]');
|
|
77
|
-
if (toggle) {
|
|
78
|
-
var panel = toggle.closest('[data-panel]');
|
|
79
|
-
if (!panel) return;
|
|
80
|
-
var body = bodyOf(panel);
|
|
81
|
-
setOpen(panel, !!(body && body.hidden), true);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── EDITING A STACK HAPPENS IN THE PANEL ────────────────────────────────────────────────────
|
|
86
|
-
//
|
|
87
|
-
// It used to happen in a form at the foot of the page, and Edit scrolled you down to it — away
|
|
88
|
-
// from the stack you were reading, into a form that also served as "Add a supervisor" and said
|
|
89
|
-
// nothing about which stack it had been filled with. Nothing needs prefilling now: every panel
|
|
90
|
-
// renders its own form, from the server, with its own values already in it.
|
|
91
|
-
var open = ev.target.closest('[data-stack-edit-open]');
|
|
92
|
-
if (open) { stackMode(open.closest('[data-stack]'), true); return; }
|
|
93
|
-
|
|
94
|
-
var cancel = ev.target.closest('[data-stack-cancel]');
|
|
95
|
-
if (cancel) {
|
|
96
|
-
var panel2 = cancel.closest('[data-stack]');
|
|
97
|
-
// A NEW stack has no reading half to go back to, so cancelling it puts the blank panel away.
|
|
98
|
-
if (panel2 && panel2.hasAttribute('data-stack-new')) { panel2.hidden = true; return; }
|
|
99
|
-
// Otherwise reload rather than restore: a Cancel that left half-typed values behind, ready to
|
|
100
|
-
// be posted by the next Save, would be worse than the extra request.
|
|
101
|
-
window.location.reload();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// "Add LMX" reveals the blank panel that is already on the page, at the top of the list.
|
|
106
|
-
var add = ev.target.closest('[data-stack-add]');
|
|
107
|
-
if (add) {
|
|
108
|
-
var blank = document.querySelector('[data-stack-new]');
|
|
109
|
-
if (!blank) return;
|
|
110
|
-
blank.hidden = false;
|
|
111
|
-
blank.scrollIntoView({ block: 'nearest' });
|
|
112
|
-
var id = blank.querySelector('[name="id"]');
|
|
113
|
-
if (id) id.focus();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
/** Swap one stack panel between reading and editing. */
|
|
118
|
-
function stackMode(panel, editing) {
|
|
119
|
-
if (!panel) return;
|
|
120
|
-
var view = panel.querySelector('[data-stack-view]');
|
|
121
|
-
var form = panel.querySelector('[data-stack-edit]');
|
|
122
|
-
if (!view || !form) return;
|
|
123
|
-
view.hidden = editing;
|
|
124
|
-
form.hidden = !editing;
|
|
125
|
-
if (editing) {
|
|
126
|
-
// The id is readonly on an existing stack, so focus the first field somebody can actually
|
|
127
|
-
// change rather than one that will not accept a keystroke.
|
|
128
|
-
var first = form.querySelector('[name="label"]') || form.querySelector('[name="status_url"]');
|
|
129
|
-
if (first) first.focus();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (document.readyState === 'loading') {
|
|
134
|
-
document.addEventListener('DOMContentLoaded', restore);
|
|
135
|
-
} else {
|
|
136
|
-
restore();
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
1
|
+
/**
|
|
2
|
+
* The fold on the Inference list, and prefilling the supervisor form from a panel.
|
|
3
|
+
*
|
|
4
|
+
* WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
|
|
5
|
+
* and a verification report — worth having, not worth reading every time you open this page to do
|
|
6
|
+
* something else. Folded, a panel is one line that still carries the whole verdict: status, the
|
|
7
|
+
* count of what depends on it, all three credentials and when it was last checked. Folding hides
|
|
8
|
+
* detail; it must never hide the reason you would have opened it.
|
|
9
|
+
*
|
|
10
|
+
* A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
|
|
11
|
+
* server on anything with a missing engine, an expiring certificate, a failed check or a nearly
|
|
12
|
+
* spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
|
|
13
|
+
* quietly honours a fold you chose last week and hides the thing that broke yesterday.
|
|
14
|
+
*
|
|
15
|
+
* THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
|
|
16
|
+
* person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
|
|
17
|
+
* a browser set to block site data throws on read, and a settings page must not break because
|
|
18
|
+
* somebody tightened their privacy settings.
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
var KEY = 's101.ai.panels';
|
|
24
|
+
|
|
25
|
+
function readPrefs() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writePref(id, open) {
|
|
34
|
+
try {
|
|
35
|
+
var prefs = readPrefs();
|
|
36
|
+
prefs[id] = !!open;
|
|
37
|
+
window.localStorage.setItem(KEY, JSON.stringify(prefs));
|
|
38
|
+
} catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bodyOf(panel) {
|
|
42
|
+
var btn = panel.querySelector('[data-panel-toggle]');
|
|
43
|
+
if (!btn) return null;
|
|
44
|
+
return document.getElementById(btn.getAttribute('aria-controls'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setOpen(panel, open, remember) {
|
|
48
|
+
var btn = panel.querySelector('[data-panel-toggle]');
|
|
49
|
+
var body = bodyOf(panel);
|
|
50
|
+
if (!btn || !body) return;
|
|
51
|
+
// `hidden`, not a style: the server renders the closed state the same way, so a panel does not
|
|
52
|
+
// flicker open on load before this script runs.
|
|
53
|
+
body.hidden = !open;
|
|
54
|
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
55
|
+
panel.classList.toggle('panel-open', open);
|
|
56
|
+
if (remember) writePref(panel.getAttribute('data-panel'), open);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function restore() {
|
|
60
|
+
var prefs = readPrefs();
|
|
61
|
+
var panels = document.querySelectorAll('[data-panel]');
|
|
62
|
+
for (var i = 0; i < panels.length; i += 1) {
|
|
63
|
+
var panel = panels[i];
|
|
64
|
+
var id = panel.getAttribute('data-panel');
|
|
65
|
+
// The server already opened this one and means it. Do not consult the preference at all —
|
|
66
|
+
// reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
|
|
67
|
+
if (panel.hasAttribute('data-panel-attention')) {
|
|
68
|
+
setOpen(panel, true, false);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
document.addEventListener('click', function (ev) {
|
|
76
|
+
var toggle = ev.target.closest('[data-panel-toggle]');
|
|
77
|
+
if (toggle) {
|
|
78
|
+
var panel = toggle.closest('[data-panel]');
|
|
79
|
+
if (!panel) return;
|
|
80
|
+
var body = bodyOf(panel);
|
|
81
|
+
setOpen(panel, !!(body && body.hidden), true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── EDITING A STACK HAPPENS IN THE PANEL ────────────────────────────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// It used to happen in a form at the foot of the page, and Edit scrolled you down to it — away
|
|
88
|
+
// from the stack you were reading, into a form that also served as "Add a supervisor" and said
|
|
89
|
+
// nothing about which stack it had been filled with. Nothing needs prefilling now: every panel
|
|
90
|
+
// renders its own form, from the server, with its own values already in it.
|
|
91
|
+
var open = ev.target.closest('[data-stack-edit-open]');
|
|
92
|
+
if (open) { stackMode(open.closest('[data-stack]'), true); return; }
|
|
93
|
+
|
|
94
|
+
var cancel = ev.target.closest('[data-stack-cancel]');
|
|
95
|
+
if (cancel) {
|
|
96
|
+
var panel2 = cancel.closest('[data-stack]');
|
|
97
|
+
// A NEW stack has no reading half to go back to, so cancelling it puts the blank panel away.
|
|
98
|
+
if (panel2 && panel2.hasAttribute('data-stack-new')) { panel2.hidden = true; return; }
|
|
99
|
+
// Otherwise reload rather than restore: a Cancel that left half-typed values behind, ready to
|
|
100
|
+
// be posted by the next Save, would be worse than the extra request.
|
|
101
|
+
window.location.reload();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// "Add LMX" reveals the blank panel that is already on the page, at the top of the list.
|
|
106
|
+
var add = ev.target.closest('[data-stack-add]');
|
|
107
|
+
if (add) {
|
|
108
|
+
var blank = document.querySelector('[data-stack-new]');
|
|
109
|
+
if (!blank) return;
|
|
110
|
+
blank.hidden = false;
|
|
111
|
+
blank.scrollIntoView({ block: 'nearest' });
|
|
112
|
+
var id = blank.querySelector('[name="id"]');
|
|
113
|
+
if (id) id.focus();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** Swap one stack panel between reading and editing. */
|
|
118
|
+
function stackMode(panel, editing) {
|
|
119
|
+
if (!panel) return;
|
|
120
|
+
var view = panel.querySelector('[data-stack-view]');
|
|
121
|
+
var form = panel.querySelector('[data-stack-edit]');
|
|
122
|
+
if (!view || !form) return;
|
|
123
|
+
view.hidden = editing;
|
|
124
|
+
form.hidden = !editing;
|
|
125
|
+
if (editing) {
|
|
126
|
+
// The id is readonly on an existing stack, so focus the first field somebody can actually
|
|
127
|
+
// change rather than one that will not accept a keystroke.
|
|
128
|
+
var first = form.querySelector('[name="label"]') || form.querySelector('[name="status_url"]');
|
|
129
|
+
if (first) first.focus();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (document.readyState === 'loading') {
|
|
134
|
+
document.addEventListener('DOMContentLoaded', restore);
|
|
135
|
+
} else {
|
|
136
|
+
restore();
|
|
137
|
+
}
|
|
138
|
+
})();
|
package/index.js
CHANGED
|
@@ -148,7 +148,26 @@ function createAiClient(deps = {}) {
|
|
|
148
148
|
callOpts = rest;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await withOneRetry(() => adapter.complete(cfg, callOpts), opts);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// A FAILED CALL CAN STILL HAVE SPENT TOKENS (0.26.1) — a model that thought until it ran out of
|
|
157
|
+
// room, a structured reply cut off mid-bracket. Recording only successes let a benchmark or a
|
|
158
|
+
// connection test on such a model run outside the ceiling. Adapters put normalised usage on
|
|
159
|
+
// the error; a failure that spent nothing (refused, unreachable) carries none and is not counted.
|
|
160
|
+
// The ledger failing must never replace the error the caller needs to see.
|
|
161
|
+
if (err && err.usage && err.usage.total > 0) {
|
|
162
|
+
try {
|
|
163
|
+
await meter.record(cfg, { usage: err.usage, model: cfg.model, ms: Date.now() - started, failed: true },
|
|
164
|
+
{ ticketId: opts.ticketId });
|
|
165
|
+
} catch (recErr) {
|
|
166
|
+
log.warn(`AI: usage of a failed call not recorded (${recErr.message})`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
152
171
|
|
|
153
172
|
// ...and the counter after it, AWAITED: two calls in quick succession must both be counted
|
|
154
173
|
// before the second's ceiling check reads the total, or the limit is enforced against a stale one.
|