mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🩹 fix: Codex review on context projection (G1 guard, IDOR, recount, summary)
- Guard `currentActive` against a stale window: a model/window switch on the current branch left the live snapshot outranking the projection (G1 didn't fire). Now defers to the projection unless streaming or the window matches. - Scope branch lookups to the authenticated user (`getMessages` filter + injected `userId`) — was loading any conversation by id (IDOR). - Recount messages with no stored `tokenCount` via the tokenizer instead of charging 0, so snapshot-less/imported histories don't under-report. - Fall back (null) for already-summarized branches rather than projecting from the full raw parent chain (the next call would send summary + tail); the client's summary-baseline-aware estimate handles them until a follow-up replays the summary boundary.
This commit is contained in:
parent
e2310c9433
commit
5701a9da9c
3 changed files with 38 additions and 12 deletions
|
|
@ -18,7 +18,7 @@ async function contextProjectionController(req, res) {
|
|||
return;
|
||||
}
|
||||
const projection = await resolveContextProjection(
|
||||
{ getMessages: db.getMessages, getAgent: db.getAgent },
|
||||
{ userId: req.user?.id, getMessages: db.getMessages, getAgent: db.getAgent },
|
||||
params,
|
||||
);
|
||||
res.json(projection ?? null);
|
||||
|
|
|
|||
|
|
@ -237,7 +237,13 @@ export default function useTokenUsage({
|
|||
* one branch's breakdown onto its siblings. */
|
||||
const currentActive =
|
||||
snapshot != null &&
|
||||
(isSubmitting || (snapshot.anchorMessageId != null && branchTotals.containsAnchor));
|
||||
(isSubmitting ||
|
||||
(snapshot.anchorMessageId != null &&
|
||||
branchTotals.containsAnchor &&
|
||||
/** G1: once streaming ends, a model/window switch leaves the live
|
||||
* snapshot's baked window stale — defer to the projection instead of
|
||||
* showing the old window/prune boundary on the current branch. */
|
||||
(resolvedMax == null || snapshot.breakdown.maxContextTokens === resolvedMax)));
|
||||
|
||||
/** Precedence: live/active snapshot → fresh persisted branch snapshot →
|
||||
* server projection (covers G1 stale-window + G2 snapshot-less branches) →
|
||||
|
|
@ -258,8 +264,7 @@ export default function useTokenUsage({
|
|||
if (effective != null) {
|
||||
const breakdown = effective.breakdown;
|
||||
const maxTokens = effective.contextBudget ?? breakdown.maxContextTokens;
|
||||
const instructionTokens =
|
||||
effective.effectiveInstructionTokens ?? breakdown.instructionTokens;
|
||||
const instructionTokens = effective.effectiveInstructionTokens ?? breakdown.instructionTokens;
|
||||
const baseUsed =
|
||||
effective.remainingContextTokens != null
|
||||
? maxTokens - effective.remainingContextTokens
|
||||
|
|
@ -324,5 +329,6 @@ export default function useTokenUsage({
|
|||
branchSnapshot,
|
||||
branchSnapshotFresh,
|
||||
projection,
|
||||
resolvedMax,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface ProjectionMessage {
|
|||
messageId: string;
|
||||
parentMessageId?: string | null;
|
||||
tokenCount?: number;
|
||||
summaryTokenCount?: number;
|
||||
isCreatedByUser?: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
|
@ -19,8 +20,10 @@ interface ProjectionAgent {
|
|||
}
|
||||
|
||||
export interface ContextProjectionDeps {
|
||||
/** Authenticated requester — branch lookups are scoped to this user. */
|
||||
userId?: string;
|
||||
getMessages: (
|
||||
filter: { conversationId: string },
|
||||
filter: { conversationId: string; user?: string },
|
||||
select?: string,
|
||||
) => Promise<ProjectionMessage[]>;
|
||||
getAgent: (filter: { id: string }) => Promise<ProjectionAgent | null>;
|
||||
|
|
@ -87,14 +90,23 @@ export async function resolveContextProjection(
|
|||
params: TContextProjectionRequest,
|
||||
): Promise<TContextUsageEvent | null> {
|
||||
const stored = await deps.getMessages(
|
||||
{ conversationId: params.conversationId },
|
||||
'messageId parentMessageId tokenCount isCreatedByUser text',
|
||||
{ conversationId: params.conversationId, user: deps.userId },
|
||||
'messageId parentMessageId tokenCount summaryTokenCount isCreatedByUser text',
|
||||
);
|
||||
const branch = resolveBranch(stored, params.messageId);
|
||||
if (branch.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A summarized/compacted branch's next call sends the saved summary + the
|
||||
* post-summary tail, NOT this raw parent chain — projecting from the full
|
||||
* history would prune/count the wrong context and omit the summary. Until the
|
||||
* follow-up replays the summary boundary, fall back (null) so the client's
|
||||
* summary-baseline-aware estimate handles these branches. */
|
||||
if (branch.some((message) => (message.summaryTokenCount ?? 0) > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let instructions: string | undefined;
|
||||
let providerValue: string | undefined = params.endpoint;
|
||||
let model = params.model;
|
||||
|
|
@ -112,18 +124,26 @@ export async function resolveContextProjection(
|
|||
return null;
|
||||
}
|
||||
|
||||
const encoding = (model ?? '').toLowerCase().includes('claude') ? 'claude' : 'o200k_base';
|
||||
const tokenCounter = await createTokenCounter(encoding);
|
||||
|
||||
const messages: BaseMessage[] = [];
|
||||
const indexTokenCountMap: Record<string, number> = {};
|
||||
for (let i = 0; i < branch.length; i++) {
|
||||
const message = branch[i];
|
||||
const text = message.text ?? '';
|
||||
messages.push(message.isCreatedByUser === true ? new HumanMessage(text) : new AIMessage(text));
|
||||
indexTokenCountMap[String(i)] = message.tokenCount ?? 0;
|
||||
const lcMessage =
|
||||
message.isCreatedByUser === true ? new HumanMessage(text) : new AIMessage(text);
|
||||
messages.push(lcMessage);
|
||||
/** Recount messages with no stored count (imported / pre-feature) rather
|
||||
* than charging 0 — a real 0 and "unknown" must not collapse, or the
|
||||
* snapshot-less histories this endpoint targets would under-report. */
|
||||
indexTokenCountMap[String(i)] =
|
||||
message.tokenCount != null && message.tokenCount > 0
|
||||
? message.tokenCount
|
||||
: tokenCounter(lcMessage);
|
||||
}
|
||||
|
||||
const encoding = (model ?? '').toLowerCase().includes('claude') ? 'claude' : 'o200k_base';
|
||||
const tokenCounter = await createTokenCounter(encoding);
|
||||
|
||||
return projectAgentContextUsage({
|
||||
agent: {
|
||||
agentId: params.agentId ?? 'projection',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue