@bitbaum/ai-kit 0.9.0 → 0.11.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.
@@ -141,6 +141,19 @@ export interface CompleteOptions {
141
141
  tools?: unknown[];
142
142
  /** Extra body fields for a vendor-specific parameter. Merged last, so it can override. */
143
143
  extraBody?: Record<string, unknown>;
144
+ /**
145
+ * Extra request headers. Merged last, so a caller can override `content-type`
146
+ * — but NOT `authorization`, which stays the key this module resolved.
147
+ *
148
+ * Vendors ask for these and quietly change behaviour without them:
149
+ * OpenRouter reads `HTTP-Referer` and `X-Title` for app attribution in its
150
+ * public rankings, and an app that stops sending them simply disappears from
151
+ * that list with no error anywhere. Without this option, adopting `complete`
152
+ * would mean silently dropping them, which is exactly the kind of small,
153
+ * invisible regression that makes a shared engine feel worse than the
154
+ * hand-rolled client it replaced.
155
+ */
156
+ extraHeaders?: Record<string, string>;
144
157
  /** Called on each link's failure before moving on — e.g. to log which id rotted. */
145
158
  onLinkFailure?: (link: Link, error: Error) => void;
146
159
  /** Injected for tests. Defaults to global `fetch`. */
package/dist/complete.js CHANGED
@@ -191,7 +191,15 @@ async function callLink(link, options, key) {
191
191
  try {
192
192
  res = await doFetch(`${link.provider.baseUrl}/chat/completions`, {
193
193
  method: "POST",
194
- headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
194
+ headers: {
195
+ "content-type": "application/json",
196
+ ...options.extraHeaders,
197
+ // Last on purpose. A caller may add or override any header it likes,
198
+ // but not this one: silently sending someone else's credential — or
199
+ // none — would turn a typo in a caller's header map into an auth
200
+ // failure blamed on the vendor.
201
+ authorization: `Bearer ${key}`,
202
+ },
195
203
  body: JSON.stringify(body),
196
204
  signal: deadline.signal,
197
205
  });
@@ -39,6 +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
43
  import type { HealthTracker } from "./health.js";
43
44
  export interface LivenessResult {
44
45
  /** Did a model answer? */
@@ -64,6 +65,22 @@ export interface LivenessOptions extends Omit<CompleteOptions, "messages" | "max
64
65
  * Default 10 minutes. Set 0 to disable caching — only for a test.
65
66
  */
66
67
  minIntervalMs?: number;
68
+ /**
69
+ * Resolve the chain when a probe actually runs, rather than once at
70
+ * construction.
71
+ *
72
+ * For an app whose provider list lives in a DATABASE — an admin screen with
73
+ * enabled/default rows and per-provider keys — a chain fixed at construction
74
+ * is a chain frozen at process start. The probe would then keep reporting on
75
+ * a configuration the operator changed twenty minutes ago, which is the
76
+ * opposite of "the truth about right now".
77
+ *
78
+ * It is called only on a real probe, never on a cache hit, so a monitor
79
+ * polling this route does not also poll the database.
80
+ *
81
+ * Takes precedence over `chain` when both are given.
82
+ */
83
+ resolveChain?: () => Link[] | Promise<Link[]>;
67
84
  /** Injected for tests. Defaults to `Date.now`. */
68
85
  now?: () => number;
69
86
  }
package/dist/liveness.js CHANGED
@@ -90,9 +90,13 @@ export function createLivenessProbe(options = {}) {
90
90
  }
91
91
  const started = now();
92
92
  try {
93
+ // Resolved here, not at construction, and only on a real probe — so a
94
+ // monitor polling this route does not also poll whatever backs it.
95
+ const chain = options.resolveChain ? await options.resolveChain() : options.chain;
93
96
  const result = await complete({
94
97
  timeoutMs: PROBE_TIMEOUT_MS,
95
98
  ...options,
99
+ chain,
96
100
  messages: PROBE_MESSAGES,
97
101
  maxTokens: PROBE_MAX_TOKENS,
98
102
  temperature: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/ai-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.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
@@ -146,6 +146,19 @@ export interface CompleteOptions {
146
146
  tools?: unknown[];
147
147
  /** Extra body fields for a vendor-specific parameter. Merged last, so it can override. */
148
148
  extraBody?: Record<string, unknown>;
149
+ /**
150
+ * Extra request headers. Merged last, so a caller can override `content-type`
151
+ * — but NOT `authorization`, which stays the key this module resolved.
152
+ *
153
+ * Vendors ask for these and quietly change behaviour without them:
154
+ * OpenRouter reads `HTTP-Referer` and `X-Title` for app attribution in its
155
+ * public rankings, and an app that stops sending them simply disappears from
156
+ * that list with no error anywhere. Without this option, adopting `complete`
157
+ * would mean silently dropping them, which is exactly the kind of small,
158
+ * invisible regression that makes a shared engine feel worse than the
159
+ * hand-rolled client it replaced.
160
+ */
161
+ extraHeaders?: Record<string, string>;
149
162
  /** Called on each link's failure before moving on — e.g. to log which id rotted. */
150
163
  onLinkFailure?: (link: Link, error: Error) => void;
151
164
  /** Injected for tests. Defaults to global `fetch`. */
@@ -322,7 +335,15 @@ async function callLink(
322
335
  try {
323
336
  res = await doFetch(`${link.provider.baseUrl}/chat/completions`, {
324
337
  method: "POST",
325
- headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
338
+ headers: {
339
+ "content-type": "application/json",
340
+ ...options.extraHeaders,
341
+ // Last on purpose. A caller may add or override any header it likes,
342
+ // but not this one: silently sending someone else's credential — or
343
+ // none — would turn a typo in a caller's header map into an auth
344
+ // failure blamed on the vendor.
345
+ authorization: `Bearer ${key}`,
346
+ },
326
347
  body: JSON.stringify(body),
327
348
  signal: deadline.signal,
328
349
  });
package/src/liveness.ts CHANGED
@@ -40,6 +40,7 @@
40
40
  */
41
41
 
42
42
  import { complete, type CompleteOptions } from "./complete.js";
43
+ import type { Link } from "./chain.js";
43
44
  import { ChainExhaustedError } from "./attempt.js";
44
45
  import type { HealthTracker } from "./health.js";
45
46
 
@@ -71,6 +72,22 @@ export interface LivenessOptions extends Omit<
71
72
  * Default 10 minutes. Set 0 to disable caching — only for a test.
72
73
  */
73
74
  minIntervalMs?: number;
75
+ /**
76
+ * Resolve the chain when a probe actually runs, rather than once at
77
+ * construction.
78
+ *
79
+ * For an app whose provider list lives in a DATABASE — an admin screen with
80
+ * enabled/default rows and per-provider keys — a chain fixed at construction
81
+ * is a chain frozen at process start. The probe would then keep reporting on
82
+ * a configuration the operator changed twenty minutes ago, which is the
83
+ * opposite of "the truth about right now".
84
+ *
85
+ * It is called only on a real probe, never on a cache hit, so a monitor
86
+ * polling this route does not also poll the database.
87
+ *
88
+ * Takes precedence over `chain` when both are given.
89
+ */
90
+ resolveChain?: () => Link[] | Promise<Link[]>;
74
91
  /** Injected for tests. Defaults to `Date.now`. */
75
92
  now?: () => number;
76
93
  }
@@ -140,9 +157,14 @@ export function createLivenessProbe(options: LivenessOptions = {}): LivenessProb
140
157
 
141
158
  const started = now();
142
159
  try {
160
+ // Resolved here, not at construction, and only on a real probe — so a
161
+ // monitor polling this route does not also poll whatever backs it.
162
+ const chain = options.resolveChain ? await options.resolveChain() : options.chain;
163
+
143
164
  const result = await complete({
144
165
  timeoutMs: PROBE_TIMEOUT_MS,
145
166
  ...options,
167
+ chain,
146
168
  messages: PROBE_MESSAGES,
147
169
  maxTokens: PROBE_MAX_TOKENS,
148
170
  temperature: 0,