@librechat/agents 3.6.14 → 3.6.16

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.
Files changed (127) hide show
  1. package/README.md +41 -0
  2. package/dist/cjs/agents/AgentContext.cjs +1 -1
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +9 -7
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/llm/contextPressureMeter.cjs +8 -6
  7. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
  8. package/dist/cjs/llm/init.cjs +1 -0
  9. package/dist/cjs/llm/init.cjs.map +1 -1
  10. package/dist/cjs/llm/invoke.cjs +9 -2
  11. package/dist/cjs/llm/invoke.cjs.map +1 -1
  12. package/dist/cjs/llm/openai/index.cjs +1 -1
  13. package/dist/cjs/llm/prepareProviderRequest.cjs +6 -3
  14. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
  15. package/dist/cjs/llm/providerRegistry.cjs +69 -0
  16. package/dist/cjs/llm/providerRegistry.cjs.map +1 -0
  17. package/dist/cjs/llm/providers.cjs +38 -25
  18. package/dist/cjs/llm/providers.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +6 -4
  20. package/dist/cjs/llm/request.cjs.map +1 -1
  21. package/dist/cjs/llm/truncation.cjs +2 -1
  22. package/dist/cjs/llm/truncation.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +18 -15
  24. package/dist/cjs/messages/format.cjs.map +1 -1
  25. package/dist/cjs/messages/prune.cjs +8 -4
  26. package/dist/cjs/messages/prune.cjs.map +1 -1
  27. package/dist/cjs/provider-registration.cjs +4 -0
  28. package/dist/cjs/run.cjs +2 -2
  29. package/dist/cjs/stream.cjs +1 -1
  30. package/dist/cjs/stream.cjs.map +1 -1
  31. package/dist/cjs/summarization/node.cjs +2 -2
  32. package/dist/cjs/summarization/node.cjs.map +1 -1
  33. package/dist/cjs/utils/llm.cjs +6 -4
  34. package/dist/cjs/utils/llm.cjs.map +1 -1
  35. package/dist/cjs/utils/toolContent.cjs +2 -2
  36. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  37. package/dist/esm/agents/AgentContext.mjs +1 -1
  38. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  39. package/dist/esm/graphs/Graph.mjs +10 -8
  40. package/dist/esm/graphs/Graph.mjs.map +1 -1
  41. package/dist/esm/llm/contextPressureMeter.mjs +8 -6
  42. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
  43. package/dist/esm/llm/init.mjs +1 -0
  44. package/dist/esm/llm/init.mjs.map +1 -1
  45. package/dist/esm/llm/invoke.mjs +9 -2
  46. package/dist/esm/llm/invoke.mjs.map +1 -1
  47. package/dist/esm/llm/openai/index.mjs +1 -1
  48. package/dist/esm/llm/prepareProviderRequest.mjs +7 -4
  49. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  50. package/dist/esm/llm/providerRegistry.mjs +64 -0
  51. package/dist/esm/llm/providerRegistry.mjs.map +1 -0
  52. package/dist/esm/llm/providers.mjs +36 -22
  53. package/dist/esm/llm/providers.mjs.map +1 -1
  54. package/dist/esm/llm/request.mjs +6 -4
  55. package/dist/esm/llm/request.mjs.map +1 -1
  56. package/dist/esm/llm/truncation.mjs +2 -1
  57. package/dist/esm/llm/truncation.mjs.map +1 -1
  58. package/dist/esm/main.mjs +12 -10
  59. package/dist/esm/messages/format.mjs.map +1 -1
  60. package/dist/esm/messages/prune.mjs +8 -4
  61. package/dist/esm/messages/prune.mjs.map +1 -1
  62. package/dist/esm/provider-registration.mjs +3 -0
  63. package/dist/esm/run.mjs +2 -2
  64. package/dist/esm/stream.mjs +1 -1
  65. package/dist/esm/stream.mjs.map +1 -1
  66. package/dist/esm/summarization/node.mjs +2 -2
  67. package/dist/esm/summarization/node.mjs.map +1 -1
  68. package/dist/esm/utils/llm.mjs +6 -4
  69. package/dist/esm/utils/llm.mjs.map +1 -1
  70. package/dist/esm/utils/toolContent.mjs +3 -3
  71. package/dist/esm/utils/toolContent.mjs.map +1 -1
  72. package/dist/types/agents/AgentContext.d.ts +5 -5
  73. package/dist/types/index.d.ts +2 -0
  74. package/dist/types/llm/contextOverflowRecovery.d.ts +2 -2
  75. package/dist/types/llm/init.d.ts +21 -14
  76. package/dist/types/llm/invoke.d.ts +5 -6
  77. package/dist/types/llm/prepareProviderRequest.d.ts +5 -6
  78. package/dist/types/llm/providerRegistry.d.ts +11 -0
  79. package/dist/types/llm/providers.d.ts +4 -5
  80. package/dist/types/llm/request.d.ts +2 -3
  81. package/dist/types/llm/truncation.d.ts +2 -2
  82. package/dist/types/messages/core.d.ts +1 -2
  83. package/dist/types/messages/format.d.ts +3 -3
  84. package/dist/types/messages/prune.d.ts +3 -2
  85. package/dist/types/provider-registration.d.ts +15 -0
  86. package/dist/types/stream.d.ts +1 -2
  87. package/dist/types/summarization/node.d.ts +1 -2
  88. package/dist/types/types/activityLabel.d.ts +3 -4
  89. package/dist/types/types/graph.d.ts +4 -5
  90. package/dist/types/types/llm.d.ts +54 -13
  91. package/dist/types/types/reasoningLabel.d.ts +2 -3
  92. package/dist/types/types/run.d.ts +3 -3
  93. package/dist/types/types/summarize.d.ts +2 -2
  94. package/dist/types/utils/errors.d.ts +3 -3
  95. package/dist/types/utils/llm.d.ts +4 -4
  96. package/dist/types/utils/llmConfig.d.ts +2 -2
  97. package/package.json +9 -1
  98. package/src/agents/AgentContext.ts +27 -26
  99. package/src/graphs/Graph.ts +14 -14
  100. package/src/index.ts +5 -0
  101. package/src/llm/contextOverflowRecovery.ts +2 -2
  102. package/src/llm/contextPressureMeter.ts +23 -13
  103. package/src/llm/init.ts +40 -17
  104. package/src/llm/invoke.ts +36 -21
  105. package/src/llm/prepareProviderRequest.ts +19 -18
  106. package/src/llm/providerRegistry.ts +134 -0
  107. package/src/llm/providers.ts +73 -30
  108. package/src/llm/request.ts +10 -6
  109. package/src/llm/truncation.ts +9 -3
  110. package/src/messages/core.ts +2 -2
  111. package/src/messages/format.ts +14 -16
  112. package/src/messages/prune.ts +22 -17
  113. package/src/provider-registration.ts +29 -0
  114. package/src/run.ts +1 -1
  115. package/src/session/AgentSession.ts +1 -1
  116. package/src/stream.ts +7 -8
  117. package/src/summarization/node.ts +7 -7
  118. package/src/types/activityLabel.ts +3 -4
  119. package/src/types/graph.ts +5 -5
  120. package/src/types/llm.ts +95 -13
  121. package/src/types/reasoningLabel.ts +2 -3
  122. package/src/types/run.ts +3 -3
  123. package/src/types/summarize.ts +2 -2
  124. package/src/utils/errors.ts +3 -3
  125. package/src/utils/llm.ts +22 -15
  126. package/src/utils/llmConfig.ts +4 -4
  127. package/src/utils/toolContent.ts +9 -3
