mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +00:00
⚡ perf: Build the Memory Message Copy Only When Something Reads It (#15164)
buildMessages formatted every history row twice per turn — a prompt copy and a memory copy — then discarded the entire memory payload unless some row carried fileContext, which is the rare case. The memory copy has exactly two consumers: that payload, and the canonical recount of a row, where it is content-identical to the prompt copy unless the row itself has fileContext. So the prompt copy is now the recount surface for context-free rows, a fileContext row builds its memory copy at recount time, and the full memory payload is assembled in a deferred pass — same formatting, same per-row merge order — only once a row has proven the payload will be kept. The common turn formats each row once instead of twice and no longer allocates a payload it throws away. Also forwards the run's useLegacyContent to formatAgentMessages as the new legacyContent option, inert on the current SDK release: once the SDK change ships, text history is emitted pre-flattened so the per-request legacy projection stops cloning every message and the context meter's identity-based count reuse holds across the projection.
This commit is contained in:
parent
6f3d303985
commit
5a8700643c
2 changed files with 86 additions and 13 deletions
|
|
@ -1693,12 +1693,15 @@ class AgentClient extends BaseClient {
|
|||
let hasFileContext = false;
|
||||
let promptTokenTotal = 0;
|
||||
const encoding = this.getEncoding();
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = formatMessage({
|
||||
message,
|
||||
userName: this.options?.name,
|
||||
assistantName: this.options?.modelLabel,
|
||||
});
|
||||
/**
|
||||
* Rebuilds the memory-side copy of one source row: the same formatting and
|
||||
* per-message merges as the prompt copy, minus the fileContext prepend.
|
||||
* Only materialized when something actually consumes it — the canonical
|
||||
* recount of a fileContext row, or the memory payload once any row proves
|
||||
* to carry fileContext — instead of unconditionally formatting every row
|
||||
* twice per turn.
|
||||
*/
|
||||
const buildMemoryFormattedMessage = (message) => {
|
||||
const memoryFormattedMessage = formatMessage({
|
||||
message,
|
||||
userName: this.options?.name,
|
||||
|
|
@ -1706,9 +1709,28 @@ class AgentClient extends BaseClient {
|
|||
});
|
||||
const sourceMessageId = message.messageId ?? message.id;
|
||||
if (typeof sourceMessageId === 'string' && sourceMessageId.length > 0) {
|
||||
formattedMessage.messageId = sourceMessageId;
|
||||
memoryFormattedMessage.messageId = sourceMessageId;
|
||||
}
|
||||
if (Array.isArray(message.quotes) && message.quotes.length > 0) {
|
||||
prependQuotes(memoryFormattedMessage, message.quotes);
|
||||
}
|
||||
const turnFiles = this.message_file_map?.[message.messageId] ?? message.files;
|
||||
applyAttachmentOnlyText(memoryFormattedMessage, turnFiles);
|
||||
return memoryFormattedMessage;
|
||||
};
|
||||
/** Memory copies built for canonical recounts, reused by the memory payload pass. */
|
||||
const memoryFormattedMessages = [];
|
||||
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = formatMessage({
|
||||
message,
|
||||
userName: this.options?.name,
|
||||
assistantName: this.options?.modelLabel,
|
||||
});
|
||||
const sourceMessageId = message.messageId ?? message.id;
|
||||
if (typeof sourceMessageId === 'string' && sourceMessageId.length > 0) {
|
||||
formattedMessage.messageId = sourceMessageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind file context to the message it belongs to. Historical attachments
|
||||
|
|
@ -1732,7 +1754,6 @@ class AgentClient extends BaseClient {
|
|||
*/
|
||||
if (Array.isArray(message.quotes) && message.quotes.length > 0) {
|
||||
prependQuotes(formattedMessage, message.quotes);
|
||||
prependQuotes(memoryFormattedMessage, message.quotes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1746,9 +1767,6 @@ class AgentClient extends BaseClient {
|
|||
*/
|
||||
const turnFiles = this.message_file_map?.[message.messageId] ?? message.files;
|
||||
applyAttachmentOnlyText(formattedMessage, turnFiles);
|
||||
applyAttachmentOnlyText(memoryFormattedMessage, turnFiles);
|
||||
|
||||
memoryPayload.push(memoryFormattedMessage);
|
||||
|
||||
const dbTokenCount = Number(orderedMessages[i].tokenCount);
|
||||
const hasDbTokenCount = Number.isFinite(dbTokenCount) && dbTokenCount > 0;
|
||||
|
|
@ -1766,7 +1784,15 @@ class AgentClient extends BaseClient {
|
|||
|
||||
let canonicalTokenCount = hasDbTokenCount ? dbTokenCount : 0;
|
||||
if (needsCanonicalTokenCount) {
|
||||
canonicalTokenCount = countFormattedMessageTokens(memoryFormattedMessage, encoding);
|
||||
/** Without fileContext the memory copy is content-identical to the
|
||||
* prompt copy, so the prompt copy is the counting surface; with it,
|
||||
* the canonical count must exclude the prepended context. */
|
||||
let countSurface = formattedMessage;
|
||||
if (message.fileContext) {
|
||||
memoryFormattedMessages[i] = buildMemoryFormattedMessage(message);
|
||||
countSurface = memoryFormattedMessages[i];
|
||||
}
|
||||
canonicalTokenCount = countFormattedMessageTokens(countSurface, encoding);
|
||||
}
|
||||
|
||||
const promptMessageTokenCount = message.fileContext
|
||||
|
|
@ -1917,6 +1943,13 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (hasFileContext) {
|
||||
for (let i = 0; i < orderedMessages.length; i++) {
|
||||
memoryPayload.push(
|
||||
memoryFormattedMessages[i] ?? buildMemoryFormattedMessage(orderedMessages[i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
this.memoryPayload = hasFileContext ? memoryPayload : null;
|
||||
messages = orderedMessages;
|
||||
promptTokens = promptTokenTotal;
|
||||
|
|
@ -3288,13 +3321,15 @@ class AgentClient extends BaseClient {
|
|||
manualSkillPrimes,
|
||||
alwaysApplySkillPrimes,
|
||||
});
|
||||
const useLegacyContent = this.options.agent?.useLegacyContent === true;
|
||||
const formatOptions =
|
||||
needsReasoningContentFormat || freshSkillPrimeNames.size > 0
|
||||
needsReasoningContentFormat || freshSkillPrimeNames.size > 0 || useLegacyContent
|
||||
? {
|
||||
...(needsReasoningContentFormat ? { preserveReasoningContent: true } : {}),
|
||||
...(freshSkillPrimeNames.size > 0
|
||||
? { skipSkillBodyNames: freshSkillPrimeNames }
|
||||
: {}),
|
||||
...(useLegacyContent ? { legacyContent: true } : {}),
|
||||
}
|
||||
: undefined;
|
||||
let {
|
||||
|
|
|
|||
|
|
@ -3783,6 +3783,44 @@ describe('AgentClient - titleConvo', () => {
|
|||
expect(client.memoryPayload[0].content).toBe('What is written here?');
|
||||
});
|
||||
|
||||
it('recounts a quote-bearing history row from quote-merged content and keeps the memory payload unbuilt without file context', async () => {
|
||||
const { countFormattedMessageTokens } = require('@librechat/api');
|
||||
countFormattedMessageTokens.mockImplementation(({ content }) => {
|
||||
const text = Array.isArray(content)
|
||||
? content.map((part) => part.text ?? part[ContentTypes.TEXT] ?? '').join('\n')
|
||||
: String(content ?? '');
|
||||
return text.includes('quoted excerpt') ? 77 : 11;
|
||||
});
|
||||
|
||||
const result = await client.buildMessages(
|
||||
[
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Discuss this.',
|
||||
isCreatedByUser: true,
|
||||
tokenCount: 5,
|
||||
quotes: ['quoted excerpt'],
|
||||
},
|
||||
{
|
||||
messageId: 'msg-2',
|
||||
parentMessageId: 'msg-1',
|
||||
sender: 'Assistant',
|
||||
text: 'Sure.',
|
||||
isCreatedByUser: false,
|
||||
tokenCount: 3,
|
||||
},
|
||||
],
|
||||
'msg-2',
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result.tokenCountMap['msg-1']).toBe(77);
|
||||
expect(result.tokenCountMap['msg-2']).toBe(3);
|
||||
expect(client.memoryPayload).toBeNull();
|
||||
});
|
||||
|
||||
it('does not duplicate a file that is both request context and scoped context', async () => {
|
||||
const sharedFile = makeTextFile('shared-file', 'shared.txt', 'Shared duplicate context');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue