What happened?
When summarization compacts an Anthropic conversation, the compaction request can include citations on a replayed assistant message without the document block they point at. Anthropic rejects the request:
400 invalid_request_error
messages.5.content.0.citations.0: Invalid document index in document citation.
The steps that lead there:
- LibreChat sends attached files to Anthropic as
document blocks with citations: { enabled: true } (packages/api/src/files/encode/document.ts), on the latest user message. The model's answer cites them by document_index.
- The citations are stored on the assistant message. On every replay,
_convertMessagesToAnthropicPayload copies them back onto the text block (message_inputs.ts) and doesn't check that the cited documents are in the same request.
- At compaction,
splitAtRecencyBoundary splits the history into older messages to summarize (messagesToRefine) and a recent tail to keep (node.ts). summarizeWithCacheHit sends only the older messages plus the instruction, [...cachedHistory, new HumanMessage(instruction)] (node.ts).
- If the cited assistant message lands in the summarized part and the user message with the documents lands in the kept tail, the request has citations and zero documents. Anthropic rejects it and the summary is never written.
I couldn't find anything in src/summarization/ or src/messages/ that strips citations or brings the documents along.
The compaction request should be valid on its own. That means either dropping citations from replayed text blocks whose documents aren't in the request (the text is still worth summarizing), or sending the cited document blocks along with the history.
Version Information
@librechat/agents 3.9.1, as used by LibreChat v0.8.8-rc4. Also reproduced on main at 64177c70.
- Provider: Anthropic (
claude-opus-4-7) with prompt caching on.
Steps to Reproduce
- Use an agent on an Anthropic model with summarization enabled.
- Attach a document (PDF or text) and ask about it, so the answer comes back with citations.
- Keep chatting until summarization triggers, with the attachment still on the latest user message.
- The compaction request fails with
Invalid document index in document citation.
The unit test below reproduces it without a live model. On main it fails with:
✕ does not send citations whose documents were left out of the compaction request
+ "messages.1.content.0.citations.0: document_index 0, request has 0 document(s)"
The first test is a control: it shows the conversation is a valid Anthropic request before compaction. The second test runs the real createSummarizeNode with a model stub that records what it is sent. It then checks every citation in the compaction request against the documents in that request.
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import type { RunnableConfig } from '@langchain/core/runnables';
import type { BaseMessage } from '@langchain/core/messages';
import type * as t from '@/types';
import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
import { createSummarizeNode } from '@/summarization/node';
import { AgentContext } from '@/agents/AgentContext';
import { Providers } from '@/common';
import * as providers from '@/llm/providers';
import * as eventUtils from '@/utils/events';
/**
* Anthropic rejects a request whose text blocks carry a document citation
* without the cited document in the same request:
* 400 invalid_request_error
* "messages.N.content.0.citations.0: Invalid document index in document citation."
*/
function invalidCitations(messages: BaseMessage[]): string[] {
const payload = _convertMessagesToAnthropicPayload(messages);
let documentCount = 0;
for (const message of payload.messages) {
if (Array.isArray(message.content)) {
documentCount += message.content.filter(
(block) => block.type === 'document'
).length;
}
}
const problems: string[] = [];
payload.messages.forEach((message, messageIndex) => {
if (!Array.isArray(message.content)) {
return;
}
message.content.forEach((block, blockIndex) => {
const citations = (block as { citations?: unknown }).citations;
if (!Array.isArray(citations)) {
return;
}
citations.forEach((citation, citationIndex) => {
const documentIndex = (citation as { document_index?: number })
.document_index;
if (documentIndex != null && documentIndex >= documentCount) {
problems.push(
`messages.${messageIndex}.content.${blockIndex}.citations.${citationIndex}: ` +
`document_index ${documentIndex}, request has ${documentCount} document(s)`
);
}
});
});
});
return problems;
}
/** Turn 1 is answered with citations into a document that the latest user
* message carries, which is where LibreChat attaches a conversation's files. */
function conversation(): BaseMessage[] {
return [
new HumanMessage('Can you read the attached spec?'),
new AIMessage({
content: [
{
type: 'text',
text: 'Yes, the spec requires BankID sign-in.',
citations: [
{
type: 'char_location',
cited_text: 'Members sign in with BankID.',
document_index: 0,
document_title: null,
start_char_index: 0,
end_char_index: 28,
},
],
},
],
}),
new HumanMessage({
content: [
{ type: 'text', text: 'Now cross-reference it against the backlog.' },
{
type: 'document',
source: {
type: 'text',
media_type: 'text/plain',
data: 'Members sign in with BankID.',
},
citations: { enabled: true },
context: 'File: "spec.txt"',
},
],
}),
];
}
describe('summarization with Anthropic document citations', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('the conversation itself is a valid Anthropic request', () => {
expect(invalidCitations(conversation())).toEqual([]);
});
it('does not send citations whose documents were left out of the compaction request', async () => {
jest
.spyOn(eventUtils, 'safeDispatchCustomEvent')
.mockImplementation((async () => undefined) as never);
const calls: BaseMessage[][] = [];
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
class {
invoke(messages: BaseMessage[]): Promise<{ content: string }> {
calls.push(messages);
return Promise.resolve({ content: 'summary' });
}
} as never
);
const agentContext = AgentContext.fromConfig({
agentId: 'agent_0',
provider: Providers.ANTHROPIC,
instructions: 'Test agent',
summarizationEnabled: true,
summarizationConfig: { retainRecent: { turns: 1 } },
} as t.AgentInputs);
let step = 0;
const node = createSummarizeNode({
agentContext,
graph: {
contentData: [],
contentIndexMap: new Map(),
config: {} as RunnableConfig,
runId: 'run_1',
isMultiAgent: false,
dispatchRunStep: async (): Promise<void> => {},
dispatchRunStepCompleted: async (): Promise<void> => {},
} as never,
generateStepId: (): [string, number] => [`step_${step++}`, 0],
});
await node(
{
messages: conversation(),
summarizationRequest: {
remainingContextTokens: 0,
agentId: 'agent_0',
},
},
{} as RunnableConfig
);
expect(calls).toHaveLength(1);
expect(invalidCitations(calls[0])).toEqual([]);
});
});
Example error response
{"type":"error","error":{"type":"invalid_request_error","message":"messages.5.content.0.citations.0: Invalid document index in document citation."}}
What happened?
When summarization compacts an Anthropic conversation, the compaction request can include
citationson a replayed assistant message without thedocumentblock they point at. Anthropic rejects the request:The steps that lead there:
documentblocks withcitations: { enabled: true }(packages/api/src/files/encode/document.ts), on the latest user message. The model's answer cites them bydocument_index._convertMessagesToAnthropicPayloadcopies them back onto the text block (message_inputs.ts) and doesn't check that the cited documents are in the same request.splitAtRecencyBoundarysplits the history into older messages to summarize (messagesToRefine) and a recent tail to keep (node.ts).summarizeWithCacheHitsends only the older messages plus the instruction,[...cachedHistory, new HumanMessage(instruction)](node.ts).I couldn't find anything in
src/summarization/orsrc/messages/that strips citations or brings the documents along.The compaction request should be valid on its own. That means either dropping
citationsfrom replayed text blocks whose documents aren't in the request (the text is still worth summarizing), or sending the citeddocumentblocks along with the history.Version Information
@librechat/agents3.9.1, as used by LibreChat v0.8.8-rc4. Also reproduced onmainat64177c70.claude-opus-4-7) with prompt caching on.Steps to Reproduce
Invalid document index in document citation.The unit test below reproduces it without a live model. On
mainit fails with:The first test is a control: it shows the conversation is a valid Anthropic request before compaction. The second test runs the real createSummarizeNode with a model stub that records what it is sent. It then checks every citation in the compaction request against the documents in that request.
Example error response
{"type":"error","error":{"type":"invalid_request_error","message":"messages.5.content.0.citations.0: Invalid document index in document citation."}}