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