ai-retry 1.9.0 → 1.9.1

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/README.md CHANGED
@@ -1183,6 +1183,13 @@ In the second case, errors during stream processing will not always be retried,
1183
1183
  > [!IMPORTANT]
1184
1184
  > **Streaming limitation:** Retries and fallbacks only apply before the first content chunk is emitted. Once streaming begins delivering content, the response is committed to the current model. Mid-stream errors will propagate to the caller rather than triggering a fallback. If reliable retries are critical for your use case, consider using `generateText` instead of `streamText`.
1185
1185
 
1186
+ #### Preamble buffering
1187
+
1188
+ Every stream begins with a non-content preamble (`stream-start`, then optionally `response-metadata` and `text-start` / `reasoning-start`) that providers emit as soon as the response headers arrive, before any content flows. Because a retry can still happen during this window, `ai-retry` does not forward the preamble immediately. It buffers the leading non-content parts and flushes them only when the first content chunk arrives (or when the stream finishes with no content). If a retry fires before any content, the buffered preamble is discarded and replaced by the fallback's, so the consumer always sees exactly one preamble — the one belonging to the model that actually produced the output, with its own `warnings` and `response-metadata`. Without this, a fallback's `stream-start` would be emitted a second time after the primary's, which some consumers (e.g. `streamText`) reject.
1189
+
1190
+ > [!NOTE]
1191
+ > One side effect: the consumer's "stream started" signal now arrives at first-content time rather than when the response headers arrive (typically a sub-second difference). For UIs that show a typing indicator off `stream-start` this is negligible.
1192
+
1186
1193
  ### API Reference
1187
1194
 
1188
1195
  #### `createRetryable(options: RetryableModelOptions): LanguageModelV3 | EmbeddingModelV3 | ImageModelV3`
@@ -1202,6 +1202,16 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
1202
1202
  let capturedWarnings = [];
1203
1203
  let capturedResponseMetadata = {};
1204
1204
  /**
1205
+ * Buffer for the leading non-content parts (`stream-start`,
1206
+ * `response-metadata`, `text-start`, `reasoning-start`, …) of this
1207
+ * attempt. While no content has been forwarded the preamble is held
1208
+ * here rather than enqueued, so a pre-content retry can discard it
1209
+ * and the consumer sees exactly one preamble — the one belonging to
1210
+ * the model that actually produced the output. Reset per attempt;
1211
+ * flushed on the first content part or at completion.
1212
+ */
1213
+ let preambleBuffer = [];
1214
+ /**
1205
1215
  * Set when a `finish` part triggers a retry decision. Causes the
1206
1216
  * inner read loop to exit without enqueuing the finish part, and
1207
1217
  * the outer loop to re-stream against the next model.
@@ -1270,11 +1280,25 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
1270
1280
  }
1271
1281
  }
1272
1282
  /**
1273
- * Mark that streaming has started once we receive actual content
1283
+ * Mark that streaming has started once we receive actual
1284
+ * content. On the first content part, flush this attempt's
1285
+ * buffered preamble (in order) ahead of the content, then
1286
+ * forward normally from here on.
1274
1287
  */
1275
- if (isStreamContentPart(value)) isStreaming = true;
1276
- /**
1277
- * Enqueue the chunk to the consumer of the stream
1288
+ if (isStreamContentPart(value)) {
1289
+ isStreaming = true;
1290
+ for (const buffered of preambleBuffer) controller.enqueue(buffered);
1291
+ preambleBuffer = [];
1292
+ controller.enqueue(value);
1293
+ } else if (!isStreaming)
1294
+ /**
1295
+ * Pre-content part: buffer it so a pre-content retry can
1296
+ * replace it with the next attempt's preamble.
1297
+ */
1298
+ preambleBuffer.push(value);
1299
+ else
1300
+ /**
1301
+ * Content already flowing: forward directly.
1278
1302
  */
1279
1303
  controller.enqueue(value);
1280
1304
  }
