mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 03:43:03 +00:00
🪙 fix: Count quote tokens on message edit so context stays accurate
The message-edit route recomputed a user message's tokenCount from the edited `text` alone, ignoring its persisted `quotes`. But the send path re-prepends those quotes into the prompt on every turn (mergeQuotedText), so after editing a quoted message the stored tokenCount under-reported by the whole quote block, skewing the context gauge and any other tokenCount consumer. The full-recount path now fetches the message's quotes and counts the merged text+quotes via a new `mergeQuotedTextForCount` helper in packages/api (mirrors the send path), so the stored count stays authoritative. The incremental content-part path is left as-is: it deltas only the edited part and preserves the rest of the count (incl. the quote contribution), and applies to content-array messages rather than text+quotes user turns. Deferred follow-up from #13953.
This commit is contained in:
parent
5706e414fd
commit
ec04a62b42
3 changed files with 72 additions and 1 deletions
|
|
@ -7,6 +7,7 @@ const {
|
|||
countTokens,
|
||||
sendFeedbackScore,
|
||||
traceIdForMessage,
|
||||
mergeQuotedTextForCount,
|
||||
} = require('@librechat/api');
|
||||
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
|
||||
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
|
||||
|
|
@ -329,7 +330,22 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) =
|
|||
const { text, index, model } = req.body;
|
||||
|
||||
if (index === undefined) {
|
||||
const tokenCount = await countTokens(text, model);
|
||||
/** A user turn's persisted `quotes` are re-prepended into the prompt on
|
||||
* every send, but this edit only changes `text`. Count the merged
|
||||
* text+quotes so the stored `tokenCount` stays authoritative (matching the
|
||||
* send path); a plain text-only count under-reports by the quote block. */
|
||||
const existing = (
|
||||
await db.getMessages(
|
||||
{ conversationId, messageId, user: req.user.id },
|
||||
'quotes isCreatedByUser',
|
||||
)
|
||||
)?.[0];
|
||||
const textToCount = mergeQuotedTextForCount(
|
||||
text,
|
||||
existing?.quotes,
|
||||
existing?.isCreatedByUser === true,
|
||||
);
|
||||
const tokenCount = await countTokens(textToCount, model);
|
||||
const result = await db.updateMessage(req?.user?.id, { messageId, text, tokenCount });
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
|
|
|||
36
packages/api/src/utils/quotes.spec.ts
Normal file
36
packages/api/src/utils/quotes.spec.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { mergeQuotedTextForCount, QUOTE_MAX_COUNT } from './quotes';
|
||||
|
||||
describe('mergeQuotedTextForCount', () => {
|
||||
it('returns text unchanged for non-user messages', () => {
|
||||
expect(mergeQuotedTextForCount('hi', ['quote'], false)).toBe('hi');
|
||||
});
|
||||
|
||||
it('returns text unchanged when there are no usable quotes', () => {
|
||||
expect(mergeQuotedTextForCount('hi', undefined, true)).toBe('hi');
|
||||
expect(mergeQuotedTextForCount('hi', [], true)).toBe('hi');
|
||||
expect(mergeQuotedTextForCount('hi', ['', ' '], true)).toBe('hi');
|
||||
expect(mergeQuotedTextForCount('hi', 'not-an-array', true)).toBe('hi');
|
||||
});
|
||||
|
||||
it('prepends merged quotes for a user message with quotes', () => {
|
||||
const out = mergeQuotedTextForCount('my question', ['excerpt one'], true);
|
||||
expect(out).toBe('> excerpt one\n\nmy question');
|
||||
/** Longer than the bare text, so counting `out` exceeds counting text alone —
|
||||
* the under-report this fix addresses. */
|
||||
expect(out.length).toBeGreaterThan('my question'.length);
|
||||
});
|
||||
|
||||
it('normalizes quotes (drops non-strings and empties, trims) before merging', () => {
|
||||
const out = mergeQuotedTextForCount('q', ['keep', 42, '', ' trim '], true);
|
||||
expect(out).toContain('> keep');
|
||||
expect(out).toContain('> trim');
|
||||
expect(out).not.toContain('42');
|
||||
});
|
||||
|
||||
it('caps the number of merged excerpts at QUOTE_MAX_COUNT', () => {
|
||||
const many = Array.from({ length: QUOTE_MAX_COUNT + 5 }, (_, i) => `q${i}`);
|
||||
const out = mergeQuotedTextForCount('body', many, true);
|
||||
const quoteBlocks = out.split('\n\n').filter((block) => block.startsWith('>'));
|
||||
expect(quoteBlocks).toHaveLength(QUOTE_MAX_COUNT);
|
||||
});
|
||||
});
|
||||
|
|
@ -67,3 +67,22 @@ export function mergeQuotedText(text: string, quotes: string[]): string {
|
|||
const body = text ?? '';
|
||||
return body.length > 0 ? `${block}\n\n${body}` : block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the prompt text to tokenize for a (possibly quoted) user turn,
|
||||
* mirroring the send path: a user message's persisted `quotes` are prepended
|
||||
* into the prompt on every turn, so an edit that only changes `text` must count
|
||||
* the merged text+quotes to keep the stored `tokenCount` authoritative. Returns
|
||||
* `text` unchanged for non-user messages or when there are no usable quotes.
|
||||
*/
|
||||
export function mergeQuotedTextForCount(
|
||||
text: string,
|
||||
rawQuotes: unknown,
|
||||
isCreatedByUser: boolean,
|
||||
): string {
|
||||
if (!isCreatedByUser) {
|
||||
return text;
|
||||
}
|
||||
const quotes = getReferencedQuotes(rawQuotes);
|
||||
return quotes ? mergeQuotedText(text, quotes) : text;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue