@bitbaum/ai-kit 0.12.0 → 0.14.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/dist/complete.js +19 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +20 -0
- package/package.json +1 -1
- package/src/complete.ts +20 -0
- package/src/liveness.ts +48 -0
package/dist/complete.js
CHANGED
|
@@ -291,6 +291,25 @@ export async function complete(options) {
|
|
|
291
291
|
options.onLinkFailure?.(link, failure);
|
|
292
292
|
if (failure.kind === "daily")
|
|
293
293
|
deadProviders.add(link.provider.id);
|
|
294
|
+
// A REJECTED KEY is a verdict about the VENDOR, not the model.
|
|
295
|
+
//
|
|
296
|
+
// 401/403 says "not you". Every remaining link at this provider presents
|
|
297
|
+
// the identical credential, so walking them spends a request each to be
|
|
298
|
+
// told the same thing — and then reports "all 5 links failed", which
|
|
299
|
+
// reads as an outage at someone else's shop and sends the reader looking
|
|
300
|
+
// for one. The fact worth surfacing is that a key this app holds was
|
|
301
|
+
// refused.
|
|
302
|
+
//
|
|
303
|
+
// Crossing to the NEXT vendor still happens: that is a different key, and
|
|
304
|
+
// the whole reason the chain spans vendors.
|
|
305
|
+
//
|
|
306
|
+
// Deliberately narrow. A 404 is a retired id, a 5xx is a vendor being
|
|
307
|
+
// unwell, a capacity 429 is a busy minute — all three are answered by
|
|
308
|
+
// asking a different model, and widening this skip to cover them would
|
|
309
|
+
// quietly turn the chain back into the pin it replaced.
|
|
310
|
+
if (failure.status === 401 || failure.status === 403) {
|
|
311
|
+
deadProviders.add(link.provider.id);
|
|
312
|
+
}
|
|
294
313
|
// The caller cancelled — the request they were waiting on is gone. Walking
|
|
295
314
|
// the rest of the chain now would spend their daily budget on an answer
|
|
296
315
|
// nobody will read, and would report "every vendor failed" about vendors
|
package/dist/liveness.d.ts
CHANGED
|
@@ -94,6 +94,36 @@ export interface LivenessOptions extends Omit<CompleteOptions, "messages" | "max
|
|
|
94
94
|
chain: Link[];
|
|
95
95
|
env?: Env;
|
|
96
96
|
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Make the call YOURSELF, using the app's own path. Takes precedence over
|
|
99
|
+
* `chain` and `resolveChain`.
|
|
100
|
+
*
|
|
101
|
+
* Not every app can hand over a `Link[]`. Four in this fleet cannot: one
|
|
102
|
+
* builds its chain from its own provider CLASSES, one from a registry it
|
|
103
|
+
* deliberately owns (its BYOK list includes paid ids), one talks to a single
|
|
104
|
+
* operator-configured endpoint chosen for data residency, and one keeps a
|
|
105
|
+
* provider layer that predates this package. Requiring a chain would have
|
|
106
|
+
* meant either rewriting those or leaving them with no probe at all — and
|
|
107
|
+
* "no probe" is what left seven apps unable to answer "does the AI work?"
|
|
108
|
+
* after a fleet-wide refactor.
|
|
109
|
+
*
|
|
110
|
+
* There is a second, better reason. A probe built from a chain this module
|
|
111
|
+
* assembles tests A path; `ask` tests THE path — the same function the app's
|
|
112
|
+
* real features call. That is strictly stronger evidence, and it means the
|
|
113
|
+
* probe cannot quietly drift away from the code it is meant to vouch for.
|
|
114
|
+
*
|
|
115
|
+
* Everything else still applies: it runs only on an explicit, authorised
|
|
116
|
+
* probe, a success is cached, and a failure never is. Return the text the
|
|
117
|
+
* model produced and, if you have it, the `provider/model` that served it.
|
|
118
|
+
*
|
|
119
|
+
* An empty or whitespace-only `text` is treated as a FAILURE, for the same
|
|
120
|
+
* reason `complete()` treats it as one: a 200 carrying nothing is the
|
|
121
|
+
* failure most likely to be reported as success.
|
|
122
|
+
*/
|
|
123
|
+
ask?: () => Promise<{
|
|
124
|
+
text: string;
|
|
125
|
+
id?: string;
|
|
126
|
+
}>;
|
|
97
127
|
/** Injected for tests. Defaults to `Date.now`. */
|
|
98
128
|
now?: () => number;
|
|
99
129
|
}
|
package/dist/liveness.js
CHANGED
|
@@ -90,6 +90,26 @@ export function createLivenessProbe(options = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
const started = now();
|
|
92
92
|
try {
|
|
93
|
+
if (options.ask) {
|
|
94
|
+
const asked = await options.ask();
|
|
95
|
+
const text = asked.text.trim();
|
|
96
|
+
// Same rule as `complete()`: a 200 carrying nothing is not an answer.
|
|
97
|
+
// Without this, an app whose own path returns "" on failure — several
|
|
98
|
+
// do, by design, so callers can degrade — would report itself healthy
|
|
99
|
+
// on exactly the outage this route exists to catch.
|
|
100
|
+
if (text === "") {
|
|
101
|
+
throw new Error("the app's own path returned empty content — no output was produced");
|
|
102
|
+
}
|
|
103
|
+
const fresh = {
|
|
104
|
+
ok: true,
|
|
105
|
+
...(asked.id ? { servedBy: asked.id } : {}),
|
|
106
|
+
answer: text,
|
|
107
|
+
ms: now() - started,
|
|
108
|
+
cached: false,
|
|
109
|
+
};
|
|
110
|
+
lastOk = { at: now(), result: fresh };
|
|
111
|
+
return fresh;
|
|
112
|
+
}
|
|
93
113
|
// Resolved here, not at construction, and only on a real probe — so a
|
|
94
114
|
// monitor polling this route does not also poll whatever backs it.
|
|
95
115
|
const resolved = options.resolveChain ? await options.resolveChain() : options.chain;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitbaum/ai-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "One install for the AI layer of an app: which model to call, what to do when the vendor retires it, how to walk the fallback chain and know when none of it worked, how to read the three kinds of 429, a fair daily budget across users, headless AI form filling — and now the model registry (one SSOT for every callable id, with the paid/free boundary as a field) and the grounding harness (facts, contract, deterministic fabrication check).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mao Nakamoto",
|
package/src/complete.ts
CHANGED
|
@@ -449,6 +449,26 @@ export async function complete(options: CompleteOptions): Promise<CompleteResult
|
|
|
449
449
|
|
|
450
450
|
if (failure.kind === "daily") deadProviders.add(link.provider.id);
|
|
451
451
|
|
|
452
|
+
// A REJECTED KEY is a verdict about the VENDOR, not the model.
|
|
453
|
+
//
|
|
454
|
+
// 401/403 says "not you". Every remaining link at this provider presents
|
|
455
|
+
// the identical credential, so walking them spends a request each to be
|
|
456
|
+
// told the same thing — and then reports "all 5 links failed", which
|
|
457
|
+
// reads as an outage at someone else's shop and sends the reader looking
|
|
458
|
+
// for one. The fact worth surfacing is that a key this app holds was
|
|
459
|
+
// refused.
|
|
460
|
+
//
|
|
461
|
+
// Crossing to the NEXT vendor still happens: that is a different key, and
|
|
462
|
+
// the whole reason the chain spans vendors.
|
|
463
|
+
//
|
|
464
|
+
// Deliberately narrow. A 404 is a retired id, a 5xx is a vendor being
|
|
465
|
+
// unwell, a capacity 429 is a busy minute — all three are answered by
|
|
466
|
+
// asking a different model, and widening this skip to cover them would
|
|
467
|
+
// quietly turn the chain back into the pin it replaced.
|
|
468
|
+
if (failure.status === 401 || failure.status === 403) {
|
|
469
|
+
deadProviders.add(link.provider.id);
|
|
470
|
+
}
|
|
471
|
+
|
|
452
472
|
// The caller cancelled — the request they were waiting on is gone. Walking
|
|
453
473
|
// the rest of the chain now would spend their daily budget on an answer
|
|
454
474
|
// nobody will read, and would report "every vendor failed" about vendors
|
package/src/liveness.ts
CHANGED
|
@@ -96,6 +96,33 @@ export interface LivenessOptions extends Omit<
|
|
|
96
96
|
*/
|
|
97
97
|
resolveChain?: () =>
|
|
98
98
|
Link[] | { chain: Link[]; env?: Env } | Promise<Link[] | { chain: Link[]; env?: Env }>;
|
|
99
|
+
/**
|
|
100
|
+
* Make the call YOURSELF, using the app's own path. Takes precedence over
|
|
101
|
+
* `chain` and `resolveChain`.
|
|
102
|
+
*
|
|
103
|
+
* Not every app can hand over a `Link[]`. Four in this fleet cannot: one
|
|
104
|
+
* builds its chain from its own provider CLASSES, one from a registry it
|
|
105
|
+
* deliberately owns (its BYOK list includes paid ids), one talks to a single
|
|
106
|
+
* operator-configured endpoint chosen for data residency, and one keeps a
|
|
107
|
+
* provider layer that predates this package. Requiring a chain would have
|
|
108
|
+
* meant either rewriting those or leaving them with no probe at all — and
|
|
109
|
+
* "no probe" is what left seven apps unable to answer "does the AI work?"
|
|
110
|
+
* after a fleet-wide refactor.
|
|
111
|
+
*
|
|
112
|
+
* There is a second, better reason. A probe built from a chain this module
|
|
113
|
+
* assembles tests A path; `ask` tests THE path — the same function the app's
|
|
114
|
+
* real features call. That is strictly stronger evidence, and it means the
|
|
115
|
+
* probe cannot quietly drift away from the code it is meant to vouch for.
|
|
116
|
+
*
|
|
117
|
+
* Everything else still applies: it runs only on an explicit, authorised
|
|
118
|
+
* probe, a success is cached, and a failure never is. Return the text the
|
|
119
|
+
* model produced and, if you have it, the `provider/model` that served it.
|
|
120
|
+
*
|
|
121
|
+
* An empty or whitespace-only `text` is treated as a FAILURE, for the same
|
|
122
|
+
* reason `complete()` treats it as one: a 200 carrying nothing is the
|
|
123
|
+
* failure most likely to be reported as success.
|
|
124
|
+
*/
|
|
125
|
+
ask?: () => Promise<{ text: string; id?: string }>;
|
|
99
126
|
/** Injected for tests. Defaults to `Date.now`. */
|
|
100
127
|
now?: () => number;
|
|
101
128
|
}
|
|
@@ -165,6 +192,27 @@ export function createLivenessProbe(options: LivenessOptions = {}): LivenessProb
|
|
|
165
192
|
|
|
166
193
|
const started = now();
|
|
167
194
|
try {
|
|
195
|
+
if (options.ask) {
|
|
196
|
+
const asked = await options.ask();
|
|
197
|
+
const text = asked.text.trim();
|
|
198
|
+
// Same rule as `complete()`: a 200 carrying nothing is not an answer.
|
|
199
|
+
// Without this, an app whose own path returns "" on failure — several
|
|
200
|
+
// do, by design, so callers can degrade — would report itself healthy
|
|
201
|
+
// on exactly the outage this route exists to catch.
|
|
202
|
+
if (text === "") {
|
|
203
|
+
throw new Error("the app's own path returned empty content — no output was produced");
|
|
204
|
+
}
|
|
205
|
+
const fresh: LivenessResult = {
|
|
206
|
+
ok: true,
|
|
207
|
+
...(asked.id ? { servedBy: asked.id } : {}),
|
|
208
|
+
answer: text,
|
|
209
|
+
ms: now() - started,
|
|
210
|
+
cached: false,
|
|
211
|
+
};
|
|
212
|
+
lastOk = { at: now(), result: fresh };
|
|
213
|
+
return fresh;
|
|
214
|
+
}
|
|
215
|
+
|
|
168
216
|
// Resolved here, not at construction, and only on a real probe — so a
|
|
169
217
|
// monitor polling this route does not also poll whatever backs it.
|
|
170
218
|
const resolved = options.resolveChain ? await options.resolveChain() : options.chain;
|