mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-09 15:58:14 +00:00
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.
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
/** Max characters kept per quoted excerpt (defense-in-depth over the client cap). */
|
|
export const QUOTE_MAX_LENGTH = 1500;
|
|
|
|
/** Max number of quoted excerpts merged into a single user turn. */
|
|
export const QUOTE_MAX_COUNT = 10;
|
|
|
|
/**
|
|
* Normalizes the `quotes` field off a request body into a clean string array.
|
|
* Trims, drops empties/non-strings, caps each excerpt length and the overall
|
|
* count. Returns `null` when there is nothing usable so callers can skip the
|
|
* merge entirely.
|
|
*/
|
|
export function getReferencedQuotes(raw: unknown): string[] | null {
|
|
if (!Array.isArray(raw)) {
|
|
return null;
|
|
}
|
|
|
|
const quotes: string[] = [];
|
|
for (const item of raw) {
|
|
if (typeof item !== 'string') {
|
|
continue;
|
|
}
|
|
const trimmed = item.trim();
|
|
if (trimmed.length === 0) {
|
|
continue;
|
|
}
|
|
quotes.push(trimmed.slice(0, QUOTE_MAX_LENGTH));
|
|
if (quotes.length >= QUOTE_MAX_COUNT) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return quotes.length > 0 ? quotes : null;
|
|
}
|
|
|
|
/** Formats one excerpt as a Markdown blockquote, prefixing every line. */
|
|
function toBlockquote(quote: string): string {
|
|
return quote
|
|
.split('\n')
|
|
.map((line) => (line.length > 0 ? `> ${line}` : '>'))
|
|
.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Renders quoted excerpts as Markdown blockquote blocks, one block per excerpt
|
|
* separated by a blank line. Provider-agnostic (no bespoke tags). Returns an
|
|
* empty string when there are no quotes.
|
|
*/
|
|
export function formatQuotesAsMarkdown(quotes: string[]): string {
|
|
return quotes.map(toBlockquote).join('\n\n');
|
|
}
|
|
|
|
/**
|
|
* Merges quoted excerpts into a body of text as Markdown blockquotes, prepended
|
|
* before the text so the excerpts read as part of the user turn. Returns the
|
|
* original text unchanged when there are no quotes.
|
|
*/
|
|
export function mergeQuotedText(text: string, quotes: string[]): string {
|
|
if (quotes.length === 0) {
|
|
return text;
|
|
}
|
|
|
|
const block = formatQuotesAsMarkdown(quotes);
|
|
if (block.length === 0) {
|
|
return text;
|
|
}
|
|
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;
|
|
}
|