@bitbaum/ai-kit 0.11.0 → 0.13.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 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
@@ -39,7 +39,7 @@
39
39
  * is "does the pipe carry water", not "is the model any good".
40
40
  */
41
41
  import { type CompleteOptions } from "./complete.js";
42
- import type { Link } from "./chain.js";
42
+ import type { Link, Env } from "./chain.js";
43
43
  import type { HealthTracker } from "./health.js";
44
44
  export interface LivenessResult {
45
45
  /** Did a model answer? */
@@ -78,9 +78,22 @@ export interface LivenessOptions extends Omit<CompleteOptions, "messages" | "max
78
78
  * It is called only on a real probe, never on a cache hit, so a monitor
79
79
  * polling this route does not also poll the database.
80
80
  *
81
+ * Return `{ chain, env }` when the KEYS move with the chain — an admin
82
+ * screen that stores a per-provider key alongside the model is the ordinary
83
+ * case, and resolving links from the database while reading credentials from
84
+ * a `process.env` captured at construction would have the probe report "no
85
+ * key" for a provider that is configured and working. Returning a bare
86
+ * `Link[]` keeps whatever `env` the probe was built with.
87
+ *
81
88
  * Takes precedence over `chain` when both are given.
82
89
  */
83
- resolveChain?: () => Link[] | Promise<Link[]>;
90
+ resolveChain?: () => Link[] | {
91
+ chain: Link[];
92
+ env?: Env;
93
+ } | Promise<Link[] | {
94
+ chain: Link[];
95
+ env?: Env;
96
+ }>;
84
97
  /** Injected for tests. Defaults to `Date.now`. */
85
98
  now?: () => number;
86
99
  }
package/dist/liveness.js CHANGED
@@ -92,11 +92,17 @@ export function createLivenessProbe(options = {}) {
92
92
  try {
93
93
  // Resolved here, not at construction, and only on a real probe — so a
94
94
  // monitor polling this route does not also poll whatever backs it.
95
- const chain = options.resolveChain ? await options.resolveChain() : options.chain;
95
+ const resolved = options.resolveChain ? await options.resolveChain() : options.chain;
96
+ const chain = Array.isArray(resolved) ? resolved : resolved?.chain;
97
+ // Only when the resolver supplied one. Otherwise the probe keeps the
98
+ // env it was built with, so a bare `Link[]` resolver behaves exactly
99
+ // as it did before this option grew a second shape.
100
+ const env = !Array.isArray(resolved) && resolved?.env !== undefined ? resolved.env : options.env;
96
101
  const result = await complete({
97
102
  timeoutMs: PROBE_TIMEOUT_MS,
98
103
  ...options,
99
104
  chain,
105
+ env,
100
106
  messages: PROBE_MESSAGES,
101
107
  maxTokens: PROBE_MAX_TOKENS,
102
108
  temperature: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/ai-kit",
3
- "version": "0.11.0",
3
+ "version": "0.13.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
@@ -40,7 +40,7 @@
40
40
  */
41
41
 
42
42
  import { complete, type CompleteOptions } from "./complete.js";
43
- import type { Link } from "./chain.js";
43
+ import type { Link, Env } from "./chain.js";
44
44
  import { ChainExhaustedError } from "./attempt.js";
45
45
  import type { HealthTracker } from "./health.js";
46
46
 
@@ -85,9 +85,17 @@ export interface LivenessOptions extends Omit<
85
85
  * It is called only on a real probe, never on a cache hit, so a monitor
86
86
  * polling this route does not also poll the database.
87
87
  *
88
+ * Return `{ chain, env }` when the KEYS move with the chain — an admin
89
+ * screen that stores a per-provider key alongside the model is the ordinary
90
+ * case, and resolving links from the database while reading credentials from
91
+ * a `process.env` captured at construction would have the probe report "no
92
+ * key" for a provider that is configured and working. Returning a bare
93
+ * `Link[]` keeps whatever `env` the probe was built with.
94
+ *
88
95
  * Takes precedence over `chain` when both are given.
89
96
  */
90
- resolveChain?: () => Link[] | Promise<Link[]>;
97
+ resolveChain?: () =>
98
+ Link[] | { chain: Link[]; env?: Env } | Promise<Link[] | { chain: Link[]; env?: Env }>;
91
99
  /** Injected for tests. Defaults to `Date.now`. */
92
100
  now?: () => number;
93
101
  }
@@ -159,12 +167,19 @@ export function createLivenessProbe(options: LivenessOptions = {}): LivenessProb
159
167
  try {
160
168
  // Resolved here, not at construction, and only on a real probe — so a
161
169
  // monitor polling this route does not also poll whatever backs it.
162
- const chain = options.resolveChain ? await options.resolveChain() : options.chain;
170
+ const resolved = options.resolveChain ? await options.resolveChain() : options.chain;
171
+ const chain = Array.isArray(resolved) ? resolved : resolved?.chain;
172
+ // Only when the resolver supplied one. Otherwise the probe keeps the
173
+ // env it was built with, so a bare `Link[]` resolver behaves exactly
174
+ // as it did before this option grew a second shape.
175
+ const env =
176
+ !Array.isArray(resolved) && resolved?.env !== undefined ? resolved.env : options.env;
163
177
 
164
178
  const result = await complete({
165
179
  timeoutMs: PROBE_TIMEOUT_MS,
166
180
  ...options,
167
181
  chain,
182
+ env,
168
183
  messages: PROBE_MESSAGES,
169
184
  maxTokens: PROBE_MAX_TOKENS,
170
185
  temperature: 0,