@librechat/agents 2.4.59 → 2.4.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "2.4.59",
3
+ "version": "2.4.61",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -557,7 +557,8 @@ export function convertResponseContentToChatGenerationChunk(
557
557
  ...functionCalls.map((fc) => ({
558
558
  ...fc,
559
559
  args: JSON.stringify(fc.args),
560
- index: extra.index,
560
+ // Un-commenting this causes LangChain to incorrectly merge tool calls together
561
+ // index: extra.index,
561
562
  type: 'tool_call_chunk' as const,
562
563
  id: 'id' in fc && typeof fc.id === 'string' ? fc.id : uuidv4(),
563
564
  }))
@@ -13,8 +13,15 @@ import {
13
13
  ChatOpenAI as OriginalChatOpenAI,
14
14
  AzureChatOpenAI as OriginalAzureChatOpenAI,
15
15
  } from '@langchain/openai';
16
+ import type {
17
+ OpenAIChatCallOptions,
18
+ OpenAIRoleEnum,
19
+ HeaderValue,
20
+ HeadersLike,
21
+ } from './types';
16
22
  import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
17
- import type { BaseMessage } from '@langchain/core/messages';
23
+ import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
24
+ import type { ChatXAIInput } from '@langchain/xai';
18
25
  import type * as t from '@langchain/openai';
19
26
  import {
20
27
  _convertMessagesToOpenAIParams,
@@ -23,25 +30,6 @@ import {
23
30
  type ResponseReturnStreamEvents,
24
31
  } from './utils';
25
32
 
26
- // TODO import from SDK when available
27
- type OpenAIRoleEnum =
28
- | 'system'
29
- | 'developer'
30
- | 'assistant'
31
- | 'user'
32
- | 'function'
33
- | 'tool';
34
-
35
- type HeaderValue = string | undefined | null;
36
- export type HeadersLike =
37
- | Headers
38
- | readonly HeaderValue[][]
39
- | Record<string, HeaderValue | readonly HeaderValue[]>
40
- | undefined
41
- | null
42
- // NullableHeaders
43
- | { values: Headers; [key: string]: unknown };
44
-
45
33
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
46
34
  const iife = <T>(fn: () => T) => fn();
47
35
 
@@ -542,10 +530,49 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
542
530
  }
543
531
  }
544
532
 
