@librechat/agents 3.1.67 → 3.1.68

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.
@@ -364,6 +364,122 @@ type LogFn = (
364
364
  data?: Record<string, unknown>
365
365
  ) => void;
366
366
 
367
+ /**
368
+ * Extracts an HTTP status code from a thrown LLM-provider error. Returns
369
+ * `undefined` for non-object values (including `null` or `undefined`, both
370
+ * valid `throw` targets in JS) so callers never dereference a nullish
371
+ * value.
372
+ */
373
+ function extractHttpStatus(err: unknown): number | undefined {
374
+ if (err == null || typeof err !== 'object') {
375
+ return undefined;
376
+ }
377
+ const errRecord = err as Record<string, unknown>;
378
+ const direct = errRecord.status;
379
+ if (typeof direct === 'number') {
380
+ return direct;
381
+ }
382
+ const statusCode = errRecord.statusCode;
383
+ if (typeof statusCode === 'number') {
384
+ return statusCode;
385
+ }
386
+ const response = errRecord.response;
387
+ if (response != null && typeof response === 'object') {
388
+ const nested = (response as Record<string, unknown>).status;
389
+ if (typeof nested === 'number') {
390
+ return nested;
391
+ }
392
+ }
393
+ return undefined;
394
+ }
395
+
396
+ /**
397
+ * Formats a provider-level error for logging. Returns both a human-readable
398
+ * suffix (safe to include in the message string so it survives any host-side
399
+ * formatter) and a structured metadata bag for rich log backends.
400
+ */
401
+ function describeProviderError(
402
+ err: unknown,
403
+ provider: string,
404
+ modelName?: string
405
+ ): { suffix: string; data: Record<string, unknown> } {
406
+ const providerLabel = `${provider}/${modelName ?? '(no-model)'}`;
407
+ const errMsg = err instanceof Error ? err.message : String(err);
408
+
409
+ const data: Record<string, unknown> = {
410
+ provider,
411
+ model: modelName,
412
+ };
413
+ if (err instanceof Error) {
414
+ data.errorName = err.name;
415
+ data.errorStack = err.stack;
416
+ }
417
+
418
+ const status = extractHttpStatus(err);
419
+ const statusSuffix = status != null ? ` (HTTP ${status})` : '';
420
+ if (status != null) {
421
+ data.status = status;
422
+ }
423
+
424
+ return {
425
+ suffix: `[${providerLabel}]${statusSuffix}: ${errMsg}`,
426
+ data,
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Formats an exhausted-fallback error. `tryFallbackProviders` throws the
432
+ * last fallback provider's error, which may be from any of the configured
433
+ * fallbacks — not the primary — so we label the log with the list of
434
+ * fallback providers attempted rather than mis-attributing to the primary.
435
+ *
436
+ * Entries in `fallbacks` are normally strongly typed, but we defend against
437
+ * malformed runtime config (null/undefined entries, missing `provider`
438
+ * field) so a recoverable summarization failure is never promoted to an
439
+ * uncaught exception from inside the logging path.
440
+ */
441
+ function describeFallbackError(
442
+ err: unknown,
443
+ fallbacks: unknown
444
+ ): { suffix: string; data: Record<string, unknown> } {
445
+ const errMsg = err instanceof Error ? err.message : String(err);
446
+ const list: ReadonlyArray<unknown> = Array.isArray(fallbacks)
447
+ ? fallbacks
448
+ : [];
449
+ const providerNames = list
450
+ .map((f) => {
451
+ if (f == null || typeof f !== 'object') {
452
+ return undefined;
453
+ }
454
+ const raw = (f as { provider?: unknown }).provider;
455
+ return raw != null ? String(raw) : undefined;
456
+ })
457
+ .filter((p): p is string => typeof p === 'string');
458
+ const label =
459
+ providerNames.length > 0
460
+ ? `fallbacks=[${providerNames.join(',')}]`
461
+ : 'no-fallbacks';
462
+
463
+ const data: Record<string, unknown> = {
464
+ fallbackProviders: providerNames,
465
+ fallbackCount: list.length,
466
+ };
467
+ if (err instanceof Error) {
468
+ data.errorName = err.name;
469
+ data.errorStack = err.stack;
470
+ }
471
+ const status = extractHttpStatus(err);
472
+ const statusSuffix = status != null ? ` (HTTP ${status})` : '';
473
+ if (status != null) {
474
+ data.status = status;
475
+ }
476
+
477
+ return {
478
+ suffix: `[${label}]${statusSuffix}: ${errMsg}`,
479
+ data,
480
+ };
481
+ }
482
+
367
483
  /**
368
484
  * Runs the summarization LLM call with primary + fallback providers,
369
485
  * falling back to a metadata stub when all calls fail.
@@ -387,18 +503,23 @@ async function executeSummarizationWithFallback(params: {
387
503
  log,
388
504
  } = params;
389
505
 
390
- const summarizationModel = initializeModel({
391
- provider: clientConfig.provider as Providers,
392
- clientOptions: clientConfig.clientOptions as t.ClientOptions,
393
- tools: agentContext.getToolsForBinding(),
394
- }) as t.ChatModel;
395
-
396
506
  const priorSummaryText = agentContext.getSummaryText()?.trim() ?? '';
397
507
 
398
508
  let summaryText = '';
399
509
  let summaryUsage: Partial<UsageMetadata> | undefined;
400
510
 
401
511
  try {
512
+ /**
513
+ * Initialize inside the try so that a misconfigured provider
514
+ * (e.g. an unrecognized summarization.provider) surfaces through the
515
+ * `log('error', ...)` path below rather than bubbling up silently.
516
+ */
517
+ const summarizationModel = initializeModel({
518
+ provider: clientConfig.provider as Providers,
519
+ clientOptions: clientConfig.clientOptions as t.ClientOptions,
520
+ tools: agentContext.getToolsForBinding(),
521
+ }) as t.ChatModel;
522
+
402
523
  const result = await summarizeWithCacheHit({
403
524
  model: summarizationModel,
404
525
  messages,
@@ -415,19 +536,20 @@ async function executeSummarizationWithFallback(params: {
415
536
  summaryText = result.text;
416
537
  summaryUsage = result.usage;
417
538
  } catch (primaryError) {
418
- log('error', 'Summarization LLM call failed', {
419
- error:
420
- primaryError instanceof Error
421
- ? primaryError.message
422
- : String(primaryError),
423
- provider: clientConfig.provider,
424
- model: clientConfig.modelName,
539
+ const primaryDescribed = describeProviderError(
540
+ primaryError,
541
+ clientConfig.provider,
542
+ clientConfig.modelName
543
+ );
544
+ log('error', `Summarization LLM call failed ${primaryDescribed.suffix}`, {
545
+ ...primaryDescribed.data,
425
546
  messagesToRefineCount: messages.length,
426
547
  });
427
548
 
428
- const fallbacks =
429
- (clientConfig.clientOptions as unknown as t.LLMConfig | undefined)
430
- ?.fallbacks ?? [];
549
+ const rawFallbacks = (
550
+ clientConfig.clientOptions as unknown as t.LLMConfig | undefined
551
+ )?.fallbacks;
552
+ const fallbacks = Array.isArray(rawFallbacks) ? rawFallbacks : [];
431
553
  if (fallbacks.length > 0) {
432
554
  try {
433
555
  const onChunk = createSummarizationChunkHandler({
@@ -460,18 +582,21 @@ async function executeSummarizationWithFallback(params: {
460
582
  );
461
583
  }
462
584
  } catch (fbErr) {
463
- log('warn', 'Fallback providers also failed', {
464
- error: fbErr instanceof Error ? fbErr.message : String(fbErr),
585
+ const fbDescribed = describeFallbackError(fbErr, fallbacks);
586
+ log('warn', `Fallback providers also failed ${fbDescribed.suffix}`, {
587
+ ...fbDescribed.data,
465
588
  });
466
589
  }
467
590
  }
468
591
  if (!summaryText) {
469
- log('warn', 'Summarization failed, falling back to metadata stub', {
470
- error:
471
- primaryError instanceof Error
472
- ? primaryError.message
473
- : String(primaryError),
474
- });
592
+ log(
593
+ 'warn',
594
+ `Summarization failed, falling back to metadata stub ${primaryDescribed.suffix}`,
595
+ {
596
+ ...primaryDescribed.data,
597
+ messagesToRefineCount: messages.length,
598
+ }
599
+ );
475
600
  summaryText = generateMetadataStub(messages);
476
601
  }
477
602
  }