@librechat/agents 3.2.61 → 3.2.63
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/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +6 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +32 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +6 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +62 -1
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +266 -43
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +274 -7
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
- package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
- package/dist/cjs/llm/openrouter/index.cjs +5 -6
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -1
- package/dist/cjs/messages/format.cjs +61 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +31 -19
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +13 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +76 -11
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +2 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +111 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +6 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +32 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +6 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +62 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +266 -43
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +274 -7
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
- package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
- package/dist/esm/llm/openrouter/index.mjs +5 -6
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/format.mjs +61 -0
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +31 -19
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +13 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +76 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +108 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +3 -1
- package/dist/types/graphs/Graph.d.ts +3 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -0
- package/dist/types/hooks/index.d.ts +7 -0
- package/dist/types/hooks/types.d.ts +18 -1
- package/dist/types/llm/bedrock/index.d.ts +5 -0
- package/dist/types/llm/openai/index.d.ts +19 -0
- package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
- package/dist/types/messages/prune.d.ts +4 -3
- package/dist/types/tools/ToolNode.d.ts +1 -0
- package/dist/types/types/graph.d.ts +10 -0
- package/dist/types/types/llm.d.ts +12 -2
- package/dist/types/types/tools.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +30 -0
- package/package.json +6 -6
- package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +12 -4
- package/src/hooks/HookRegistry.ts +45 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
- package/src/hooks/__tests__/executeHooks.test.ts +85 -2
- package/src/hooks/executeHooks.ts +15 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/types.ts +18 -1
- package/src/langfuse.ts +134 -1
- package/src/llm/bedrock/index.ts +434 -83
- package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
- package/src/llm/custom-chat-models.smoke.test.ts +7 -0
- package/src/llm/openai/index.ts +604 -6
- package/src/llm/openai/managedRequests.test.ts +182 -0
- package/src/llm/openai/streamMetadata.spec.ts +86 -0
- package/src/llm/openai/streamMetadata.ts +95 -0
- package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
- package/src/llm/openrouter/index.ts +9 -5
- package/src/messages/format.ts +96 -3
- package/src/messages/formatAgentMessages.steer.test.ts +326 -0
- package/src/messages/labelContentByAgent.test.ts +75 -0
- package/src/messages/prune.ts +56 -30
- package/src/specs/anthropic.simple.test.ts +4 -2
- package/src/specs/cache.simple.test.ts +17 -4
- package/src/specs/langfuse-callbacks.test.ts +61 -0
- package/src/specs/openai.simple.test.ts +4 -2
- package/src/specs/spec.utils.ts +12 -0
- package/src/specs/summarization.test.ts +9 -13
- package/src/specs/token-accounting-pipeline.test.ts +130 -4
- package/src/specs/tokens.test.ts +214 -0
- package/src/stream.ts +22 -3
- package/src/tools/ToolNode.ts +112 -13
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
- package/src/tools/__tests__/hitl.test.ts +112 -0
- package/src/tools/__tests__/subagentHooks.test.ts +124 -0
- package/src/tools/subagent/SubagentExecutor.ts +3 -0
- package/src/types/graph.ts +10 -0
- package/src/types/llm.ts +12 -2
- package/src/types/tools.ts +1 -1
- package/src/utils/tokens.ts +181 -3
package/src/llm/openai/index.ts
CHANGED
|
@@ -33,12 +33,14 @@ import type { BindToolsInput } from '@langchain/core/language_models/chat_models
|
|
|
33
33
|
import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
|
|
34
34
|
import type { ChatXAIInput } from '@langchain/xai';
|
|
35
35
|
import type * as t from '@langchain/openai';
|
|
36
|
+
import type { SeenScalarMetadata } from './streamMetadata';
|
|
36
37
|
import type { HeaderValue, HeadersLike } from './types';
|
|
37
38
|
import {
|
|
38
39
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
39
40
|
OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
|
|
40
41
|
} from '@/tools/streamedToolCallSeals';
|
|
41
42
|
import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
|
|
43
|
+
import { dropRepeatedScalarMetadata } from './streamMetadata';
|
|
42
44
|
|
|
43
45
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
44
46
|
const iife = <T>(fn: () => T) => fn();
|
|
@@ -100,9 +102,13 @@ type LibreChatOpenAIFields = t.ChatOpenAIFields & {
|
|
|
100
102
|
includeReasoningContent?: boolean;
|
|
101
103
|
includeReasoningDetails?: boolean;
|
|
102
104
|
convertReasoningDetailsToContent?: boolean;
|
|
105
|
+
promptCacheExplicit?: boolean;
|
|
106
|
+
safety_identifier?: string;
|
|
103
107
|
};
|
|
104
108
|
type LibreChatAzureOpenAIFields = t.AzureOpenAIInput & {
|
|
105
109
|
_lc_stream_delay?: number;
|
|
110
|
+
promptCacheExplicit?: boolean;
|
|
111
|
+
safety_identifier?: string;
|
|
106
112
|
};
|
|
107
113
|
type ReasoningCallOptions = {
|
|
108
114
|
reasoning?: OpenAIClient.Reasoning;
|
|
@@ -151,6 +157,366 @@ type OpenAIChatCompletionRetry = (
|
|
|
151
157
|
) => Promise<
|
|
152
158
|
AsyncIterable<OpenAIChatCompletionStreamItem> | OpenAIChatCompletion
|
|
153
159
|
>;
|
|
160
|
+
type OpenAIManagedRequestFields = {
|
|
161
|
+
promptCacheExplicit?: boolean;
|
|
162
|
+
safetyIdentifier?: string;
|
|
163
|
+
};
|
|
164
|
+
type OpenAIManagedRequestParams = {
|
|
165
|
+
prompt_cache_options?: {
|
|
166
|
+
mode: 'explicit';
|
|
167
|
+
ttl: '30m';
|
|
168
|
+
};
|
|
169
|
+
safety_identifier?: string;
|
|
170
|
+
};
|
|
171
|
+
type ResponsesRequest =
|
|
172
|
+
| OpenAIClient.Responses.ResponseCreateParamsStreaming
|
|
173
|
+
| OpenAIClient.Responses.ResponseCreateParamsNonStreaming;
|
|
174
|
+
type ResponsesResult =
|
|
175
|
+
| AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>
|
|
176
|
+
| OpenAIClient.Responses.Response;
|
|
177
|
+
type CacheableChatPart = {
|
|
178
|
+
type: 'text' | 'image_url' | 'input_audio' | 'file' | 'refusal';
|
|
179
|
+
prompt_cache_breakpoint?: { mode: 'explicit' };
|
|
180
|
+
[key: string]: unknown;
|
|
181
|
+
};
|
|
182
|
+
type CacheableResponsePart = (
|
|
183
|
+
| OpenAIClient.Responses.ResponseInputText
|
|
184
|
+
| OpenAIClient.Responses.ResponseInputImage
|
|
185
|
+
| OpenAIClient.Responses.ResponseInputFile
|
|
186
|
+
) & {
|
|
187
|
+
prompt_cache_breakpoint?: { mode: 'explicit' };
|
|
188
|
+
};
|
|
189
|
+
type ResponsesUsageWithCacheWrite = OpenAIClient.Responses.ResponseUsage & {
|
|
190
|
+
input_tokens_details?: OpenAIClient.Responses.ResponseUsage['input_tokens_details'] & {
|
|
191
|
+
cache_write_tokens?: number;
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const CACHE_WRITE_METADATA_KEY = '__librechat_cache_write_tokens';
|
|
195
|
+
|
|
196
|
+
function applyManagedRequestParams<T extends object>(
|
|
197
|
+
params: T,
|
|
198
|
+
fields: OpenAIManagedRequestFields
|
|
199
|
+
): T & OpenAIManagedRequestParams {
|
|
200
|
+
return {
|
|
201
|
+
...params,
|
|
202
|
+
...(fields.promptCacheExplicit === true && {
|
|
203
|
+
prompt_cache_options: {
|
|
204
|
+
mode: 'explicit' as const,
|
|
205
|
+
ttl: '30m' as const,
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
...(fields.safetyIdentifier != null && {
|
|
209
|
+
safety_identifier: fields.safetyIdentifier,
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isCacheableChatPart(part: unknown): part is CacheableChatPart {
|
|
215
|
+
if (typeof part !== 'object' || part == null || !('type' in part)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return (
|
|
219
|
+
part.type === 'text' ||
|
|
220
|
+
part.type === 'image_url' ||
|
|
221
|
+
part.type === 'input_audio' ||
|
|
222
|
+
part.type === 'file' ||
|
|
223
|
+
part.type === 'refusal'
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function canAddChatBreakpoint(
|
|
228
|
+
message: OpenAIClient.Chat.Completions.ChatCompletionMessageParam
|
|
229
|
+
): boolean {
|
|
230
|
+
if (
|
|
231
|
+
message.role !== 'system' &&
|
|
232
|
+
message.role !== 'developer' &&
|
|
233
|
+
message.role !== 'user' &&
|
|
234
|
+
message.role !== 'assistant' &&
|
|
235
|
+
message.role !== 'tool'
|
|
236
|
+
) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (typeof message.content === 'string') {
|
|
240
|
+
return message.content.length > 0;
|
|
241
|
+
}
|
|
242
|
+
return (
|
|
243
|
+
Array.isArray(message.content) &&
|
|
244
|
+
message.content.some((part) => isCacheableChatPart(part))
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addChatBreakpoint(
|
|
249
|
+
message: OpenAIClient.Chat.Completions.ChatCompletionMessageParam
|
|
250
|
+
): OpenAIClient.Chat.Completions.ChatCompletionMessageParam {
|
|
251
|
+
if (!canAddChatBreakpoint(message)) {
|
|
252
|
+
return message;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (typeof message.content === 'string') {
|
|
256
|
+
return {
|
|
257
|
+
...message,
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: message.content,
|
|
262
|
+
prompt_cache_breakpoint: { mode: 'explicit' },
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
} as unknown as OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
|
|
266
|
+
}
|
|
267
|
+
if (!Array.isArray(message.content) || message.content.length === 0) {
|
|
268
|
+
return message;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = [...message.content] as unknown as CacheableChatPart[];
|
|
272
|
+
let index = content.length - 1;
|
|
273
|
+
while (index >= 0 && !isCacheableChatPart(content[index])) {
|
|
274
|
+
index--;
|
|
275
|
+
}
|
|
276
|
+
if (index < 0) {
|
|
277
|
+
return message;
|
|
278
|
+
}
|
|
279
|
+
content[index] = {
|
|
280
|
+
...content[index],
|
|
281
|
+
prompt_cache_breakpoint: { mode: 'explicit' },
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
...message,
|
|
285
|
+
content,
|
|
286
|
+
} as unknown as OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function selectCacheBreakpointIndexes(
|
|
290
|
+
roles: Array<string | undefined>,
|
|
291
|
+
cacheable: boolean[]
|
|
292
|
+
): number[] {
|
|
293
|
+
let instructionIndex = -1;
|
|
294
|
+
let latestUserIndex = -1;
|
|
295
|
+
for (let index = 0; index < roles.length; index++) {
|
|
296
|
+
const role = roles[index];
|
|
297
|
+
if ((role === 'system' || role === 'developer') && cacheable[index]) {
|
|
298
|
+
instructionIndex = index;
|
|
299
|
+
}
|
|
300
|
+
if (role === 'user') {
|
|
301
|
+
latestUserIndex = index;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const indexes = new Set<number>();
|
|
306
|
+
if (instructionIndex >= 0) {
|
|
307
|
+
indexes.add(instructionIndex);
|
|
308
|
+
}
|
|
309
|
+
for (let index = latestUserIndex - 1; index >= 0; index--) {
|
|
310
|
+
if (cacheable[index]) {
|
|
311
|
+
indexes.add(index);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return [...indexes];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @internal */
|
|
319
|
+
export function addChatCacheBreakpoints(
|
|
320
|
+
messages: OpenAIClient.Chat.Completions.ChatCompletionMessageParam[]
|
|
321
|
+
): OpenAIClient.Chat.Completions.ChatCompletionMessageParam[] {
|
|
322
|
+
const indexes = new Set(
|
|
323
|
+
selectCacheBreakpointIndexes(
|
|
324
|
+
messages.map((message) => message.role),
|
|
325
|
+
messages.map((message) => canAddChatBreakpoint(message))
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
return messages.map((message, index) =>
|
|
329
|
+
indexes.has(index) ? addChatBreakpoint(message) : message
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isResponseMessage(
|
|
334
|
+
item: OpenAIClient.Responses.ResponseInputItem
|
|
335
|
+
): item is OpenAIClient.Responses.ResponseInputItem.Message {
|
|
336
|
+
return item.type === 'message';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Only `input_text`/`input_image`/`input_file` accept a Responses breakpoint;
|
|
340
|
+
* `output_text`/`refusal` (replayed assistant blocks) are rejected with a 400. */
|
|
341
|
+
function isCacheableResponsePart(part: unknown): part is CacheableResponsePart {
|
|
342
|
+
if (typeof part !== 'object' || part == null || !('type' in part)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
return (
|
|
346
|
+
part.type === 'input_text' ||
|
|
347
|
+
part.type === 'input_image' ||
|
|
348
|
+
part.type === 'input_file'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function addResponseBreakpoint(
|
|
353
|
+
item: OpenAIClient.Responses.ResponseInputItem
|
|
354
|
+
): OpenAIClient.Responses.ResponseInputItem {
|
|
355
|
+
if (!isResponseMessage(item)) {
|
|
356
|
+
return item;
|
|
357
|
+
}
|
|
358
|
+
const rawContent = item.content as
|
|
359
|
+
| string
|
|
360
|
+
| OpenAIClient.Responses.ResponseInputMessageContentList;
|
|
361
|
+
if (typeof rawContent === 'string') {
|
|
362
|
+
if (rawContent.length === 0) {
|
|
363
|
+
return item;
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
...item,
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: 'input_text',
|
|
370
|
+
text: rawContent,
|
|
371
|
+
prompt_cache_breakpoint: { mode: 'explicit' },
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
} as OpenAIClient.Responses.ResponseInputItem;
|
|
375
|
+
}
|
|
376
|
+
if (!Array.isArray(rawContent) || rawContent.length === 0) {
|
|
377
|
+
return item;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const content = [...rawContent];
|
|
381
|
+
let index = content.length - 1;
|
|
382
|
+
while (index >= 0 && !isCacheableResponsePart(content[index])) {
|
|
383
|
+
index--;
|
|
384
|
+
}
|
|
385
|
+
if (index < 0) {
|
|
386
|
+
return item;
|
|
387
|
+
}
|
|
388
|
+
content[index] = {
|
|
389
|
+
...(content[index] as CacheableResponsePart),
|
|
390
|
+
prompt_cache_breakpoint: { mode: 'explicit' },
|
|
391
|
+
};
|
|
392
|
+
return { ...item, content };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @internal */
|
|
396
|
+
export function addResponseCacheBreakpoints(
|
|
397
|
+
input: OpenAIClient.Responses.ResponseCreateParams['input']
|
|
398
|
+
): OpenAIClient.Responses.ResponseCreateParams['input'] {
|
|
399
|
+
if (!Array.isArray(input)) {
|
|
400
|
+
return input;
|
|
401
|
+
}
|
|
402
|
+
const indexes = new Set(
|
|
403
|
+
selectCacheBreakpointIndexes(
|
|
404
|
+
input.map((item) => (isResponseMessage(item) ? item.role : undefined)),
|
|
405
|
+
input.map((item) => {
|
|
406
|
+
if (!isResponseMessage(item)) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
/** Only input roles take a Responses breakpoint. Assistant/tool turns
|
|
410
|
+
* carry output content (string or output_text) that the API rejects
|
|
411
|
+
* under an input marker, so they're never eligible. */
|
|
412
|
+
if (
|
|
413
|
+
item.role !== 'system' &&
|
|
414
|
+
item.role !== 'developer' &&
|
|
415
|
+
item.role !== 'user'
|
|
416
|
+
) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const content = item.content as
|
|
420
|
+
| string
|
|
421
|
+
| OpenAIClient.Responses.ResponseInputMessageContentList;
|
|
422
|
+
return typeof content === 'string'
|
|
423
|
+
? content.length > 0
|
|
424
|
+
: Array.isArray(content) &&
|
|
425
|
+
content.some((part) => isCacheableResponsePart(part));
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
return input.map((item, index) =>
|
|
430
|
+
indexes.has(index) ? addResponseBreakpoint(item) : item
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** @internal */
|
|
435
|
+
export function shouldIncludeEncryptedReasoning(
|
|
436
|
+
model: string,
|
|
437
|
+
params: {
|
|
438
|
+
store?: boolean | null;
|
|
439
|
+
reasoning?: unknown;
|
|
440
|
+
}
|
|
441
|
+
): boolean {
|
|
442
|
+
const reasoningContext = (
|
|
443
|
+
params.reasoning as
|
|
444
|
+
| { context?: 'auto' | 'current_turn' | 'all_turns' }
|
|
445
|
+
| undefined
|
|
446
|
+
)?.context;
|
|
447
|
+
return (
|
|
448
|
+
/^gpt-5\.6(?:-|$)/i.test(model) &&
|
|
449
|
+
(params.store === false || reasoningContext !== 'current_turn')
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getCacheWriteTokens(message: BaseMessage): number | undefined {
|
|
454
|
+
const responseMetadata = message.response_metadata as {
|
|
455
|
+
usage?: ResponsesUsageWithCacheWrite;
|
|
456
|
+
metadata?: Record<string, string>;
|
|
457
|
+
};
|
|
458
|
+
const reported =
|
|
459
|
+
responseMetadata.usage?.input_tokens_details.cache_write_tokens;
|
|
460
|
+
if (reported != null) {
|
|
461
|
+
return reported;
|
|
462
|
+
}
|
|
463
|
+
const serialized = responseMetadata.metadata?.[CACHE_WRITE_METADATA_KEY];
|
|
464
|
+
if (serialized == null) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const parsed = Number(serialized);
|
|
468
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function attachCacheWriteUsage(message: BaseMessage): void {
|
|
472
|
+
const cacheWriteTokens = getCacheWriteTokens(message);
|
|
473
|
+
if (
|
|
474
|
+
cacheWriteTokens == null ||
|
|
475
|
+
!isAIMessage(message) ||
|
|
476
|
+
message.usage_metadata == null
|
|
477
|
+
) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
message.usage_metadata.input_token_details = {
|
|
481
|
+
...message.usage_metadata.input_token_details,
|
|
482
|
+
cache_creation: cacheWriteTokens,
|
|
483
|
+
};
|
|
484
|
+
const responseMetadata = message.response_metadata as {
|
|
485
|
+
metadata?: Record<string, string>;
|
|
486
|
+
};
|
|
487
|
+
if (responseMetadata.metadata?.[CACHE_WRITE_METADATA_KEY] == null) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const metadata = { ...responseMetadata.metadata };
|
|
491
|
+
delete metadata[CACHE_WRITE_METADATA_KEY];
|
|
492
|
+
message.response_metadata = {
|
|
493
|
+
...message.response_metadata,
|
|
494
|
+
metadata,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function attachCacheWriteMetadata(
|
|
499
|
+
response: OpenAIClient.Responses.Response
|
|
500
|
+
): OpenAIClient.Responses.Response {
|
|
501
|
+
const usage = response.usage as ResponsesUsageWithCacheWrite | undefined;
|
|
502
|
+
const cacheWriteTokens = usage?.input_tokens_details.cache_write_tokens;
|
|
503
|
+
if (cacheWriteTokens == null) {
|
|
504
|
+
return response;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
...response,
|
|
508
|
+
metadata: {
|
|
509
|
+
...(response.metadata ?? {}),
|
|
510
|
+
[CACHE_WRITE_METADATA_KEY]: String(cacheWriteTokens),
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isResponsesStream(
|
|
516
|
+
result: ResponsesResult
|
|
517
|
+
): result is AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent> {
|
|
518
|
+
return Symbol.asyncIterator in result;
|
|
519
|
+
}
|
|
154
520
|
|
|
155
521
|
function createUsageMetadata(
|
|
156
522
|
usage?: OpenAIClient.Completions.CompletionUsage
|
|
@@ -514,7 +880,13 @@ async function* delayStreamChunks(
|
|
|
514
880
|
chunks: AsyncGenerator<ChatGenerationChunk>,
|
|
515
881
|
delay?: number,
|
|
516
882
|
signal?: AbortSignal,
|
|
517
|
-
runManager?: CallbackManagerForLLMRun
|
|
883
|
+
runManager?: CallbackManagerForLLMRun,
|
|
884
|
+
// When provided, de-duplicate repeated scalar metadata just before emitting,
|
|
885
|
+
// so token callbacks and the yielded chunk observe the same cleaned data.
|
|
886
|
+
// Omitted by callers that wrap this stream and finalize downstream (e.g.
|
|
887
|
+
// `ChatOpenRouter`, which needs the raw `finish_reason` as its flush signal
|
|
888
|
+
// and de-duplicates after its own processing).
|
|
889
|
+
seenScalarMetadata?: SeenScalarMetadata
|
|
518
890
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
519
891
|
let lastYieldedAt: number | undefined;
|
|
520
892
|
for await (const chunk of chunks) {
|
|
@@ -531,6 +903,9 @@ async function* delayStreamChunks(
|
|
|
531
903
|
}
|
|
532
904
|
signal?.throwIfAborted();
|
|
533
905
|
lastYieldedAt = Date.now();
|
|
906
|
+
if (seenScalarMetadata != null) {
|
|
907
|
+
dropRepeatedScalarMetadata(outputChunk, seenScalarMetadata);
|
|
908
|
+
}
|
|
534
909
|
await emitStreamChunkCallback(outputChunk, runManager);
|
|
535
910
|
signal?.throwIfAborted();
|
|
536
911
|
yield outputChunk;
|
|
@@ -741,6 +1116,8 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
|
|
|
741
1116
|
private includeReasoningContent?: boolean;
|
|
742
1117
|
private includeReasoningDetails?: boolean;
|
|
743
1118
|
private convertReasoningDetailsToContent?: boolean;
|
|
1119
|
+
private promptCacheExplicit?: boolean;
|
|
1120
|
+
private safetyIdentifier?: string;
|
|
744
1121
|
|
|
745
1122
|
constructor(fields?: LibreChatOpenAIFields) {
|
|
746
1123
|
super(fields);
|
|
@@ -748,6 +1125,18 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
|
|
|
748
1125
|
this.includeReasoningDetails = fields?.includeReasoningDetails;
|
|
749
1126
|
this.convertReasoningDetailsToContent =
|
|
750
1127
|
fields?.convertReasoningDetailsToContent;
|
|
1128
|
+
this.promptCacheExplicit = fields?.promptCacheExplicit;
|
|
1129
|
+
this.safetyIdentifier = fields?.safety_identifier;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
invocationParams(
|
|
1133
|
+
options?: this['ParsedCallOptions'],
|
|
1134
|
+
extra?: { streaming?: boolean }
|
|
1135
|
+
): ReturnType<OriginalChatOpenAICompletions['invocationParams']> {
|
|
1136
|
+
return applyManagedRequestParams(super.invocationParams(options, extra), {
|
|
1137
|
+
promptCacheExplicit: this.promptCacheExplicit,
|
|
1138
|
+
safetyIdentifier: this.safetyIdentifier,
|
|
1139
|
+
});
|
|
751
1140
|
}
|
|
752
1141
|
|
|
753
1142
|
protected _getReasoningParams(
|
|
@@ -777,7 +1166,9 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
|
|
|
777
1166
|
requestOptions?: OpenAICoreRequestOptions
|
|
778
1167
|
): Promise<AsyncIterable<OpenAIChatCompletionChunk> | OpenAIChatCompletion> {
|
|
779
1168
|
return completionWithFilteredOpenAIStream(
|
|
780
|
-
|
|
1169
|
+
this.promptCacheExplicit === true
|
|
1170
|
+
? { ...request, messages: addChatCacheBreakpoints(request.messages) }
|
|
1171
|
+
: request,
|
|
781
1172
|
requestOptions,
|
|
782
1173
|
super.completionWithRetry.bind(this) as OpenAIChatCompletionRetry
|
|
783
1174
|
);
|
|
@@ -1141,6 +1532,88 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
|
|
|
1141
1532
|
}
|
|
1142
1533
|
|
|
1143
1534
|
class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
|
|
1535
|
+
private promptCacheExplicit?: boolean;
|
|
1536
|
+
private safetyIdentifier?: string;
|
|
1537
|
+
|
|
1538
|
+
constructor(fields?: LibreChatOpenAIFields) {
|
|
1539
|
+
super(fields);
|
|
1540
|
+
this.promptCacheExplicit = fields?.promptCacheExplicit;
|
|
1541
|
+
this.safetyIdentifier = fields?.safety_identifier;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
invocationParams(
|
|
1545
|
+
options?: this['ParsedCallOptions']
|
|
1546
|
+
): ReturnType<OriginalChatOpenAIResponses['invocationParams']> {
|
|
1547
|
+
const params = applyManagedRequestParams(super.invocationParams(options), {
|
|
1548
|
+
promptCacheExplicit: this.promptCacheExplicit,
|
|
1549
|
+
safetyIdentifier: this.safetyIdentifier,
|
|
1550
|
+
});
|
|
1551
|
+
if (shouldIncludeEncryptedReasoning(this.model, params)) {
|
|
1552
|
+
params.include = [
|
|
1553
|
+
...new Set([
|
|
1554
|
+
...(params.include ?? []),
|
|
1555
|
+
'reasoning.encrypted_content' as const,
|
|
1556
|
+
]),
|
|
1557
|
+
];
|
|
1558
|
+
}
|
|
1559
|
+
return params;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async completionWithRetry(
|
|
1563
|
+
request: OpenAIClient.Responses.ResponseCreateParamsStreaming,
|
|
1564
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1565
|
+
): Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
|
|
1566
|
+
async completionWithRetry(
|
|
1567
|
+
request: OpenAIClient.Responses.ResponseCreateParamsNonStreaming,
|
|
1568
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1569
|
+
): Promise<OpenAIClient.Responses.Response>;
|
|
1570
|
+
async completionWithRetry(
|
|
1571
|
+
request: ResponsesRequest,
|
|
1572
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1573
|
+
): Promise<ResponsesResult> {
|
|
1574
|
+
const managedRequest = {
|
|
1575
|
+
...request,
|
|
1576
|
+
input:
|
|
1577
|
+
this.promptCacheExplicit === true
|
|
1578
|
+
? addResponseCacheBreakpoints(request.input)
|
|
1579
|
+
: request.input,
|
|
1580
|
+
};
|
|
1581
|
+
const result = await super.completionWithRetry(
|
|
1582
|
+
managedRequest as OpenAIClient.Responses.ResponseCreateParamsStreaming,
|
|
1583
|
+
requestOptions
|
|
1584
|
+
);
|
|
1585
|
+
return isResponsesStream(result)
|
|
1586
|
+
? result
|
|
1587
|
+
: attachCacheWriteMetadata(result);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
async _generate(
|
|
1591
|
+
messages: BaseMessage[],
|
|
1592
|
+
options: this['ParsedCallOptions'],
|
|
1593
|
+
runManager?: CallbackManagerForLLMRun
|
|
1594
|
+
): Promise<ChatResult> {
|
|
1595
|
+
const result = await super._generate(messages, options, runManager);
|
|
1596
|
+
for (const generation of result.generations) {
|
|
1597
|
+
attachCacheWriteUsage(generation.message);
|
|
1598
|
+
}
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async *_streamResponseChunks(
|
|
1603
|
+
messages: BaseMessage[],
|
|
1604
|
+
options: this['ParsedCallOptions'],
|
|
1605
|
+
runManager?: CallbackManagerForLLMRun
|
|
1606
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
1607
|
+
for await (const chunk of super._streamResponseChunks(
|
|
1608
|
+
messages,
|
|
1609
|
+
options,
|
|
1610
|
+
runManager
|
|
1611
|
+
)) {
|
|
1612
|
+
attachCacheWriteUsage(chunk.message);
|
|
1613
|
+
yield chunk;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1144
1617
|
protected _getReasoningParams(
|
|
1145
1618
|
options?: this['ParsedCallOptions']
|
|
1146
1619
|
): OpenAIClient.Reasoning | undefined {
|
|
@@ -1155,6 +1628,25 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
|
|
|
1155
1628
|
}
|
|
1156
1629
|
|
|
1157
1630
|
class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions {
|
|
1631
|
+
private promptCacheExplicit?: boolean;
|
|
1632
|
+
private safetyIdentifier?: string;
|
|
1633
|
+
|
|
1634
|
+
constructor(fields?: LibreChatAzureOpenAIFields) {
|
|
1635
|
+
super(fields);
|
|
1636
|
+
this.promptCacheExplicit = fields?.promptCacheExplicit;
|
|
1637
|
+
this.safetyIdentifier = fields?.safety_identifier;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
invocationParams(
|
|
1641
|
+
options?: this['ParsedCallOptions'],
|
|
1642
|
+
extra?: { streaming?: boolean }
|
|
1643
|
+
): ReturnType<OriginalAzureChatOpenAICompletions['invocationParams']> {
|
|
1644
|
+
return applyManagedRequestParams(super.invocationParams(options, extra), {
|
|
1645
|
+
promptCacheExplicit: this.promptCacheExplicit,
|
|
1646
|
+
safetyIdentifier: this.safetyIdentifier,
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1158
1650
|
protected _getReasoningParams(
|
|
1159
1651
|
options?: this['ParsedCallOptions']
|
|
1160
1652
|
): OpenAIClient.Reasoning | undefined {
|
|
@@ -1262,7 +1754,9 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
|
|
|
1262
1754
|
requestOptions?: OpenAICoreRequestOptions
|
|
1263
1755
|
): Promise<AsyncIterable<OpenAIChatCompletionChunk> | OpenAIChatCompletion> {
|
|
1264
1756
|
return completionWithFilteredOpenAIStream(
|
|
1265
|
-
|
|
1757
|
+
this.promptCacheExplicit === true
|
|
1758
|
+
? { ...request, messages: addChatCacheBreakpoints(request.messages) }
|
|
1759
|
+
: request,
|
|
1266
1760
|
requestOptions,
|
|
1267
1761
|
super.completionWithRetry.bind(this) as OpenAIChatCompletionRetry
|
|
1268
1762
|
);
|
|
@@ -1270,6 +1764,88 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
|
|
|
1270
1764
|
}
|
|
1271
1765
|
|
|
1272
1766
|
class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
|
|
1767
|
+
private promptCacheExplicit?: boolean;
|
|
1768
|
+
private safetyIdentifier?: string;
|
|
1769
|
+
|
|
1770
|
+
constructor(fields?: LibreChatAzureOpenAIFields) {
|
|
1771
|
+
super(fields);
|
|
1772
|
+
this.promptCacheExplicit = fields?.promptCacheExplicit;
|
|
1773
|
+
this.safetyIdentifier = fields?.safety_identifier;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
invocationParams(
|
|
1777
|
+
options?: this['ParsedCallOptions']
|
|
1778
|
+
): ReturnType<OriginalAzureChatOpenAIResponses['invocationParams']> {
|
|
1779
|
+
const params = applyManagedRequestParams(super.invocationParams(options), {
|
|
1780
|
+
promptCacheExplicit: this.promptCacheExplicit,
|
|
1781
|
+
safetyIdentifier: this.safetyIdentifier,
|
|
1782
|
+
});
|
|
1783
|
+
if (shouldIncludeEncryptedReasoning(this.model, params)) {
|
|
1784
|
+
params.include = [
|
|
1785
|
+
...new Set([
|
|
1786
|
+
...(params.include ?? []),
|
|
1787
|
+
'reasoning.encrypted_content' as const,
|
|
1788
|
+
]),
|
|
1789
|
+
];
|
|
1790
|
+
}
|
|
1791
|
+
return params;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async completionWithRetry(
|
|
1795
|
+
request: OpenAIClient.Responses.ResponseCreateParamsStreaming,
|
|
1796
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1797
|
+
): Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
|
|
1798
|
+
async completionWithRetry(
|
|
1799
|
+
request: OpenAIClient.Responses.ResponseCreateParamsNonStreaming,
|
|
1800
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1801
|
+
): Promise<OpenAIClient.Responses.Response>;
|
|
1802
|
+
async completionWithRetry(
|
|
1803
|
+
request: ResponsesRequest,
|
|
1804
|
+
requestOptions?: OpenAICoreRequestOptions
|
|
1805
|
+
): Promise<ResponsesResult> {
|
|
1806
|
+
const managedRequest = {
|
|
1807
|
+
...request,
|
|
1808
|
+
input:
|
|
1809
|
+
this.promptCacheExplicit === true
|
|
1810
|
+
? addResponseCacheBreakpoints(request.input)
|
|
1811
|
+
: request.input,
|
|
1812
|
+
};
|
|
1813
|
+
const result = await super.completionWithRetry(
|
|
1814
|
+
managedRequest as OpenAIClient.Responses.ResponseCreateParamsStreaming,
|
|
1815
|
+
requestOptions
|
|
1816
|
+
);
|
|
1817
|
+
return isResponsesStream(result)
|
|
1818
|
+
? result
|
|
1819
|
+
: attachCacheWriteMetadata(result);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
async _generate(
|
|
1823
|
+
messages: BaseMessage[],
|
|
1824
|
+
options: this['ParsedCallOptions'],
|
|
1825
|
+
runManager?: CallbackManagerForLLMRun
|
|
1826
|
+
): Promise<ChatResult> {
|
|
1827
|
+
const result = await super._generate(messages, options, runManager);
|
|
1828
|
+
for (const generation of result.generations) {
|
|
1829
|
+
attachCacheWriteUsage(generation.message);
|
|
1830
|
+
}
|
|
1831
|
+
return result;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
async *_streamResponseChunks(
|
|
1835
|
+
messages: BaseMessage[],
|
|
1836
|
+
options: this['ParsedCallOptions'],
|
|
1837
|
+
runManager?: CallbackManagerForLLMRun
|
|
1838
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
1839
|
+
for await (const chunk of super._streamResponseChunks(
|
|
1840
|
+
messages,
|
|
1841
|
+
options,
|
|
1842
|
+
runManager
|
|
1843
|
+
)) {
|
|
1844
|
+
attachCacheWriteUsage(chunk.message);
|
|
1845
|
+
yield chunk;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1273
1849
|
protected _getReasoningParams(
|
|
1274
1850
|
options?: this['ParsedCallOptions']
|
|
1275
1851
|
): OpenAIClient.Reasoning | undefined {
|
|
@@ -1422,6 +1998,25 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
|
1422
1998
|
runManager?: CallbackManagerForLLMRun
|
|
1423
1999
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1424
2000
|
yield* delayStreamChunks(
|
|
2001
|
+
super._streamResponseChunks(messages, options, undefined),
|
|
2002
|
+
this._lc_stream_delay,
|
|
2003
|
+
options.signal,
|
|
2004
|
+
runManager,
|
|
2005
|
+
new Map()
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Raw variant that skips scalar-metadata de-duplication. Used by subclasses
|
|
2011
|
+
* (e.g. `ChatOpenRouter`) that read `finish_reason` as a control signal and
|
|
2012
|
+
* must de-duplicate only after their own finalization.
|
|
2013
|
+
*/
|
|
2014
|
+
protected _streamRawResponseChunks(
|
|
2015
|
+
messages: BaseMessage[],
|
|
2016
|
+
options: this['ParsedCallOptions'],
|
|
2017
|
+
runManager?: CallbackManagerForLLMRun
|
|
2018
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
2019
|
+
return delayStreamChunks(
|
|
1425
2020
|
super._streamResponseChunks(messages, options, undefined),
|
|
1426
2021
|
this._lc_stream_delay,
|
|
1427
2022
|
options.signal,
|
|
@@ -1537,7 +2132,8 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
|
|
|
1537
2132
|
super._streamResponseChunks(messages, options, undefined),
|
|
1538
2133
|
this._lc_stream_delay,
|
|
1539
2134
|
options.signal,
|
|
1540
|
-
runManager
|
|
2135
|
+
runManager,
|
|
2136
|
+
new Map()
|
|
1541
2137
|
);
|
|
1542
2138
|
}
|
|
1543
2139
|
}
|
|
@@ -1670,7 +2266,8 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
|
1670
2266
|
this._streamResponseChunksWithReasoning(messages, options, undefined),
|
|
1671
2267
|
this._lc_stream_delay,
|
|
1672
2268
|
options.signal,
|
|
1673
|
-
runManager
|
|
2269
|
+
runManager,
|
|
2270
|
+
new Map()
|
|
1674
2271
|
);
|
|
1675
2272
|
}
|
|
1676
2273
|
|
|
@@ -2131,7 +2728,8 @@ export class ChatXAI extends OriginalChatXAI {
|
|
|
2131
2728
|
super._streamResponseChunks(messages, options, undefined),
|
|
2132
2729
|
this._lc_stream_delay,
|
|
2133
2730
|
options.signal,
|
|
2134
|
-
runManager
|
|
2731
|
+
runManager,
|
|
2732
|
+
new Map()
|
|
2135
2733
|
);
|
|
2136
2734
|
}
|
|
2137
2735
|
}
|