@bitbaum/ai-kit 0.13.0 → 0.15.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.
@@ -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
  }
@@ -119,8 +149,15 @@ export interface AiHealthHandlerOptions extends LivenessOptions {
119
149
  * When absent, the handler NEVER probes — it only reports passive health.
120
150
  * That default is deliberate: an app that forgets to configure a secret gets
121
151
  * a route that cannot spend money, rather than an open endpoint that can.
152
+ *
153
+ * Pass a FUNCTION to read it per request. A handler is normally built once
154
+ * and reused (its cache has to live somewhere), so a plain string is captured
155
+ * at that moment — which means the secret is whatever the environment held on
156
+ * the first request, and rotating it needs a process restart. A getter also
157
+ * makes the route testable: with a captured string, the first test that runs
158
+ * without a secret configured pins every later one to 501.
122
159
  */
123
- secret?: string;
160
+ secret?: string | (() => string | undefined);
124
161
  /** Passive health to report alongside. Optional. */
125
162
  health?: HealthTracker;
126
163
  }
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;
@@ -169,17 +189,20 @@ export function createAiHealthHandler(options = {}) {
169
189
  if (!wantsProbe) {
170
190
  return json(200, { probed: false, ...passive });
171
191
  }
192
+ // Read per request when a getter was given, so rotating the secret does not
193
+ // need a restart and a route built before the env was set is not stuck.
194
+ const expected = typeof secret === "function" ? secret() : secret;
172
195
  // No secret configured means probing is switched off, which is a different
173
196
  // answer from "your secret is wrong" — say so, rather than implying the
174
197
  // caller could retry with a better credential.
175
- if (!secret) {
198
+ if (!expected) {
176
199
  return json(501, {
177
200
  probed: false,
178
201
  error: "Probing is not configured on this deployment (no secret set).",
179
202
  ...passive,
180
203
  });
181
204
  }
182
- if (!offered || !timingSafeEqual(offered, secret)) {
205
+ if (!offered || !timingSafeEqual(offered, expected)) {
183
206
  return json(401, { probed: false, error: "Bad or missing probe secret.", ...passive });
184
207
  }
185
208
  const result = await probe.run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/ai-kit",
3
- "version": "0.13.0",
3
+ "version": "0.15.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/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;
@@ -229,8 +277,15 @@ export interface AiHealthHandlerOptions extends LivenessOptions {
229
277
  * When absent, the handler NEVER probes — it only reports passive health.
230
278
  * That default is deliberate: an app that forgets to configure a secret gets
231
279
  * a route that cannot spend money, rather than an open endpoint that can.
280
+ *
281
+ * Pass a FUNCTION to read it per request. A handler is normally built once
282
+ * and reused (its cache has to live somewhere), so a plain string is captured
283
+ * at that moment — which means the secret is whatever the environment held on
284
+ * the first request, and rotating it needs a process restart. A getter also
285
+ * makes the route testable: with a captured string, the first test that runs
286
+ * without a secret configured pins every later one to 501.
232
287
  */
233
- secret?: string;
288
+ secret?: string | (() => string | undefined);
234
289
  /** Passive health to report alongside. Optional. */
235
290
  health?: HealthTracker;
236
291
  }
@@ -268,17 +323,21 @@ export function createAiHealthHandler(
268
323
  return json(200, { probed: false, ...passive });
269
324
  }
270
325
 
326
+ // Read per request when a getter was given, so rotating the secret does not
327
+ // need a restart and a route built before the env was set is not stuck.
328
+ const expected = typeof secret === "function" ? secret() : secret;
329
+
271
330
  // No secret configured means probing is switched off, which is a different
272
331
  // answer from "your secret is wrong" — say so, rather than implying the
273
332
  // caller could retry with a better credential.
274
- if (!secret) {
333
+ if (!expected) {
275
334
  return json(501, {
276
335
  probed: false,
277
336
  error: "Probing is not configured on this deployment (no secret set).",
278
337
  ...passive,
279
338
  });
280
339
  }
281
- if (!offered || !timingSafeEqual(offered, secret)) {
340
+ if (!offered || !timingSafeEqual(offered, expected)) {
282
341
  return json(401, { probed: false, error: "Bad or missing probe secret.", ...passive });
283
342
  }
284
343