@absolutejs/ai 0.6.3 → 0.6.4

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/CHANGELOG.md CHANGED
@@ -6,12 +6,15 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
- ## 0.6.3 — 2026-10-22
9
+ ## 0.6.4 — 2026-10-22
10
+
11
+ ### Added
12
+
13
+ - **Stream approved fallback models before output or tool execution** (`streamWithProviderFallback`)
10
14
 
11
15
  ### Changed
12
16
 
13
- - **Accepts @absolutejs/sync 3**
14
- The `@absolutejs/sync` peer range stopped below 3, so an app on sync 3 (built against Elysia 2.0.0-rc.0) installed a second copy for ai. sync 3.0.0 changed no API, so the range now runs from 2.14.0 below 4.
17
+ - **Expose provider billing rejection and credential-scoped suppression** (`ProviderError`, `ProviderErrorInit`)
15
18
 
16
19
  ## 0.6.2 — 2026-10-22
17
20
 
package/README.md CHANGED
@@ -590,3 +590,6 @@ progress/CTA semantics. A funding pause offers Add credits; only confirmed
590
590
  funding changes it to Continue research. Completed work offers View results;
591
591
  uncertain work offers Check status, never replay. The host persists the work ID,
592
592
  polls or subscribes to actual progress, and binds actions to authenticated routes.
593
+
594
+
595
+ `streamWithProviderFallback` accepts approved model IDs, an individually metered `stream(model)` callback, and an optional `onFallback` hook. It never restarts after a chunk or caller cancellation. `ProviderError.billingUnavailable` identifies provider billing failures; `notSent` identifies requests blocked locally. Billing suppression is scoped to the configured provider instance, with a five-minute probe interval.
package/changelog.json CHANGED
@@ -5,13 +5,23 @@
5
5
  {
6
6
  "changes": [
7
7
  {
8
- "detail": "The `@absolutejs/sync` peer range stopped below 3, so an app on sync 3 (built against Elysia 2.0.0-rc.0) installed a second copy for ai. sync 3.0.0 changed no API, so the range now runs from 2.14.0 below 4.",
9
8
  "kind": "changed",
10
- "summary": "Accepts @absolutejs/sync 3"
9
+ "summary": "Expose provider billing rejection and credential-scoped suppression",
10
+ "symbols": [
11
+ "ProviderError",
12
+ "ProviderErrorInit"
13
+ ]
14
+ },
15
+ {
16
+ "kind": "added",
17
+ "summary": "Stream approved fallback models before output or tool execution",
18
+ "symbols": [
19
+ "streamWithProviderFallback"
20
+ ]
11
21
  }
12
22
  ],
13
23
  "date": "2026-10-22",
14
- "version": "0.6.3"
24
+ "version": "0.6.4"
15
25
  },