@@ -1306,7 +1330,13 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
1306
1330
  currentRetry,
1307
1331
  recorder
1308
1332
  });
1309
- await reader?.cancel();
1333
+ /**
1334
+ * Cancelling a reader whose stream has already errored (e.g.
1335
+ * a mid-stream `controller.error`) rejects with that stored
1336
+ * error. Swallow it: the retry already succeeded and that
1337
+ * rejection must not abort the wrapped stream.
1338
+ */
1339
+ await reader?.cancel().catch(() => {});
1310
1340
  result = retriedResult.result;
1311
1341
  attempts = retriedResult.attempts;
1312
1342
  finalCallOptions = retriedResult.callOptions;
@@ -1318,6 +1348,13 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
1318
1348
  outcome: "success",
1319
1349
  finishReason: streamFinishReason
1320
1350
  });
1351
+ /**
1352
+ * A stream that completes with no content part still has its
1353
+ * preamble buffered. Flush it so a zero-content completion emits
1354
+ * its `stream-start` (and any metadata/finish) before closing.
1355
+ */
1356
+ for (const buffered of preambleBuffer) controller.enqueue(buffered);
1357
+ preambleBuffer = [];
1321
1358
  controller.close();
1322
1359
  break;
1323
1360
  } catch (error) {
@@ -1408,9 +1445,13 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
1408
1445
  recorder
1409
1446
  });
1410
1447
  /**
1411
- * Cancel the previous reader and stream if we are retrying
1448
+ * Cancel the previous reader and stream if we are retrying.
1449
+ * Cancelling a reader whose stream has already errored (e.g. a
1450
+ * mid-stream `controller.error`) rejects with that stored
1451
+ * error. Swallow it: the retry already succeeded and that
1452
+ * rejection must not abort the wrapped stream.
1412
1453
  */
1413
- await reader?.cancel();
1454
+ await reader?.cancel().catch(() => {});
1414
1455
  result = retriedResult.result;
1415
1456
  attempts = retriedResult.attempts;
1416
1457
  finalCallOptions = retriedResult.callOptions;
@@ -1,4 +1,4 @@
1
- import { t as createRetryable$1 } from "../../create-retryable-model-VgywojZt.mjs";
1
+ import { t as createRetryable$1 } from "../../create-retryable-model-Ch2cqC3Z.mjs";
2
2
  import "../../error-CaTT-xX8.mjs";
3
3
  import { aborted, error, httpStatus, timeout } from "./retryables/index.mjs";
4
4
 
@@ -1,4 +1,4 @@
1
- import { t as createRetryable$1 } from "../../create-retryable-model-VgywojZt.mjs";
1
+ import { t as createRetryable$1 } from "../../create-retryable-model-Ch2cqC3Z.mjs";
2
2
  import "../../error-CaTT-xX8.mjs";
3
3
  import { a as noImage, i as timeout, n as error, r as httpStatus, t as aborted } from "../../retryables-CPAbu_M3.mjs";
4
4
 
@@ -1,4 +1,4 @@
1
- import { t as createRetryable$1 } from "../../create-retryable-model-VgywojZt.mjs";
1
+ import { t as createRetryable$1 } from "../../create-retryable-model-Ch2cqC3Z.mjs";
2
2
  import "../../error-CaTT-xX8.mjs";
3
3
  import { a as result, i as httpStatus, n as error, o as schemaInvalid, r as finishReason, s as timeout, t as aborted } from "../../retryables-M5l_6w9k.mjs";
4
4
 
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as getModelKey, t as createRetryable } from "./create-retryable-model-VgywojZt.mjs";
1
+ import { n as getModelKey, t as createRetryable } from "./create-retryable-model-Ch2cqC3Z.mjs";
2
2
  import { n as isErrorAttempt, s as isResultAttempt } from "./guards-D8UJtxDK.mjs";
3
3
 
4
4
  export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Retry and fallback mechanisms for AI SDK",
5
5
  "keywords": [
6
6
  "ai",