@bitbaum/ai-kit 1.5.0 → 1.6.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/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export { type ChatMessage, type ContentPart, type ToolCall, type CompleteOptions
58
58
  export { type HealthStatus, type Health, type HealthTrackerOptions, type HealthTracker, createHealthTracker, } from "./health.js";
59
59
  export { type LivenessResult, type LivenessOptions, type LivenessProbe, type AiHealthHandlerOptions, createLivenessProbe, createAiHealthHandler, } from "./liveness.js";
60
60
  export { type RateLimitKind, classifyRateLimit, retryAfterSeconds, humanizeWait, rateLimitMessage, } from "./limits.js";
61
- export { type QuotaScope, type QuotaWindow, type QuotaReading, type HeaderBag, readQuota, readingFromRefusal, parseResetAt, answersRemaining, } from "./meter.js";
61
+ export { type QuotaScope, type QuotaWindow, type QuotaReading, type HeaderBag, readQuota, readingFromRefusal, readingFromRefusalBody, parseResetAt, answersRemaining, } from "./meter.js";
62
62
  export { type ParsedToolCall, TEXT_TOOL_PROTOCOL_HINT, parseTextToolCalls, stripToolCallLines, safeJsonObject, toolNamesFrom, } from "./tool-protocol.js";
63
63
  export { type PoolId, type RungId, type TierPolicy, type AiPolicy, type UserState, type WallOption, type Wall, type Decision, DEFAULT_LADDER, decide, shouldSurface, nextUtcReset, } from "./policy.js";
64
64
  export { DAY_SECONDS, DEFAULT_BURST, type ShareInput, type ShareReason, type ShareDecision, fairShare, utcDayElapsed, utcDayKey, } from "./fair-share.js";
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ export { LinkFailure, complete, linkId, } from "./complete.js";
61
61
  export { createHealthTracker, } from "./health.js";
62
62
  export { createLivenessProbe, createAiHealthHandler, } from "./liveness.js";
63
63
  export { classifyRateLimit, retryAfterSeconds, humanizeWait, rateLimitMessage, } from "./limits.js";
64
- export { readQuota, readingFromRefusal, parseResetAt, answersRemaining, } from "./meter.js";
64
+ export { readQuota, readingFromRefusal, readingFromRefusalBody, parseResetAt, answersRemaining, } from "./meter.js";
65
65
  export { TEXT_TOOL_PROTOCOL_HINT, parseTextToolCalls, stripToolCallLines, safeJsonObject, toolNamesFrom, } from "./tool-protocol.js";
66
66
  export { DEFAULT_LADDER, decide, shouldSurface, nextUtcReset, } from "./policy.js";
67
67
  export { DAY_SECONDS, DEFAULT_BURST, fairShare, utcDayElapsed, utcDayKey, } from "./fair-share.js";
package/dist/meter.d.ts CHANGED
@@ -102,6 +102,7 @@ export declare function readQuota(headers: HeaderBag, link: Link, now?: number):
102
102
  * disagreeing with it.
103
103
  */
104
104
  export declare function readingFromRefusal(link: Link, retryAfterSec: number | null, scope?: QuotaScope, now?: number): QuotaReading;
105
+ export declare function readingFromRefusalBody(link: Link, body: string, retryAfterSec?: number | null, now?: number): QuotaReading | null;
105
106
  /**
106
107
  * Turn a remaining-token count into the unit a person thinks in.
107
108
  *
package/dist/meter.js CHANGED
@@ -196,6 +196,64 @@ export function readingFromRefusal(link, retryAfterSec, scope = "requests", now
196
196
  observedAt: now,
197
197
  };
198
198
  }
199
+ /**
200
+ * The counter a vendor discloses ONLY when it refuses.
201
+ *
202
+ * ── WHY A BODY PARSER EARNS ITS KEEP ─────────────────────────────────────────
203
+ *
204
+ * Headers do not describe every limit a vendor enforces. Groq publishes its
205
+ * per-minute token window and its per-day REQUEST count as headers, and meters
206
+ * a third limit — tokens per DAY — that appears in no header at all. It is
207
+ * stated once, in prose, in the body of the 429 that enforces it:
208
+ *
209
+ * Rate limit reached for model `openai/gpt-oss-20b` in organization `org_…`
210
+ * service tier `on_demand` on tokens per day (TPD): Limit 200000,
211
+ * Used 199773, Requested 571. Please try again in 2m28.608s.
212
+ *
213
+ * Observed on production 2026-09-13. Every header the same response carried
214
+ * looked healthy — 2,672 of 8,000 tokens left this minute, 999 of 1,000
215
+ * requests left today — while the account was locked out of the model for the
216
+ * rest of the day. A dashboard fed only by headers therefore reported a
217
+ * working provider throughout an outage, which is precisely the failure the
218
+ * top of this file exists to prevent, arriving through a door it left open.
219
+ *
220
+ * ── STILL NEVER INVENTS ──────────────────────────────────────────────────────
221
+ *
222
+ * Returns null unless the body states the numbers. An unparsed refusal is the
223
+ * "did not say" state, and the caller should fall back to `readingFromRefusal`,
224
+ * which records the one fact a 429 always carries: spent, now.
225
+ */
226
+ const REFUSAL_COUNTER = /on\s+(tokens|requests)\s+per\s+(minute|day)\s*\([^)]*\)\s*:\s*Limit\s+(\d+)\s*,\s*Used\s+(\d+)/i;
227
+ export function readingFromRefusalBody(link, body, retryAfterSec = null, now = Date.now()) {
228
+ const m = REFUSAL_COUNTER.exec(body);
229
+ if (!m)
230
+ return null;
231
+ const [, rawScope, rawWindow, rawLimit, rawUsed] = m;
232
+ if (!rawScope || !rawWindow || !rawLimit || !rawUsed)
233
+ return null;
234
+ const scope = rawScope.toLowerCase();
235
+ const window = rawWindow.toLowerCase();
236
+ const limit = Number(rawLimit);
237
+ const used = Number(rawUsed);
238
+ if (!Number.isFinite(limit) || !Number.isFinite(used))
239
+ return null;
240
+ return {
241
+ provider: link.provider.id,
242
+ model: link.model,
243
+ scope,
244
+ // Straight from the vendor's own sentence, so unlike a header name this
245
+ // window is not a guess and must not be overridden by the provider profile.
246
+ window,
247
+ limit,
248
+ // The refusal names what was consumed, not what is left. Clamped because a
249
+ // vendor counting a rejected request against the total would otherwise
250
+ // produce a negative "remaining" and a dashboard that renders nonsense.
251
+ remaining: Math.max(0, limit - used),
252
+ resetAt: retryAfterSec === null ? null : now + retryAfterSec * 1000,
253
+ source: "429-body",
254
+ observedAt: now,
255
+ };
256
+ }
199
257
  /**
200
258
  * Turn a remaining-token count into the unit a person thinks in.
201
259
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/ai-kit",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "license": "MIT",
5
5
  "author": "Mao Nakamoto",
6
6
  "homepage": "https://github.com/bitbaum/ai-kit#readme",
package/src/index.ts CHANGED
@@ -141,6 +141,7 @@ export {
141
141
  type HeaderBag,
142
142
  readQuota,
143
143
  readingFromRefusal,
144
+ readingFromRefusalBody,
144
145
  parseResetAt,
145
146
  answersRemaining,
146
147
  } from "./meter.js";
package/src/meter.ts CHANGED
@@ -248,6 +248,72 @@ export function readingFromRefusal(
248
248
  };
249
249
  }
250
250
 
251
+ /**
252
+ * The counter a vendor discloses ONLY when it refuses.
253
+ *
254
+ * ── WHY A BODY PARSER EARNS ITS KEEP ─────────────────────────────────────────
255
+ *
256
+ * Headers do not describe every limit a vendor enforces. Groq publishes its
257
+ * per-minute token window and its per-day REQUEST count as headers, and meters
258
+ * a third limit — tokens per DAY — that appears in no header at all. It is
259
+ * stated once, in prose, in the body of the 429 that enforces it:
260
+ *
261
+ * Rate limit reached for model `openai/gpt-oss-20b` in organization `org_…`
262
+ * service tier `on_demand` on tokens per day (TPD): Limit 200000,
263
+ * Used 199773, Requested 571. Please try again in 2m28.608s.
264
+ *
265
+ * Observed on production 2026-09-13. Every header the same response carried
266
+ * looked healthy — 2,672 of 8,000 tokens left this minute, 999 of 1,000
267
+ * requests left today — while the account was locked out of the model for the
268
+ * rest of the day. A dashboard fed only by headers therefore reported a
269
+ * working provider throughout an outage, which is precisely the failure the
270
+ * top of this file exists to prevent, arriving through a door it left open.
271
+ *
272
+ * ── STILL NEVER INVENTS ──────────────────────────────────────────────────────
273
+ *
274
+ * Returns null unless the body states the numbers. An unparsed refusal is the
275
+ * "did not say" state, and the caller should fall back to `readingFromRefusal`,
276
+ * which records the one fact a 429 always carries: spent, now.
277
+ */
278
+ const REFUSAL_COUNTER =
279
+ /on\s+(tokens|requests)\s+per\s+(minute|day)\s*\([^)]*\)\s*:\s*Limit\s+(\d+)\s*,\s*Used\s+(\d+)/i;
280
+
281
+ export function readingFromRefusalBody(
282
+ link: Link,
283
+ body: string,
284
+ retryAfterSec: number | null = null,
285
+ now = Date.now(),
286
+ ): QuotaReading | null {
287
+ const m = REFUSAL_COUNTER.exec(body);
288
+ if (!m) return null;
289
+
290
+ const [, rawScope, rawWindow, rawLimit, rawUsed] = m;
291
+ if (!rawScope || !rawWindow || !rawLimit || !rawUsed) return null;
292
+
293
+ const scope = rawScope.toLowerCase() as QuotaScope;
294
+ const window = rawWindow.toLowerCase() as QuotaWindow;
295
+ const limit = Number(rawLimit);
296
+ const used = Number(rawUsed);
297
+ if (!Number.isFinite(limit) || !Number.isFinite(used)) return null;
298
+
299
+ return {
300
+ provider: link.provider.id,
301
+ model: link.model,
302
+ scope,
303
+ // Straight from the vendor's own sentence, so unlike a header name this
304
+ // window is not a guess and must not be overridden by the provider profile.
305
+ window,
306
+ limit,
307
+ // The refusal names what was consumed, not what is left. Clamped because a
308
+ // vendor counting a rejected request against the total would otherwise
309
+ // produce a negative "remaining" and a dashboard that renders nonsense.
310
+ remaining: Math.max(0, limit - used),
311
+ resetAt: retryAfterSec === null ? null : now + retryAfterSec * 1000,
312
+ source: "429-body",
313
+ observedAt: now,
314
+ };
315
+ }
316
+
251
317
  /**
252
318
  * Turn a remaining-token count into the unit a person thinks in.
253
319
  *