533
+ /** xAI-specific usage metadata type */
534
+ export interface XAIUsageMetadata
535
+ extends OpenAIClient.Completions.CompletionUsage {
536
+ prompt_tokens_details?: {
537
+ audio_tokens?: number;
538
+ cached_tokens?: number;
539
+ text_tokens?: number;
540
+ image_tokens?: number;
541
+ };
542
+ completion_tokens_details?: {
543
+ audio_tokens?: number;
544
+ reasoning_tokens?: number;
545
+ accepted_prediction_tokens?: number;
546
+ rejected_prediction_tokens?: number;
547
+ };
548
+ num_sources_used?: number;
549
+ }
550
+
545
551
  export class ChatXAI extends OriginalChatXAI {
552
+ constructor(
553
+ fields?: Partial<ChatXAIInput> & {
554
+ configuration?: { baseURL?: string };
555
+ clientConfig?: { baseURL?: string };
556
+ }
557
+ ) {
558
+ super(fields);
559
+ const customBaseURL =
560
+ fields?.configuration?.baseURL ?? fields?.clientConfig?.baseURL;
561
+ if (customBaseURL != null && customBaseURL) {
562
+ this.clientConfig = {
563
+ ...this.clientConfig,
564
+ baseURL: customBaseURL,
565
+ };
566
+ // Reset the client to force recreation with new config
567
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
568
+ this.client = undefined as any;
569
+ }
570
+ }
571
+
546
572
  public get exposedClient(): CustomOpenAIClient {
547
573
  return this.client;
548
574
  }
575
+
549
576
  protected _getClientOptions(
550
577
  options?: OpenAICoreRequestOptions
551
578
  ): OpenAICoreRequestOptions {
@@ -573,4 +600,166 @@ export class ChatXAI extends OriginalChatXAI {
573
600
  } as OpenAICoreRequestOptions;
574
601
  return requestOptions;
575
602
  }
603
+
604
+ async *_streamResponseChunks(
605
+ messages: BaseMessage[],
606
+ options: this['ParsedCallOptions'],
607
+ runManager?: CallbackManagerForLLMRun
608
+ ): AsyncGenerator<ChatGenerationChunk> {
609
+ const messagesMapped: OpenAICompletionParam[] =
610
+ _convertMessagesToOpenAIParams(messages, this.model);
611
+
612
+ const params = {
613
+ ...this.invocationParams(options, {
614
+ streaming: true,
615
+ }),
616
+ messages: messagesMapped,
617
+ stream: true as const,
618
+ };
619
+ let defaultRole: OpenAIRoleEnum | undefined;
620
+
621
+ const streamIterable = await this.completionWithRetry(params, options);
622
+ let usage: OpenAIClient.Completions.CompletionUsage | undefined;
623
+ for await (const data of streamIterable) {
624
+ const choice = data.choices[0] as
625
+ | Partial<OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice>
626
+ | undefined;
627
+ if (data.usage) {
628
+ usage = data.usage;
629
+ }
630
+ if (!choice) {
631
+ continue;
632
+ }
633
+
634
+ const { delta } = choice;
635
+ if (!delta) {
636
+ continue;
637
+ }
638
+ const chunk = this._convertOpenAIDeltaToBaseMessageChunk(
639
+ delta,
640
+ data,
641
+ defaultRole
642
+ );
643
+ if (chunk.usage_metadata != null) {
644
+ chunk.usage_metadata = {
645
+ input_tokens:
646
+ (chunk.usage_metadata as Partial<UsageMetadata>).input_tokens ?? 0,
647
+ output_tokens:
648
+ (chunk.usage_metadata as Partial<UsageMetadata>).output_tokens ?? 0,
649
+ total_tokens:
650
+ (chunk.usage_metadata as Partial<UsageMetadata>).total_tokens ?? 0,
651
+ };
652
+ }
653
+ if ('reasoning_content' in delta) {
654
+ chunk.additional_kwargs.reasoning_content = delta.reasoning_content;
655
+ }
656
+ defaultRole = delta.role ?? defaultRole;
657
+ const newTokenIndices = {
658
+ prompt: (options as OpenAIChatCallOptions).promptIndex ?? 0,
659
+ completion: choice.index ?? 0,
660
+ };
661
+ if (typeof chunk.content !== 'string') {
662
+ // eslint-disable-next-line no-console
663
+ console.log(
664
+ '[WARNING]: Received non-string content from OpenAI. This is currently not supported.'
665
+ );
666
+ continue;
667
+ }
668
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
669
+ const generationInfo: Record<string, any> = { ...newTokenIndices };
670
+ if (choice.finish_reason != null) {
671
+ generationInfo.finish_reason = choice.finish_reason;
672
+ // Only include system fingerprint in the last chunk for now
673
+ // to avoid concatenation issues
674
+ generationInfo.system_fingerprint = data.system_fingerprint;
675
+ generationInfo.model_name = data.model;
676
+ generationInfo.service_tier = data.service_tier;
677
+ }
678
+ if (this.logprobs == true) {
679
+ generationInfo.logprobs = choice.logprobs;
680
+ }
681
+ const generationChunk = new ChatGenerationChunk({
682
+ message: chunk,
683
+ text: chunk.content,
684
+ generationInfo,
685
+ });
686
+ yield generationChunk;
687
+ await runManager?.handleLLMNewToken(
688
+ generationChunk.text || '',
689
+ newTokenIndices,
690
+ undefined,
691
+ undefined,
692
+ undefined,
693
+ { chunk: generationChunk }
694
+ );
695
+ }
696
+ if (usage) {
697
+ // Type assertion for xAI-specific usage structure
698
+ const xaiUsage = usage as XAIUsageMetadata;
699
+ const inputTokenDetails = {
700
+ // Standard OpenAI fields
701
+ ...(usage.prompt_tokens_details?.audio_tokens != null && {
702
+ audio: usage.prompt_tokens_details.audio_tokens,
703
+ }),
704
+ ...(usage.prompt_tokens_details?.cached_tokens != null && {
705
+ cache_read: usage.prompt_tokens_details.cached_tokens,
706
+ }),
707
+ // Add xAI-specific prompt token details if they exist
708
+ ...(xaiUsage.prompt_tokens_details?.text_tokens != null && {
709
+ text: xaiUsage.prompt_tokens_details.text_tokens,
710
+ }),
711
+ ...(xaiUsage.prompt_tokens_details?.image_tokens != null && {
712
+ image: xaiUsage.prompt_tokens_details.image_tokens,
713
+ }),
714
+ };
715
+ const outputTokenDetails = {
716
+ // Standard OpenAI fields
717
+ ...(usage.completion_tokens_details?.audio_tokens != null && {
718
+ audio: usage.completion_tokens_details.audio_tokens,
719
+ }),
720
+ ...(usage.completion_tokens_details?.reasoning_tokens != null && {
721
+ reasoning: usage.completion_tokens_details.reasoning_tokens,
722
+ }),
723
+ // Add xAI-specific completion token details if they exist
724
+ ...(xaiUsage.completion_tokens_details?.accepted_prediction_tokens !=
725
+ null && {
726
+ accepted_prediction:
727
+ xaiUsage.completion_tokens_details.accepted_prediction_tokens,
728
+ }),
729
+ ...(xaiUsage.completion_tokens_details?.rejected_prediction_tokens !=
730
+ null && {
731
+ rejected_prediction:
732
+ xaiUsage.completion_tokens_details.rejected_prediction_tokens,
733
+ }),
734
+ };
735
+ const generationChunk = new ChatGenerationChunk({
736
+ message: new AIMessageChunk({
737
+ content: '',
738
+ response_metadata: {
739
+ usage: { ...usage },
740
+ // Include xAI-specific metadata if it exists
741
+ ...(xaiUsage.num_sources_used != null && {
742
+ num_sources_used: xaiUsage.num_sources_used,
743
+ }),
744
+ },
745
+ usage_metadata: {
746
+ input_tokens: usage.prompt_tokens,
747
+ output_tokens: usage.completion_tokens,
748
+ total_tokens: usage.total_tokens,
749
+ ...(Object.keys(inputTokenDetails).length > 0 && {
750
+ input_token_details: inputTokenDetails,
751
+ }),
752
+ ...(Object.keys(outputTokenDetails).length > 0 && {
753
+ output_token_details: outputTokenDetails,
754
+ }),
755
+ },
756
+ }),
757
+ text: '',
758
+ });
759
+ yield generationChunk;
760
+ }
761
+ if (options.signal?.aborted === true) {
762
+ throw new Error('AbortError');
763
+ }
764
+ }
576
765
  }