package/src/llm/invoke.ts CHANGED
@@ -13,14 +13,9 @@ import type { ToolCall } from '@langchain/core/messages/tool';
13
13
  import type { BaseMessage } from '@langchain/core/messages';
14
14
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
15
15
  import type { PreparedProviderRequest } from '@/llm/prepareProviderRequest';
16
- import type { StreamLimitState } from '@/llm/streamLimits';
17
16
  import type { ContextOverflowContext } from '@/utils/errors';
17
+ import type { StreamLimitState } from '@/llm/streamLimits';
18
18
  import type * as t from '@/types';
19
- import {
20
- modifyDeltaProperties,
21
- } from '@/messages';
22
- import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
23
- import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
24
19
  import {
25
20
  enforceStreamLimitsForWireChunk,
26
21
  registerActiveStreamLimitGeneration,
@@ -35,11 +30,17 @@ import {
35
30
  assertPreparedProviderRequestFor,
36
31
  prepareProviderRequest,
37
32
  } from '@/llm/prepareProviderRequest';
33
+ import {
34
+ getProviderFamily,
35
+ providerUsesManualToolStream,
36
+ } from '@/llm/providers';
37
+ import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
38
+ import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
38
39
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
39
- import { manualToolStreamProviders } from '@/llm/providers';
40
40
  import { safeDispatchCustomEvent } from '@/utils/events';
41
41
  import { getContextOverflowInfo } from '@/utils/errors';
42
42
  import { appendCallbacks } from '@/utils/callbacks';
43
+ import { modifyDeltaProperties } from '@/messages';
43
44
  import { canSealPreempt } from '@/llm/preempt';
44
45
  import { initializeModel } from '@/llm/init';
45
46
 
@@ -105,6 +106,19 @@ export type OnChunk = (
105
106
  /** Unique per-model-attempt sequence; see the stamp in `attemptInvoke`. */
106
107
  let streamLimitAttemptSeq = 0;
107
108
 
109
+ function getManualToolStreamNormalizationProvider(
110
+ provider: t.ProviderName
111
+ ): t.ProviderName {
112
+ const family = getProviderFamily(provider);
113
+ if (family === 'anthropic') {
114
+ return Providers.ANTHROPIC;
115
+ }
116
+ if (family === 'bedrock') {
117
+ return Providers.BEDROCK;
118
+ }
119
+ return provider;
120
+ }
121
+
108
122
  /**
109
123
  * The registered handler that owns content-part dispatch, if any.
110
124
  *
@@ -136,7 +150,7 @@ function removeOpenRouterFinalReasoningReplayContent({
136
150
  }: {
137
151
  current?: AIMessageChunk;
138
152
  next: AIMessageChunk;
139
- provider: Providers;
153
+ provider: t.ProviderName;
140
154
  }): AIMessageChunk {
141
155
  const content = getOpenRouterFinalReasoningContent({
142
156
  current,
@@ -161,7 +175,7 @@ function getOpenRouterFinalReasoningContent({
161
175
  }: {
162
176
  current?: AIMessageChunk;
163
177
  next: AIMessageChunk;
164
- provider: Providers;
178
+ provider: t.ProviderName;
165
179
  }): string | undefined {
166
180
  if (
167
181
  provider !== Providers.OPENROUTER ||
@@ -197,7 +211,7 @@ function getStreamHandlingChunk({
197
211
  }: {
198
212
  current?: AIMessageChunk;
199
213
  next: AIMessageChunk;
200
- provider: Providers;
214
+ provider: t.ProviderName;
201
215
  }): AIMessageChunk | undefined {
202
216
  const content = getOpenRouterFinalReasoningContent({
203
217
  current,
@@ -473,7 +487,7 @@ function appendStreamChunk({
473
487
  }: {
474
488
  current?: AIMessageChunk;
475
489
  next: AIMessageChunk;
476
- provider: Providers;
490
+ provider: t.ProviderName;
477
491
  }): AIMessageChunk {
478
492
  if (current == null) {
479
493
  return next;
@@ -514,11 +528,11 @@ type AttemptInvokeParams = AttemptInvokeCommonParams &
514
528
  request?: never;
515
529
  model: t.ChatModel;
516
530
  messages: BaseMessage[];
517
- provider: Providers;
531
+ provider: t.ProviderName;
518
532
  }
519
533
  );
520
534
 
521
- function resolveAttemptProvider(params: AttemptInvokeParams): Providers {
535
+ function resolveAttemptProvider(params: AttemptInvokeParams): t.ProviderName {
522
536
  if (params.request != null) {
523
537
  return params.request.provider;
524
538
  }
@@ -586,9 +600,7 @@ export async function attemptInvoke(
586
600
  : undefined;
587
601
  const generationKey =
588
602
  leaseTarget != null
589
- ? resolveGenerationKey(
590
- stampedConfig.metadata as Record<string, unknown>
591
- )
603
+ ? resolveGenerationKey(stampedConfig.metadata as Record<string, unknown>)
592
604
  : undefined;
593
605
  if (leaseTarget != null && generationKey != null) {
594
606
  registerActiveStreamLimitGeneration(leaseTarget, generationKey);
@@ -809,8 +821,11 @@ async function attemptInvokeBody(
809
821
  }
810
822
  }
811
823
 
812
- if (manualToolStreamProviders.has(provider)) {
813
- finalChunk = modifyDeltaProperties(provider, finalChunk);
824
+ if (providerUsesManualToolStream(provider)) {
825
+ finalChunk = modifyDeltaProperties(
826
+ getManualToolStreamNormalizationProvider(provider),
827
+ finalChunk
828
+ );
814
829
  }
815
830
 
816
831
  if (preempted && finalChunk != null) {
@@ -857,7 +872,7 @@ async function attemptInvokeBody(
857
872
  * differ, which is the whole reason a fallback exists.
858
873
  */
859
874
  export interface FallbackErrorContext {
860
- provider: Providers;
875
+ provider: t.ProviderName;
861
876
  clientOptions?: t.ClientOptions;
862
877
  maxContextTokens?: number;
863
878
  }
@@ -968,7 +983,7 @@ export async function tryFallbackProviders({
968
983
  prepareProviderRequest?: (input: {
969
984
  model: t.ChatModel;
970
985
  messages: BaseMessage[];
971
- provider: Providers;
986
+ provider: t.ProviderName;
972
987
  clientOptions?: t.ClientOptions;
973
988
  maxContextTokens?: number;
974
989
  config?: RunnableConfig;
@@ -977,7 +992,7 @@ export async function tryFallbackProviders({
977
992
  prepareProviderMessages?: (input: {
978
993
  model: t.ChatModel;
979
994
  messages: BaseMessage[];
980
- provider: Providers;
995
+ provider: t.ProviderName;
981
996
  clientOptions?: t.ClientOptions;
982
997
  maxContextTokens?: number;
983
998
  config?: RunnableConfig;
@@ -14,7 +14,6 @@ import {
14
14
  } from '@/messages/core';
15
15
  import {
16
16
  coalesceAdjacentUserTurns,
17
- strictAlternationProviders,
18
17
  appendPredecessorHandoffCue,
19
18
  removePredecessorHandoffCue,
20
19
  } from '@/messages';
@@ -23,8 +22,10 @@ import {
23
22
  stripBedrockCacheControl,
24
23
  } from '@/messages/cache';
25
24
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
26
- import { Providers } from '@/common';
25
+ import { providerRequiresStrictAlternation } from '@/llm/providers';
27
26
  import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
27
+ import { getProviderFamily } from '@/llm/providerRegistry';
28
+ import { Providers } from '@/common';
28
29
 
29
30
  const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
30
31
 
@@ -43,7 +44,7 @@ export interface ProviderPayloadMeasurement {
43
44
  export interface PreparedProviderRequest {
44
45
  readonly model: t.ChatModel;
45
46
  readonly modelId?: string;
46
- readonly provider: Providers;
47
+ readonly provider: t.ProviderName;
47
48
  readonly projectionMode: ProviderMessageProjectionMode;
48
49
  readonly messages: BaseMessage[];
49
50
  readonly measurement?: ProviderPayloadMeasurement;
@@ -63,7 +64,7 @@ export interface ProviderRequestContext {
63
64
  export interface PrepareProviderRequestParams {
64
65
  model: t.ChatModel;
65
66
  messages: BaseMessage[];
66
- provider: Providers;
67
+ provider: t.ProviderName;
67
68
  context?: ProviderRequestContext;
68
69
  config?: RunnableConfig;
69
70
  maxToolResultChars?: number;
@@ -72,7 +73,7 @@ export interface PrepareProviderRequestParams {
72
73
 
73
74
  export function usesNativeOpenAIResponses(
74
75
  model: t.ChatModel,
75
- provider: Providers,
76
+ provider: t.ProviderName,
76
77
  callOptions?: unknown
77
78
  ): boolean {
78
79
  if (!isOpenAILike(provider)) {
@@ -142,7 +143,7 @@ export function usesNativeOpenAIResponses(
142
143
 
143
144
  function resolveProviderMessageProjectionMode(
144
145
  model: t.ChatModel,
145
- provider: Providers,
146
+ provider: t.ProviderName,
146
147
  callOptions?: unknown
147
148
  ): ProviderMessageProjectionMode {
148
149
  return usesNativeOpenAIResponses(model, provider, callOptions)
@@ -153,18 +154,16 @@ function resolveProviderMessageProjectionMode(
153
154
  interface ProjectMessagesForProviderParams {
154
155
  model: t.ChatModel;
155
156
  messages: BaseMessage[];
156
- provider: Providers;
157
+ provider: t.ProviderName;
157
158
  maxToolResultChars?: number;
158
159
  callOptions?: unknown;
159
160
  }
160
161
 
161
- function projectMessagesForProviderMode({
162
- messages,
163
- provider,
164
- maxToolResultChars,
165
- }: ProjectMessagesForProviderParams,
166
- projectionMode: ProviderMessageProjectionMode
162
+ function projectMessagesForProviderMode(
163
+ { messages, provider, maxToolResultChars }: ProjectMessagesForProviderParams,
164
+ projectionMode: ProviderMessageProjectionMode
167
165
  ): BaseMessage[] {
166
+ const providerFamily = getProviderFamily(provider);
168
167
  const nativeOpenAIResponses = projectionMode === 'openai-responses';
169
168
  const providerInputMessages = projectToolStreamContentForProvider(
170
169
  messages,
@@ -197,7 +196,7 @@ projectionMode: ProviderMessageProjectionMode
197
196
  )
198
197
  );
199
198
  }
200
- if (provider === Providers.ANTHROPIC) {
199
+ if (provider === Providers.ANTHROPIC || providerFamily === 'anthropic') {
201
200
  return projectComputerCallOutputsToText(
202
201
  projectSingleTextToolOutputsToText(
203
202
  stripBedrockCacheControl(providerInputMessages),
@@ -205,7 +204,7 @@ projectionMode: ProviderMessageProjectionMode
205
204
  )
206
205
  );
207
206
  }
208
- if (provider === Providers.BEDROCK) {
207
+ if (provider === Providers.BEDROCK || providerFamily === 'bedrock') {
209
208
  return stripAnthropicCacheControl(
210
209
  projectComputerCallOutputsToText(
211
210
  projectCacheControlledToolOutputsToText(
@@ -310,7 +309,7 @@ export function prepareProviderRequest({
310
309
  : (message): boolean => isRunProduced.call(context, message)
311
310
  )
312
311
  : removePredecessorHandoffCue(annotated);
313
- const preparedMessages = strictAlternationProviders.has(provider)
312
+ const preparedMessages = providerRequiresStrictAlternation(provider)
314
313
  ? coalesceAdjacentUserTurns(cued)
315
314
  : cued;
316
315
 
@@ -332,7 +331,7 @@ export function prepareProviderRequest({
332
331
  export function assertPreparedProviderRequestFor(
333
332
  request: PreparedProviderRequest,
334
333
  model: t.ChatModel,
335
- provider: Providers,
334
+ provider: t.ProviderName,
336
335
  config?: RunnableConfig
337
336
  ): void {
338
337
  if (
@@ -344,7 +343,9 @@ export function assertPreparedProviderRequestFor(
344
343
  throw new Error('Prepared provider request does not match serving model');
345
344
  }
346
345
  if (request.provider !== provider) {
347
- throw new Error('Prepared provider request does not match serving provider');
346
+ throw new Error(
347
+ 'Prepared provider request does not match serving provider'
348
+ );
348
349
  }
349
350
  if (
350
351
  request.projectionMode !==
@@ -0,0 +1,134 @@
1
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
2
+ import type {
3
+ ProviderFamily,
4
+ ProviderRegistrationOptions,
5
+ } from '../provider-registration';
6
+ import type { ProviderModelConstructor, ProviderName } from '@/types';
7
+
8
+ interface StoredProviderRegistration {
9
+ model: new (config: never) => BaseChatModel;
10
+ family: ProviderFamily;
11
+ manualToolStream: boolean;
12
+ strictAlternation: boolean;
13
+ owner: symbol;
14
+ }
15
+
16
+ interface ProviderRegistryGlobal {
17
+ [key: symbol]: Map<ProviderName, StoredProviderRegistration> | undefined;
18
+ }
19
+
20
+ const PROVIDER_REGISTRY_KEY = Symbol.for(
21
+ '@librechat/agents:providerRegistry:v1'
22
+ );
23
+ const providerRegistryGlobal = globalThis as ProviderRegistryGlobal;
24
+ const registeredProviders =
25
+ providerRegistryGlobal[PROVIDER_REGISTRY_KEY] ??
26
+ new Map<ProviderName, StoredProviderRegistration>();
27
+ providerRegistryGlobal[PROVIDER_REGISTRY_KEY] = registeredProviders;
28
+ const builtInProviders = new Map<ProviderName, StoredProviderRegistration>();
29
+
30
+ function normalizeProvider(provider: ProviderName): ProviderName {
31
+ if (typeof provider !== 'string' || provider.trim() === '') {
32
+ throw new TypeError('LLM provider name must be a non-empty string');
33
+ }
34
+ if (provider !== provider.trim()) {
35
+ throw new TypeError(
36
+ 'LLM provider name must not have surrounding whitespace'
37
+ );
38
+ }
39
+ return provider;
40
+ }
41
+
42
+ function isConstructible<T extends abstract new (...args: never[]) => object>(
43
+ value: T
44
+ ): boolean {
45
+ try {
46
+ Reflect.construct(String, [], value);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function createRegistration<
54
+ TOptions extends object,
55
+ TModel extends BaseChatModel,
56
+ >(
57
+ provider: ProviderName,
58
+ options: ProviderRegistrationOptions<TOptions, TModel>
59
+ ): StoredProviderRegistration {
60
+ if (typeof options.model !== 'function' || !isConstructible(options.model)) {
61
+ throw new TypeError(`LLM provider constructor is invalid: ${provider}`);
62
+ }
63
+ return {
64
+ model: options.model,
65
+ family: options.family ?? 'generic',
66
+ manualToolStream: options.manualToolStream ?? false,
67
+ strictAlternation: options.strictAlternation ?? false,
68
+ owner: Symbol(provider),
69
+ };
70
+ }
71
+
72
+ function getRegistration(
73
+ provider: ProviderName
74
+ ): StoredProviderRegistration | undefined {
75
+ return builtInProviders.get(provider) ?? registeredProviders.get(provider);
76
+ }
77
+
78
+ /** Registers one host provider until the returned disposer is called. */
79
+ export function registerProvider<
80
+ TOptions extends object,
81
+ TModel extends BaseChatModel,
82
+ >(options: ProviderRegistrationOptions<TOptions, TModel>): () => void {
83
+ const provider = normalizeProvider(options.provider);
84
+ if (getRegistration(provider) != null) {
85
+ throw new Error(`LLM provider already registered: ${provider}`);
86
+ }
87
+
88
+ const registration = createRegistration(provider, options);
89
+ registeredProviders.set(provider, registration);
90
+
91
+ return (): void => {
92
+ if (registeredProviders.get(provider)?.owner === registration.owner) {
93
+ registeredProviders.delete(provider);
94
+ }
95
+ };
96
+ }
97
+
98
+ /** Initializes one built-in for the current package module graph. */
99
+ export function registerBuiltInProvider<
100
+ TOptions extends object,
101
+ TModel extends BaseChatModel,
102
+ >(options: ProviderRegistrationOptions<TOptions, TModel>): void {
103
+ const provider = normalizeProvider(options.provider);
104
+ if (builtInProviders.has(provider)) {
105
+ throw new Error(`LLM provider already registered: ${provider}`);
106
+ }
107
+ builtInProviders.set(provider, createRegistration(provider, options));
108
+ }
109
+
110
+ export function getRegisteredChatModelClass<P extends ProviderName>(
111
+ provider: P
112
+ ): ProviderModelConstructor<P> {
113
+ const registration = getRegistration(provider);
114
+ if (!registration) {
115
+ throw new Error(`Unsupported LLM provider: ${provider}`);
116
+ }
117
+ return registration.model as ProviderModelConstructor<P>;
118
+ }
119
+
120
+ export function getProviderFamily(
121
+ provider: ProviderName
122
+ ): ProviderFamily | undefined {
123
+ return getRegistration(provider)?.family;
124
+ }
125
+
126
+ export function providerUsesManualToolStream(provider: ProviderName): boolean {
127
+ return getRegistration(provider)?.manualToolStream ?? false;
128
+ }
129
+
130
+ export function providerRequiresStrictAlternation(
131
+ provider: ProviderName
132
+ ): boolean {
133
+ return getRegistration(provider)?.strictAlternation ?? false;
134
+ }
@@ -1,10 +1,9 @@
1
- // src/llm/providers.ts
2
- import { CustomChatMistralAI } from '@/llm/mistral';
3
1
  import type {
4
2
  ChatModelConstructorMap,
5
- ProviderOptionsMap,
6
- ChatModelMap,
3
+ ProviderModelConstructor,
4
+ ProviderName,
7
5
  } from '@/types';
6
+ import type { ProviderFamily } from '../provider-registration';
8
7
  import {
9
8
  AzureChatOpenAI,
10
9
  ChatDeepSeek,
@@ -12,41 +11,85 @@ import {
12
11
  ChatOpenAI,
13
12
  ChatXAI,
14
13
  } from '@/llm/openai';
14
+ import {
15
+ getRegisteredChatModelClass,
16
+ registerBuiltInProvider,
17
+ } from '@/llm/providerRegistry';
15
18
  import { CustomChatGoogleGenerativeAI } from '@/llm/google';
16
19
  import { CustomChatBedrockConverse } from '@/llm/bedrock';
20
+ import { CustomChatMistralAI } from '@/llm/mistral';
17
21
  import { CustomAnthropic } from '@/llm/anthropic';
18
22
  import { ChatOpenRouter } from '@/llm/openrouter';
19
23
  import { ChatVertexAI } from '@/llm/vertexai';
20
24
  import { Providers } from '@/common';
21
25
 
22
- export const llmProviders: Partial<ChatModelConstructorMap> = {
23
- [Providers.XAI]: ChatXAI,
24
- [Providers.OPENAI]: ChatOpenAI,
25
- [Providers.AZURE]: AzureChatOpenAI,
26
- [Providers.VERTEXAI]: ChatVertexAI,
27
- [Providers.DEEPSEEK]: ChatDeepSeek,
28
- [Providers.MISTRALAI]: CustomChatMistralAI,
29
- [Providers.MISTRAL]: CustomChatMistralAI,
30
- [Providers.ANTHROPIC]: CustomAnthropic,
31
- [Providers.OPENROUTER]: ChatOpenRouter,
32
- [Providers.BEDROCK]: CustomChatBedrockConverse,
33
- // [Providers.ANTHROPIC]: ChatAnthropic,
34
- [Providers.GOOGLE]: CustomChatGoogleGenerativeAI,
35
- [Providers.MOONSHOT]: ChatMoonshot,
26
+ type BuiltInProviderTraits = {
27
+ family: ProviderFamily;
28
+ manualToolStream?: boolean;
29
+ strictAlternation?: boolean;
36
30
  };
37
31
 
38
- export const manualToolStreamProviders = new Set<Providers | string>([
39
- Providers.ANTHROPIC,
40
- Providers.BEDROCK,
41
- ]);
32
+ function initializeBuiltInProvider<P extends Providers>(
33
+ provider: P,
34
+ model: ChatModelConstructorMap[P],
35
+ traits: BuiltInProviderTraits
36
+ ): void {
37
+ registerBuiltInProvider({
38
+ provider,
39
+ model,
40
+ ...traits,
41
+ });
42
+ }
42
43
 
43
- export const getChatModelClass = <P extends Providers>(
44
+ initializeBuiltInProvider(Providers.XAI, ChatXAI, { family: 'openai' });
45
+ initializeBuiltInProvider(Providers.OPENAI, ChatOpenAI, { family: 'openai' });
46
+ initializeBuiltInProvider(Providers.AZURE, AzureChatOpenAI, {
47
+ family: 'openai',
48
+ });
49
+ initializeBuiltInProvider(Providers.VERTEXAI, ChatVertexAI, {
50
+ family: 'google',
51
+ });
52
+ initializeBuiltInProvider(Providers.DEEPSEEK, ChatDeepSeek, {
53
+ family: 'openai',
54
+ });
55
+ initializeBuiltInProvider(Providers.MISTRALAI, CustomChatMistralAI, {
56
+ family: 'mistral',
57
+ strictAlternation: true,
58
+ });
59
+ initializeBuiltInProvider(Providers.MISTRAL, CustomChatMistralAI, {
60
+ family: 'mistral',
61
+ strictAlternation: true,
62
+ });
63
+ initializeBuiltInProvider(Providers.ANTHROPIC, CustomAnthropic, {
64
+ family: 'anthropic',
65
+ manualToolStream: true,
66
+ });
67
+ initializeBuiltInProvider(Providers.OPENROUTER, ChatOpenRouter, {
68
+ family: 'openai',
69
+ });
70
+ initializeBuiltInProvider(Providers.BEDROCK, CustomChatBedrockConverse, {
71
+ family: 'bedrock',
72
+ manualToolStream: true,
73
+ strictAlternation: true,
74
+ });
75
+ initializeBuiltInProvider(Providers.GOOGLE, CustomChatGoogleGenerativeAI, {
76
+ family: 'google',
77
+ });
78
+ initializeBuiltInProvider(Providers.MOONSHOT, ChatMoonshot, {
79
+ family: 'generic',
80
+ });
81
+
82
+ export const getChatModelClass = <P extends ProviderName>(
44
83
  provider: P
45
- ): new (config: ProviderOptionsMap[P]) => ChatModelMap[P] => {
46
- const ChatModelClass = llmProviders[provider];
47
- if (!ChatModelClass) {
48
- throw new Error(`Unsupported LLM provider: ${provider}`);
49
- }
84
+ ): ProviderModelConstructor<P> => getRegisteredChatModelClass(provider);
50
85
 
51
- return ChatModelClass;
52
- };
86
+ export {
87
+ getProviderFamily,
88
+ providerRequiresStrictAlternation,
89
+ providerUsesManualToolStream,
90
+ registerProvider,
91
+ } from '@/llm/providerRegistry';
92
+ export type {
93
+ ProviderFamily,
94
+ ProviderRegistrationOptions,
95
+ } from '../provider-registration';
@@ -1,4 +1,5 @@
1
1
  import type * as t from '@/types';
2
+ import { getProviderFamily } from '@/llm/providerRegistry';
2
3
  import { Providers } from '@/common';
3
4
 
4
5
  /**
@@ -7,20 +8,21 @@ import { Providers } from '@/common';
7
8
  * and OpenAI-compat (modelKwargs.thinking).
8
9
  */
9
10
  export function isThinkingEnabled(
10
- provider: Providers,
11
+ provider: t.ProviderName,
11
12
  clientOptions?: t.ClientOptions
12
13
  ): boolean {
13
14
  if (!clientOptions) return false;
15
+ const family = getProviderFamily(provider);
14
16
 
15
17
  if (
16
- provider === Providers.ANTHROPIC &&
18
+ (provider === Providers.ANTHROPIC || family === 'anthropic') &&
17
19
  (clientOptions as t.AnthropicClientOptions).thinking != null
18
20
  ) {
19
21
  return true;
20
22
  }
21
23
 
22
24
  if (
23
- provider === Providers.BEDROCK &&
25
+ (provider === Providers.BEDROCK || family === 'bedrock') &&
24
26
  (clientOptions as t.BedrockAnthropicInput).additionalModelRequestFields?.[
25
27
  'thinking'
26
28
  ] != null
@@ -29,7 +31,7 @@ export function isThinkingEnabled(
29
31
  }
30
32
 
31
33
  if (
32
- provider === Providers.OPENAI &&
34
+ (provider === Providers.OPENAI || family === 'openai') &&
33
35
  (
34
36
  (clientOptions as t.OpenAIClientOptions).modelKwargs
35
37
  ?.thinking as t.AnthropicClientOptions['thinking']
@@ -47,9 +49,11 @@ export function isThinkingEnabled(
47
49
  * use `maxTokens`.
48
50
  */
49
51
  export function getMaxOutputTokensKey(
50
- provider: Providers | string
52
+ provider: t.ProviderName
51
53
  ): 'maxOutputTokens' | 'maxTokens' {
52
- return provider === Providers.GOOGLE || provider === Providers.VERTEXAI
54
+ return provider === Providers.GOOGLE ||
55
+ provider === Providers.VERTEXAI ||
56
+ getProviderFamily(provider) === 'google'
53
57
  ? 'maxOutputTokens'
54
58
  : 'maxTokens';
55
59
  }
@@ -1,4 +1,6 @@
1
1
  import type { AIMessageChunk, BaseMessage } from '@langchain/core/messages';
2
+ import type { ProviderName } from '@/types';
3
+ import { getProviderFamily } from '@/llm/providerRegistry';
2
4
  import { Providers } from '@/common';
3
5
 
4
6
  /**
@@ -7,7 +9,7 @@ import { Providers } from '@/common';
7
9
  * `functionCall` whole and seal it on arrival, so a `MAX_TOKENS` finish does
8
10
  * NOT imply the arguments were cut off — the truncation guard must skip them.
9
11
  */
10
- const ATOMIC_TOOL_CALL_ARG_PROVIDERS = new Set<Providers>([
12
+ const ATOMIC_TOOL_CALL_ARG_PROVIDERS = new Set<ProviderName>([
11
13
  Providers.GOOGLE,
12
14
  Providers.VERTEXAI,
13
15
  ]);
@@ -162,12 +164,16 @@ function collectToolCallNames(message: AIMessageChunk | BaseMessage): string[] {
162
164
  */
163
165
  export function assertNotTruncatedToolCall(
164
166
  message: AIMessageChunk | BaseMessage | undefined | null,
165
- provider?: Providers
167
+ provider?: ProviderName
166
168
  ): void {
167
169
  if (message == null) {
168
170
  return;
169
171
  }
170
- if (provider != null && ATOMIC_TOOL_CALL_ARG_PROVIDERS.has(provider)) {
172
+ if (
173
+ provider != null &&
174
+ (ATOMIC_TOOL_CALL_ARG_PROVIDERS.has(provider) ||
175
+ getProviderFamily(provider) === 'google')
176
+ ) {
171
177
  return;
172
178
  }
173
179
  const stopReason = getTruncationStopReason(message);
@@ -90,7 +90,7 @@ const modifyContent = ({
90
90
  messageType,
91
91
  content,
92
92
  }: {
93
- provider: Providers;
93
+ provider: t.ProviderName;
94
94
  messageType: string;
95
95
  content: t.ExtendedMessageContent[];
96
96
  }): (t.ExtendedMessageContent | null)[] => {
@@ -243,7 +243,7 @@ function hasReasoningContent(content: BaseMessage['content']): boolean {
243
243
  }
244
244
 
245
245
  export function modifyDeltaProperties(
246
- provider: Providers,
246
+ provider: t.ProviderName,
247
247
  obj?: AIMessageChunk
248
248
  ): AIMessageChunk | undefined {
249
249
  if (!obj || typeof obj !== 'object') return obj;