@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 +6 -3
- package/README.md +3 -0
- package/changelog.json +13 -3
- package/dist/ai/index.js +68 -3
- package/dist/ai/index.js.map +8 -7
- package/dist/ai/providers/anthropic.js +42 -3
- package/dist/ai/providers/anthropic.js.map +4 -4
- package/dist/ai/providers/gemini.js +42 -3
- package/dist/ai/providers/gemini.js.map +4 -4
- package/dist/ai/providers/ollama.js +42 -3
- package/dist/ai/providers/ollama.js.map +4 -4
- package/dist/ai/providers/openai.js +42 -3
- package/dist/ai/providers/openai.js.map +4 -4
- package/dist/ai/providers/openaiCompatible.js +42 -3
- package/dist/ai/providers/openaiCompatible.js.map +4 -4
- package/dist/ai/providers/openaiResponses.js +42 -3
- package/dist/ai/providers/openaiResponses.js.map +4 -4
- package/dist/ai/providers/openrouter.js +42 -3
- package/dist/ai/providers/openrouter.js.map +4 -4
- package/dist/src/ai/errors/providerError.d.ts +4 -0
- package/dist/src/ai/index.d.ts +1 -0
- package/dist/src/ai/providerFallback.d.ts +14 -0
- package/package.json +3 -3
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.
|
|
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
|
-
- **
|
|
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": "
|
|
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.
|
|
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.
|
|
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=
|
|
9414
|
+
//# debugId=15DF9981B943EAAB64756E2164756E21
|
|
9350
9415
|
//# sourceMappingURL=index.js.map
|