16
26
  {
17
27
  "changes": [
package/dist/ai/index.js CHANGED
@@ -2233,12 +2233,21 @@ var CONNECTION_ERROR_PATTERNS = [
2233
2233
  ];
2234
2234
  var isAbortError = (err) => err instanceof Error && (err.name === "AbortError" || /\babort(ed)?\b/i.test(err.message));
2235
2235
  var providerStatusPage = (provider) => PROVIDER_STATUS_PAGES[provider] ?? null;
2236
+ var BILLING_ERROR_TYPES = new Set([
2237
+ "credit_balance_exhausted",
2238
+ "insufficient_quota",
2239
+ "billing_hard_limit_reached",
2240
+ "billing_not_active",
2241
+ "insufficient_credits"
2242
+ ]);
2236
2243
 
2237
2244
  class ProviderError extends Error {
2238
2245
  provider;
2239
2246
  status;
2240
2247
  type;
2241
2248
  retryable;
2249
+ billingUnavailable;
2250
+ notSent;
2242
2251
  metadata;
2243
2252
  statusPageUrl;
2244
2253
  constructor(init) {
@@ -2246,8 +2255,10 @@ class ProviderError extends Error {
2246
2255
  this.name = "ProviderError";
2247
2256
  this.provider = init.provider;
2248
2257
  this.status = init.status ?? null;
2249
- this.type = init.type ?? null;
2250
- this.retryable = init.retryable;
2258
+ this.type = init.type ?? (typeof init.metadata?.code === "string" ? init.metadata.code : typeof init.metadata?.type === "string" ? init.metadata.type : null);
2259
+ this.billingUnavailable = BILLING_ERROR_TYPES.has(this.type ?? "") || init.status === 402;
2260
+ this.notSent = init.notSent === true;
2261
+ this.retryable = !this.billingUnavailable && init.retryable;
2251
2262
  this.metadata = init.metadata;
2252
2263
  this.statusPageUrl = providerStatusPage(init.provider);
2253
2264
  }
@@ -2332,6 +2343,7 @@ var errorPayload = (error) => {
2332
2343
  message: providerError.message,
2333
2344
  provider: providerError.provider,
2334
2345
  retryable: providerError.retryable,
2346
+ notSent: providerError.notSent,
2335
2347
  status: providerError.status,
2336
2348
  type: providerError.type
2337
2349
  };
@@ -2446,6 +2458,7 @@ var parseRemoteStream = async function* (response) {
2446
2458
  const parsed = JSON.parse(data);
2447
2459
  if (event === "error") {
2448
2460
  throw new ProviderError({
2461
+ notSent: "notSent" in parsed && parsed.notSent === true,
2449
2462
  message: "message" in parsed && typeof parsed.message === "string" ? parsed.message : "Remote provider stream failed",
2450
2463
  provider: "provider" in parsed && typeof parsed.provider === "string" ? parsed.provider : "remote",
2451
2464
  retryable: "retryable" in parsed && typeof parsed.retryable === "boolean" ? parsed.retryable : true,
@@ -4894,6 +4907,7 @@ var circuitOpen = (provider) => {
4894
4907
  message: `${provider} API is reported down${record.external.reason ? `: ${record.external.reason}` : ""}`,
4895
4908
  provider,
4896
4909
  retryable: true,
4910
+ notSent: true,
4897
4911
  status: null,
4898
4912
  type: record.external.indicator
4899
4913
  });
@@ -4909,11 +4923,16 @@ var circuitOpen = (provider) => {
4909
4923
  message: `${provider} is temporarily unavailable (circuit open after ${record.consecutiveFailures} failures): ${detail}`,
4910
4924
  provider,
4911
4925
  retryable: true,
4926
+ notSent: true,
4912
4927
  status: record.lastError?.status ?? null,
4913
4928
  type: record.lastError?.type ?? null
4914
4929
  });
4915
4930
  };
4916
4931
  var withResilience = (provider, providerName = "unknown") => {
4932
+ let billingFailure = null;
4933
+ let billingRetryAt = 0;
4934
+ let billingProbe = false;
4935
+ const billingCooldownMs = 5 * 60000;
4917
4936
  const attempt = async function* (params, attemptNo) {
4918
4937
  let yielded = false;
4919
4938
  try {
@@ -4921,9 +4940,16 @@ var withResilience = (provider, providerName = "unknown") => {
4921
4940
  yielded = true;
4922
4941
  yield chunk;
4923
4942
  }
4943
+ billingFailure = null;
4944
+ billingProbe = false;
4924
4945
  noteSuccess(providerName);
4925
4946
  } catch (err) {
4926
4947
  const providerError = ProviderError.from(err, providerName);
4948
+ if (providerError.billingUnavailable) {
4949
+ billingFailure = providerError;
4950
+ billingRetryAt = Date.now() + billingCooldownMs;
4951
+ }
4952
+ billingProbe = false;
4927
4953
  const canRetry = !yielded && providerError.retryable && attemptNo < (params.maxRetries === undefined ? config.maxRetries : Math.max(0, Math.min(config.maxRetries, params.maxRetries))) && !params.signal?.aborted;
4928
4954
  if (canRetry) {
4929
4955
  await sleep(backoffDelay(attemptNo), params.signal);
@@ -4938,6 +4964,21 @@ var withResilience = (provider, providerName = "unknown") => {
4938
4964
  return {
4939
4965
  ...provider,
4940
4966
  stream: (params) => {
4967
+ if (billingFailure && (Date.now() < billingRetryAt || billingProbe)) {
4968
+ const failure = billingFailure;
4969
+ return async function* () {
4970
+ throw new ProviderError({
4971
+ provider: providerName,
4972
+ message: "Provider billing is unavailable",
4973
+ type: failure.type,
4974
+ status: failure.status,
4975
+ retryable: false,
4976
+ notSent: true
4977
+ });
4978
+ }();
4979
+ }
4980
+ if (billingFailure)
4981
+ billingProbe = true;
4941
4982
  const tripped = circuitOpen(providerName);
4942
4983
  if (tripped) {
4943
4984
  return async function* () {
@@ -9228,6 +9269,29 @@ var createAIProviderRouter = (routes) => {
9228
9269
  stream: (params) => resolve(params).stream(params)
9229
9270
  };
9230
9271
  };
9272
+ // src/ai/providerFallback.ts
9273
+ async function* streamWithProviderFallback(options) {
9274
+ const models = [...new Set(options.models)];
9275
+ if (!models.length)
9276
+ throw new Error("At least one approved model is required");
9277
+ for (const [index, model] of models.entries()) {
9278
+ options.signal?.throwIfAborted();
9279
+ let emitted = false;
9280
+ try {
9281
+ for await (const chunk of options.stream(model)) {
9282
+ emitted = true;
9283
+ yield chunk;
9284
+ }
9285
+ return;
9286
+ } catch (error) {
9287
+ options.signal?.throwIfAborted();
9288
+ const next = models[index + 1];
9289
+ if (emitted || !next || !(error instanceof ProviderError) || !(error.billingUnavailable || error.retryable || error.notSent))
9290
+ throw error;
9291
+ await options.onFallback?.({ from: model, to: next, error });
9292
+ }
9293
+ }
9294
+ }
9231
9295
  export {
9232
9296
  AIInputError,
9233
9297
  BUILTIN_UI_CARDS,
@@ -9339,6 +9403,7 @@ export {
9339
9403
  streamAIToSSE,
9340
9404
  streamAIWithTools,
9341
9405
  streamWithAIContext,
9406
+ streamWithProviderFallback,
9342
9407
  tableCard,
9343
9408
  toolOutputText,
9344
9409
  verifyOpenRouterWebhookSignature,
@@ -9346,5 +9411,5 @@ export {
9346
9411
  xai
9347
9412
  };
9348
9413
 
9349
- //# debugId=577984AE55403A4664756E2164756E21
9414
+ //# debugId=15DF9981B943EAAB64756E2164756E21
9350
9415
  //# sourceMappingURL=index.js.map