@jaypie/llm 1.2.32 → 1.2.34
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/cjs/index.cjs +196 -74
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +20 -1
- package/dist/cjs/operate/retry/createStaleRejectionGuard.d.ts +19 -0
- package/dist/esm/index.js +196 -74
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +20 -1
- package/dist/esm/operate/retry/createStaleRejectionGuard.d.ts +19 -0
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -1199,11 +1199,11 @@ const NOT_RETRYABLE_ERROR_NAMES = [
|
|
|
1199
1199
|
// Patterns (not exact names) so dated variants and future releases are covered
|
|
1200
1200
|
// without code changes — Anthropic is trending toward removing temperature on
|
|
1201
1201
|
// newer Claude models.
|
|
1202
|
-
const MODELS_WITHOUT_TEMPERATURE$
|
|
1202
|
+
const MODELS_WITHOUT_TEMPERATURE$2 = [
|
|
1203
1203
|
/^claude-opus-4-[789]/,
|
|
1204
1204
|
/^claude-opus-[5-9]/,
|
|
1205
1205
|
];
|
|
1206
|
-
function isTemperatureDeprecationError$
|
|
1206
|
+
function isTemperatureDeprecationError$2(error) {
|
|
1207
1207
|
if (!error || typeof error !== "object")
|
|
1208
1208
|
return false;
|
|
1209
1209
|
const err = error;
|
|
@@ -1273,7 +1273,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1273
1273
|
supportsTemperature(model) {
|
|
1274
1274
|
if (this.runtimeNoTemperatureModels.has(model))
|
|
1275
1275
|
return false;
|
|
1276
|
-
return !MODELS_WITHOUT_TEMPERATURE$
|
|
1276
|
+
return !MODELS_WITHOUT_TEMPERATURE$2.some((pattern) => pattern.test(model));
|
|
1277
1277
|
}
|
|
1278
1278
|
supportsStructuredOutput(model) {
|
|
1279
1279
|
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
@@ -1451,7 +1451,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1451
1451
|
return undefined;
|
|
1452
1452
|
// If the model rejected `temperature`, cache it and retry without the param
|
|
1453
1453
|
if (anthropicRequest.temperature !== undefined &&
|
|
1454
|
-
isTemperatureDeprecationError$
|
|
1454
|
+
isTemperatureDeprecationError$2(error)) {
|
|
1455
1455
|
this.rememberModelRejectsTemperature(anthropicRequest.model);
|
|
1456
1456
|
const retryRequest = { ...anthropicRequest };
|
|
1457
1457
|
delete retryRequest.temperature;
|
|
@@ -1512,7 +1512,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1512
1512
|
}
|
|
1513
1513
|
catch (error) {
|
|
1514
1514
|
if (streamRequest.temperature !== undefined &&
|
|
1515
|
-
isTemperatureDeprecationError$
|
|
1515
|
+
isTemperatureDeprecationError$2(error)) {
|
|
1516
1516
|
this.rememberModelRejectsTemperature(streamRequest.model);
|
|
1517
1517
|
streamRequest = {
|
|
1518
1518
|
...streamRequest,
|
|
@@ -2239,14 +2239,24 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
2239
2239
|
// Tool Result Handling
|
|
2240
2240
|
//
|
|
2241
2241
|
formatToolResult(toolCall, result) {
|
|
2242
|
-
// Gemini
|
|
2243
|
-
//
|
|
2242
|
+
// Gemini's `function_response.response` is a protobuf Struct, which only
|
|
2243
|
+
// accepts plain objects. Some models (e.g. gemini-3.1-pro) accept scalar
|
|
2244
|
+
// values silently; gemini-3.1-flash-lite rejects them. Parse the output
|
|
2245
|
+
// and wrap any non-object value (string, number, boolean, array, null)
|
|
2246
|
+
// in `{ result: value }` so every tool result is a valid Struct.
|
|
2244
2247
|
let responseData;
|
|
2245
2248
|
try {
|
|
2246
|
-
|
|
2249
|
+
const parsed = JSON.parse(result.output);
|
|
2250
|
+
if (parsed !== null &&
|
|
2251
|
+
typeof parsed === "object" &&
|
|
2252
|
+
!Array.isArray(parsed)) {
|
|
2253
|
+
responseData = parsed;
|
|
2254
|
+
}
|
|
2255
|
+
else {
|
|
2256
|
+
responseData = { result: parsed };
|
|
2257
|
+
}
|
|
2247
2258
|
}
|
|
2248
2259
|
catch {
|
|
2249
|
-
// If parsing fails, wrap the output as a string result
|
|
2250
2260
|
responseData = { result: result.output };
|
|
2251
2261
|
}
|
|
2252
2262
|
return {
|
|
@@ -2667,11 +2677,11 @@ const NOT_RETRYABLE_ERROR_TYPES = [
|
|
|
2667
2677
|
// Patterns (not exact names) so dated variants and future releases are covered
|
|
2668
2678
|
// without code changes — OpenAI is removing temperature on newer reasoning
|
|
2669
2679
|
// models.
|
|
2670
|
-
const MODELS_WITHOUT_TEMPERATURE = [
|
|
2680
|
+
const MODELS_WITHOUT_TEMPERATURE$1 = [
|
|
2671
2681
|
/^gpt-5\.5/, // gpt-5.5 series deprecated temperature
|
|
2672
2682
|
/^o\d/, // o-series reasoning models (o1, o3, o4, ...)
|
|
2673
2683
|
];
|
|
2674
|
-
function isTemperatureDeprecationError(error) {
|
|
2684
|
+
function isTemperatureDeprecationError$1(error) {
|
|
2675
2685
|
if (!error || typeof error !== "object")
|
|
2676
2686
|
return false;
|
|
2677
2687
|
if (!(error instanceof openai.BadRequestError) &&
|
|
@@ -2711,7 +2721,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
2711
2721
|
supportsTemperature(model) {
|
|
2712
2722
|
if (this.runtimeNoTemperatureModels.has(model))
|
|
2713
2723
|
return false;
|
|
2714
|
-
return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
|
|
2724
|
+
return !MODELS_WITHOUT_TEMPERATURE$1.some((pattern) => pattern.test(model));
|
|
2715
2725
|
}
|
|
2716
2726
|
//
|
|
2717
2727
|
// Request Building
|
|
@@ -2818,7 +2828,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
2818
2828
|
return undefined;
|
|
2819
2829
|
// If the model rejected `temperature`, cache it and retry without the param
|
|
2820
2830
|
if (openaiRequest.temperature !== undefined &&
|
|
2821
|
-
isTemperatureDeprecationError(error)) {
|
|
2831
|
+
isTemperatureDeprecationError$1(error)) {
|
|
2822
2832
|
this.rememberModelRejectsTemperature(openaiRequest.model);
|
|
2823
2833
|
const retryRequest = { ...openaiRequest };
|
|
2824
2834
|
delete retryRequest.temperature;
|
|
@@ -3168,10 +3178,31 @@ function isStructuredOutputUnsupportedError(error) {
|
|
|
3168
3178
|
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
3169
3179
|
return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
|
|
3170
3180
|
}
|
|
3181
|
+
// OpenRouter routes that don't accept `temperature`. Patterns match the
|
|
3182
|
+
// vendor-prefixed route id (e.g. `openai/gpt-5.5`) so dated variants are
|
|
3183
|
+
// covered without code changes.
|
|
3184
|
+
const MODELS_WITHOUT_TEMPERATURE = [
|
|
3185
|
+
/^openai\/gpt-5\.5/,
|
|
3186
|
+
/^openai\/o\d/,
|
|
3187
|
+
/^anthropic\/claude-opus-4-[789]/,
|
|
3188
|
+
/^anthropic\/claude-opus-[5-9]/,
|
|
3189
|
+
];
|
|
3190
|
+
function isTemperatureDeprecationError(error) {
|
|
3191
|
+
if (!error || typeof error !== "object")
|
|
3192
|
+
return false;
|
|
3193
|
+
const err = error;
|
|
3194
|
+
const status = err.status ?? err.statusCode;
|
|
3195
|
+
if (status !== 400)
|
|
3196
|
+
return false;
|
|
3197
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
3198
|
+
return messages.some((m) => m.toLowerCase().includes("temperature"));
|
|
3199
|
+
}
|
|
3171
3200
|
/**
|
|
3172
|
-
* Convert standardized content items to OpenRouter format
|
|
3173
|
-
*
|
|
3174
|
-
*
|
|
3201
|
+
* Convert standardized content items to OpenRouter format. Images become
|
|
3202
|
+
* `image_url` parts and files become `file` parts; both pass through to
|
|
3203
|
+
* OpenRouter which routes to the selected backend. Backends that don't
|
|
3204
|
+
* support the modality 4xx — that's a model-capability mismatch, surfaced
|
|
3205
|
+
* by the call rather than silently dropped here.
|
|
3175
3206
|
*/
|
|
3176
3207
|
function convertContentToOpenRouter(content) {
|
|
3177
3208
|
if (typeof content === "string") {
|
|
@@ -3179,25 +3210,38 @@ function convertContentToOpenRouter(content) {
|
|
|
3179
3210
|
}
|
|
3180
3211
|
const parts = [];
|
|
3181
3212
|
for (const item of content) {
|
|
3182
|
-
// Text content - pass through
|
|
3183
3213
|
if (item.type === exports.LlmMessageType.InputText) {
|
|
3184
3214
|
parts.push({ type: "text", text: item.text });
|
|
3185
3215
|
continue;
|
|
3186
3216
|
}
|
|
3187
|
-
// Image content - warn and discard
|
|
3188
3217
|
if (item.type === exports.LlmMessageType.InputImage) {
|
|
3189
|
-
|
|
3218
|
+
const url = item.image_url ?? "";
|
|
3219
|
+
if (!url) {
|
|
3220
|
+
log$1.log.warn("OpenRouter image content missing image_url; image discarded");
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
parts.push({ type: "image_url", imageUrl: { url } });
|
|
3190
3224
|
continue;
|
|
3191
3225
|
}
|
|
3192
|
-
// File/Document content - warn and discard
|
|
3193
3226
|
if (item.type === exports.LlmMessageType.InputFile) {
|
|
3194
|
-
|
|
3227
|
+
const fileData = typeof item.file_data === "string" ? item.file_data : "";
|
|
3228
|
+
if (!fileData) {
|
|
3229
|
+
log$1.log.warn({ filename: item.filename }, "OpenRouter file content missing file_data; file discarded");
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
parts.push({
|
|
3233
|
+
type: "file",
|
|
3234
|
+
file: {
|
|
3235
|
+
filename: item.filename,
|
|
3236
|
+
fileData,
|
|
3237
|
+
},
|
|
3238
|
+
});
|
|
3195
3239
|
continue;
|
|
3196
3240
|
}
|
|
3197
3241
|
// Unknown type - warn and skip
|
|
3198
3242
|
log$1.log.warn({ item }, "Unknown content type for OpenRouter; discarded");
|
|
3199
3243
|
}
|
|
3200
|
-
// If no
|
|
3244
|
+
// If no parts remain, return empty string to avoid empty array
|
|
3201
3245
|
if (parts.length === 0) {
|
|
3202
3246
|
return "";
|
|
3203
3247
|
}
|
|
@@ -3251,6 +3295,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3251
3295
|
// `response_format: json_schema`. When a model is in this set, buildRequest
|
|
3252
3296
|
// engages the legacy fake-tool path instead of native structured output.
|
|
3253
3297
|
this.runtimeNoStructuredOutputModels = new Set();
|
|
3298
|
+
// Session-level cache of routes observed to reject `temperature`. Populated
|
|
3299
|
+
// by executeRequest on 400 errors so repeat calls skip the param.
|
|
3300
|
+
this.runtimeNoTemperatureModels = new Set();
|
|
3254
3301
|
}
|
|
3255
3302
|
rememberModelRejectsStructuredOutput(model) {
|
|
3256
3303
|
this.runtimeNoStructuredOutputModels.add(model);
|
|
@@ -3261,6 +3308,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3261
3308
|
supportsStructuredOutput(model) {
|
|
3262
3309
|
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
3263
3310
|
}
|
|
3311
|
+
rememberModelRejectsTemperature(model) {
|
|
3312
|
+
this.runtimeNoTemperatureModels.add(model);
|
|
3313
|
+
}
|
|
3314
|
+
clearRuntimeNoTemperatureModels() {
|
|
3315
|
+
this.runtimeNoTemperatureModels.clear();
|
|
3316
|
+
}
|
|
3317
|
+
supportsTemperature(model) {
|
|
3318
|
+
if (this.runtimeNoTemperatureModels.has(model))
|
|
3319
|
+
return false;
|
|
3320
|
+
return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
|
|
3321
|
+
}
|
|
3264
3322
|
//
|
|
3265
3323
|
// Request Building
|
|
3266
3324
|
//
|
|
@@ -3330,6 +3388,12 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3330
3388
|
openRouterRequest.temperature =
|
|
3331
3389
|
request.temperature;
|
|
3332
3390
|
}
|
|
3391
|
+
// Strip temperature for routes that don't support it (denylist + runtime cache)
|
|
3392
|
+
const requestRecord = openRouterRequest;
|
|
3393
|
+
if (requestRecord.temperature !== undefined &&
|
|
3394
|
+
!this.supportsTemperature(openRouterRequest.model)) {
|
|
3395
|
+
delete requestRecord.temperature;
|
|
3396
|
+
}
|
|
3333
3397
|
return openRouterRequest;
|
|
3334
3398
|
}
|
|
3335
3399
|
formatTools(toolkit,
|
|
@@ -3356,11 +3420,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3356
3420
|
jsonSchema.type = "object"; // Normalize type
|
|
3357
3421
|
}
|
|
3358
3422
|
else {
|
|
3359
|
-
// Convert NaturalSchema to JSON schema through Zod
|
|
3423
|
+
// Convert NaturalSchema to JSON schema through Zod. Re-spread into a
|
|
3424
|
+
// plain object: Zod v4's z.toJSONSchema returns an object that carries
|
|
3425
|
+
// a non-configurable `~standard` Standard-Schema interop marker as a
|
|
3426
|
+
// non-enumerable own property. Anthropic's output_config.format
|
|
3427
|
+
// validation is strict and rejects unknown properties when OpenRouter
|
|
3428
|
+
// forwards the schema, so we drop the marker here. JSON.stringify
|
|
3429
|
+
// already skips it but the OpenRouter SDK enumerates own properties
|
|
3430
|
+
// during serialization.
|
|
3360
3431
|
const zodSchema = schema instanceof v4.z.ZodType
|
|
3361
3432
|
? schema
|
|
3362
3433
|
: naturalZodSchema(schema);
|
|
3363
|
-
jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
3434
|
+
jsonSchema = { ...v4.z.toJSONSchema(zodSchema) };
|
|
3364
3435
|
}
|
|
3365
3436
|
// Remove $schema property (can cause issues with some providers)
|
|
3366
3437
|
if (jsonSchema.$schema) {
|
|
@@ -3388,6 +3459,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3388
3459
|
catch (error) {
|
|
3389
3460
|
if (signal?.aborted)
|
|
3390
3461
|
return undefined;
|
|
3462
|
+
// If the route rejected `temperature` (e.g., openai/gpt-5.5 forwarding),
|
|
3463
|
+
// cache it and retry without the param.
|
|
3464
|
+
const requestRecord = openRouterRequest;
|
|
3465
|
+
if (requestRecord.temperature !== undefined &&
|
|
3466
|
+
isTemperatureDeprecationError(error)) {
|
|
3467
|
+
this.rememberModelRejectsTemperature(openRouterRequest.model);
|
|
3468
|
+
const retryRequest = { ...openRouterRequest };
|
|
3469
|
+
delete retryRequest.temperature;
|
|
3470
|
+
const response = (await openRouter.chat.send(this.toSdkChatParams(retryRequest), signal ? { signal } : undefined));
|
|
3471
|
+
if (wantsStructuredOutput) {
|
|
3472
|
+
response.__jaypieStructuredOutput = true;
|
|
3473
|
+
}
|
|
3474
|
+
return response;
|
|
3475
|
+
}
|
|
3391
3476
|
// If the model rejected `response_format`, cache it and retry with the
|
|
3392
3477
|
// legacy fake-tool emulation path.
|
|
3393
3478
|
if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
|
|
@@ -4732,6 +4817,78 @@ class RetryPolicy {
|
|
|
4732
4817
|
// Export a default policy instance
|
|
4733
4818
|
const defaultRetryPolicy = new RetryPolicy();
|
|
4734
4819
|
|
|
4820
|
+
//
|
|
4821
|
+
//
|
|
4822
|
+
// Helpers
|
|
4823
|
+
//
|
|
4824
|
+
/**
|
|
4825
|
+
* Compare an unhandled rejection reason against errors the retry loop has
|
|
4826
|
+
* already caught. Reference equality first, then fall back to message + name
|
|
4827
|
+
* — providers sometimes surface twin rejections as fresh Error instances
|
|
4828
|
+
* rebuilt from the same upstream failure.
|
|
4829
|
+
*/
|
|
4830
|
+
function matchesCaughtError(reason, caught) {
|
|
4831
|
+
if (caught.has(reason))
|
|
4832
|
+
return true;
|
|
4833
|
+
if (!(reason instanceof Error))
|
|
4834
|
+
return false;
|
|
4835
|
+
for (const handled of caught) {
|
|
4836
|
+
if (handled instanceof Error &&
|
|
4837
|
+
handled.name === reason.name &&
|
|
4838
|
+
handled.message === reason.message) {
|
|
4839
|
+
return true;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
return false;
|
|
4843
|
+
}
|
|
4844
|
+
//
|
|
4845
|
+
//
|
|
4846
|
+
// Main
|
|
4847
|
+
//
|
|
4848
|
+
/**
|
|
4849
|
+
* Create a guard that suppresses unhandled rejections firing as siblings of
|
|
4850
|
+
* an error the retry loop has already caught. Provider SDKs occasionally
|
|
4851
|
+
* surface a single upstream failure as twin rejections — the retry layer
|
|
4852
|
+
* accepts responsibility for the first; this guard prevents the second from
|
|
4853
|
+
* crashing the host while the retry is in flight.
|
|
4854
|
+
*
|
|
4855
|
+
* The guard also continues to suppress transient socket teardown errors
|
|
4856
|
+
* (e.g. undici `TypeError: terminated`) emitted between attempts.
|
|
4857
|
+
*/
|
|
4858
|
+
function createStaleRejectionGuard() {
|
|
4859
|
+
const log = getLogger$5();
|
|
4860
|
+
const caughtErrors = new Set();
|
|
4861
|
+
let listener;
|
|
4862
|
+
return {
|
|
4863
|
+
install() {
|
|
4864
|
+
if (listener)
|
|
4865
|
+
return;
|
|
4866
|
+
listener = (reason, promise) => {
|
|
4867
|
+
if (isTransientNetworkError(reason)) {
|
|
4868
|
+
promise?.catch?.(() => { });
|
|
4869
|
+
log.trace("Suppressed stale socket error during retry");
|
|
4870
|
+
return;
|
|
4871
|
+
}
|
|
4872
|
+
if (matchesCaughtError(reason, caughtErrors)) {
|
|
4873
|
+
promise?.catch?.(() => { });
|
|
4874
|
+
log.trace("Suppressed sibling rejection of already-handled error");
|
|
4875
|
+
}
|
|
4876
|
+
};
|
|
4877
|
+
process.on("unhandledRejection", listener);
|
|
4878
|
+
},
|
|
4879
|
+
remove() {
|
|
4880
|
+
if (listener) {
|
|
4881
|
+
process.removeListener("unhandledRejection", listener);
|
|
4882
|
+
listener = undefined;
|
|
4883
|
+
}
|
|
4884
|
+
caughtErrors.clear();
|
|
4885
|
+
},
|
|
4886
|
+
recordCaught(error) {
|
|
4887
|
+
caughtErrors.add(error);
|
|
4888
|
+
},
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4735
4892
|
//
|
|
4736
4893
|
//
|
|
4737
4894
|
// Main
|
|
@@ -4760,27 +4917,11 @@ class RetryExecutor {
|
|
|
4760
4917
|
async execute(operation, options) {
|
|
4761
4918
|
const log = getLogger$5();
|
|
4762
4919
|
let attempt = 0;
|
|
4763
|
-
//
|
|
4764
|
-
//
|
|
4765
|
-
//
|
|
4766
|
-
//
|
|
4767
|
-
|
|
4768
|
-
const installGuard = () => {
|
|
4769
|
-
if (staleGuard)
|
|
4770
|
-
return;
|
|
4771
|
-
staleGuard = (reason) => {
|
|
4772
|
-
if (isTransientNetworkError(reason)) {
|
|
4773
|
-
log.trace("Suppressed stale socket error during retry");
|
|
4774
|
-
}
|
|
4775
|
-
};
|
|
4776
|
-
process.on("unhandledRejection", staleGuard);
|
|
4777
|
-
};
|
|
4778
|
-
const removeGuard = () => {
|
|
4779
|
-
if (staleGuard) {
|
|
4780
|
-
process.removeListener("unhandledRejection", staleGuard);
|
|
4781
|
-
staleGuard = undefined;
|
|
4782
|
-
}
|
|
4783
|
-
};
|
|
4920
|
+
// Guard against stale rejections firing on a subsequent microtask after
|
|
4921
|
+
// the retry layer has already caught the originating error: undici socket
|
|
4922
|
+
// teardown (TypeError: terminated) and twin upstream-SDK rejections
|
|
4923
|
+
// (e.g. issue #336 — OpenRouter SyntaxError siblings).
|
|
4924
|
+
const guard = createStaleRejectionGuard();
|
|
4784
4925
|
try {
|
|
4785
4926
|
while (true) {
|
|
4786
4927
|
const controller = new AbortController();
|
|
@@ -4792,12 +4933,9 @@ class RetryExecutor {
|
|
|
4792
4933
|
return result;
|
|
4793
4934
|
}
|
|
4794
4935
|
catch (error) {
|
|
4795
|
-
// Abort the previous request to kill lingering socket callbacks
|
|
4796
4936
|
controller.abort("retry");
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
// sleep, or the next operation attempt)
|
|
4800
|
-
installGuard();
|
|
4937
|
+
guard.recordCaught(error);
|
|
4938
|
+
guard.install();
|
|
4801
4939
|
// Check if we've exhausted retries
|
|
4802
4940
|
if (!this.policy.shouldRetry(attempt)) {
|
|
4803
4941
|
log.error(`API call failed after ${this.policy.maxRetries} retries`);
|
|
@@ -4843,7 +4981,7 @@ class RetryExecutor {
|
|
|
4843
4981
|
}
|
|
4844
4982
|
}
|
|
4845
4983
|
finally {
|
|
4846
|
-
|
|
4984
|
+
guard.remove();
|
|
4847
4985
|
}
|
|
4848
4986
|
}
|
|
4849
4987
|
}
|
|
@@ -5453,25 +5591,10 @@ class StreamLoop {
|
|
|
5453
5591
|
// Retry loop for connection-level failures
|
|
5454
5592
|
let attempt = 0;
|
|
5455
5593
|
let chunksYielded = false;
|
|
5456
|
-
//
|
|
5457
|
-
//
|
|
5458
|
-
|
|
5459
|
-
const
|
|
5460
|
-
if (staleGuard)
|
|
5461
|
-
return;
|
|
5462
|
-
staleGuard = (reason) => {
|
|
5463
|
-
if (isTransientNetworkError(reason)) {
|
|
5464
|
-
log.trace("Suppressed stale socket error during retry");
|
|
5465
|
-
}
|
|
5466
|
-
};
|
|
5467
|
-
process.on("unhandledRejection", staleGuard);
|
|
5468
|
-
};
|
|
5469
|
-
const removeGuard = () => {
|
|
5470
|
-
if (staleGuard) {
|
|
5471
|
-
process.removeListener("unhandledRejection", staleGuard);
|
|
5472
|
-
staleGuard = undefined;
|
|
5473
|
-
}
|
|
5474
|
-
};
|
|
5594
|
+
// Guard against stale rejections firing after the stream loop has already
|
|
5595
|
+
// caught the originating error: undici socket teardown and twin
|
|
5596
|
+
// upstream-SDK rejections (issue #336).
|
|
5597
|
+
const guard = createStaleRejectionGuard();
|
|
5475
5598
|
try {
|
|
5476
5599
|
while (true) {
|
|
5477
5600
|
const controller = new AbortController();
|
|
@@ -5512,10 +5635,9 @@ class StreamLoop {
|
|
|
5512
5635
|
break;
|
|
5513
5636
|
}
|
|
5514
5637
|
catch (error) {
|
|
5515
|
-
// Abort the previous request to kill lingering socket callbacks
|
|
5516
5638
|
controller.abort("retry");
|
|
5517
|
-
|
|
5518
|
-
|
|
5639
|
+
guard.recordCaught(error);
|
|
5640
|
+
guard.install();
|
|
5519
5641
|
// If chunks were already yielded, we can't transparently retry
|
|
5520
5642
|
if (chunksYielded) {
|
|
5521
5643
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -5548,7 +5670,7 @@ class StreamLoop {
|
|
|
5548
5670
|
}
|
|
5549
5671
|
}
|
|
5550
5672
|
finally {
|
|
5551
|
-
|
|
5673
|
+
guard.remove();
|
|
5552
5674
|
}
|
|
5553
5675
|
// Execute afterEachModelResponse hook
|
|
5554
5676
|
await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
|