@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 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
 
@@ -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 result = await withOneRetry(() => adapter.complete(cfg, callOpts), opts);
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.