@@ -0,0 +1,24 @@
1
+ import type { OpenAICallOptions } from '@langchain/openai';
2
+
3
+ export interface OpenAIChatCallOptions extends OpenAICallOptions {
4
+ promptIndex?: number;
5
+ }
6
+
7
+ // TODO import from SDK when available
8
+ export type OpenAIRoleEnum =
9
+ | 'system'
10
+ | 'developer'
11
+ | 'assistant'
12
+ | 'user'
13
+ | 'function'
14
+ | 'tool';
15
+
16
+ export type HeaderValue = string | undefined | null;
17
+ export type HeadersLike =
18
+ | Headers
19
+ | readonly HeaderValue[][]
20
+ | Record<string, HeaderValue | readonly HeaderValue[]>
21
+ | undefined
22
+ | null
23
+ // NullableHeaders
24
+ | { values: Headers; [key: string]: unknown };
@@ -1,7 +1,11 @@
1
1
  // src/scripts/cli.ts
2
2
  import { config } from 'dotenv';
3
3
  config();
4
- import { HumanMessage, BaseMessage } from '@langchain/core/messages';
4
+ import {
5
+ HumanMessage,
6
+ BaseMessage,
7
+ UsageMetadata,
8
+ } from '@langchain/core/messages';
5
9
  import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
6
10
  import type * as t from '@/types';
7
11
  import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
@@ -17,6 +21,7 @@ import { Run } from '@/run';
17
21
 
18
22
  const conversationHistory: BaseMessage[] = [];
19
23
  let _contentParts: t.MessageContentComplex[] = [];
24
+ let collectedUsage: UsageMetadata[] = [];
20
25
 
21
26
  async function testStandardStreaming(): Promise<void> {
22
27
  const { userName, location, provider, currentDate } = await getArgs();
@@ -24,7 +29,7 @@ async function testStandardStreaming(): Promise<void> {
24
29
  _contentParts = contentParts as t.MessageContentComplex[];
25
30
  const customHandlers = {
26
31
  [GraphEvents.TOOL_END]: new ToolEndHandler(),
27
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
32
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
28
33
  [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
29
34
  [GraphEvents.ON_RUN_STEP_COMPLETED]: {
30
35
  handle: (
@@ -177,8 +182,9 @@ async function testStandardStreaming(): Promise<void> {
177
182
  };
178
183
  }
179
184
  const titleResult = await run.generateTitle(titleOptions);
185
+ console.log('Collected usage metadata:', collectedUsage);
180
186
  console.log('Generated Title:', titleResult);
181
- console.log('Collected metadata:', collected);
187
+ console.log('Collected title usage metadata:', collected);
182
188
  }
183
189
 
184
190
  process.on('unhandledRejection', (reason, promise) => {