Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
3d8501d
fix(agents): keep dynamic tool context after additional instructions
Shabonasar Sep 25, 2026
0c0ecd2
fix(agents): keep per-turn instructions after cacheable prompts
lia-librechat Sep 27, 2026
241795b
Merge branch 'dev' into fix/prompt-prefix-cache-order
Shabonasar Oct 1, 2026
c202d8a
Merge remote-tracking branch 'upstream/dev' into fix/prompt-prefix-ca…
Shabonasar Oct 14, 2026
627541e
fix(agents): anchor prompt time to the conversation start and announc…
Shabonasar Oct 14, 2026
cb487f8
fix(client): let the server resolve promptPrefix variables for agents…
Shabonasar Oct 14, 2026
e05c694
Merge remote-tracking branch 'upstream/dev' into fix/prompt-prefix-ca…
Shabonasar Oct 14, 2026
07ab11d
fix(agents): keep the cacheable prompt prefix stable across the turns…
Shabonasar Oct 15, 2026
4c59edb
Merge remote-tracking branch 'upstream/dev' into fix/prompt-prefix-ca…
Shabonasar Oct 15, 2026
c923e5f
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
e06c84a
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
75f2c84
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
ca8f32b
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
f5e2acc
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
b052ac1
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
492ae5f
Merge remote-tracking branch 'upstream/dev' into fix/prompt-prefix-ca…
Shabonasar Oct 16, 2026
f2b7365
Merge branch 'feat/stable-tool-groups' into feat/stable-builtin-tools
Shabonasar Oct 16, 2026
512c7cc
Merge branch 'fix/prompt-prefix-cache-order' into feat/structured-sys…
Shabonasar Oct 16, 2026
741f8a6
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 16, 2026
d776e8a
Merge branch 'feat/structured-system-parts-v2' into feat/turn-context
Shabonasar Oct 16, 2026
ec04e41
Merge branch 'feat/turn-context' into feat/stable-tool-groups
Shabonasar Oct 16, 2026
43297f1
Merge remote-tracking branch 'upstream/dev' into fix/prompt-prefix-ca…
Shabonasar Oct 16, 2026
3fea9ee
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
38c3ecf
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
75a8d6a
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
08dd7b4
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
86b9fca
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 16, 2026
4969e6b
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
11c2f36
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
2c06d74
fix(agents): keep dynamic tool context after additional instructions
Shabonasar Sep 25, 2026
347fdf2
fix(agents): keep per-turn instructions after cacheable prompts
lia-librechat Sep 27, 2026
71d1e51
fix(agents): anchor prompt time to the conversation start and announc…
Shabonasar Oct 14, 2026
d479543
fix(client): let the server resolve promptPrefix variables for agents…
Shabonasar Oct 14, 2026
067e44a
fix(agents): keep the cacheable prompt prefix stable across the turns…
Shabonasar Oct 15, 2026
e1155bd
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
81ead72
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
b3ac71f
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
8ed0653
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
d2fcf02
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
70da8d2
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
0c71322
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 18, 2026
cb6536c
fix(agents): anchor the memory of handoff targets with the turn context
Shabonasar Oct 19, 2026
e70c697
test(agents): cover the anchored memory of a handoff target in the ca…
Shabonasar Oct 19, 2026
b300e8f
fix(agents): keep dynamic tool context after additional instructions
Shabonasar Sep 25, 2026
2cb24e2
fix(agents): keep per-turn instructions after cacheable prompts
lia-librechat Sep 27, 2026
9d6a06f
fix(agents): anchor prompt time to the conversation start and announc…
Shabonasar Oct 14, 2026
639965f
fix(client): let the server resolve promptPrefix variables for agents…
Shabonasar Oct 14, 2026
62237f0
fix(agents): keep the cacheable prompt prefix stable across the turns…
Shabonasar Oct 15, 2026
fcfbf33
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
f57b6ec
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
0c81cb0
fix(agents): anchor the memory of handoff targets with the turn context
Shabonasar Oct 19, 2026
482fbfe
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
3c63ab0
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
485610c
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
bd2f3eb
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
ae892db
test(agents): cover the anchored memory of a handoff target in the ca…
Shabonasar Oct 19, 2026
36733ac
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 20, 2026
922bb0a
fix(agents): keep dynamic tool context after additional instructions
Shabonasar Sep 25, 2026
0b8347a
fix(agents): keep per-turn instructions after cacheable prompts
lia-librechat Sep 27, 2026
b5c354e
fix(agents): anchor prompt time to the conversation start and announc…
Shabonasar Oct 14, 2026
ce9a4ad
fix(client): let the server resolve promptPrefix variables for agents…
Shabonasar Oct 14, 2026
dba0b8a
fix(agents): keep the cacheable prompt prefix stable across the turns…
Shabonasar Oct 15, 2026
f4bf535
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
8db9dd0
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
1eff2b6
fix(agents): anchor the memory of handoff targets with the turn context
Shabonasar Oct 19, 2026
934e5a4
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
8e5ff70
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
d97d6ec
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
ee0002a
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
7184bfc
test(agents): cover the anchored memory of a handoff target in the ca…
Shabonasar Oct 19, 2026
04390e4
fix(agents): leave MCP servers hidden from the user out of the anchor…
Shabonasar Oct 22, 2026
5593377
fix(agents): re-anchor when an anchored MCP server becomes hidden fro…
Shabonasar Oct 22, 2026
9353df7
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 22, 2026
f94337f
fix(agents): keep dynamic tool context after additional instructions
Shabonasar Sep 25, 2026
6d13279
fix(agents): keep per-turn instructions after cacheable prompts
lia-librechat Sep 27, 2026
95f1ede
fix(agents): anchor prompt time to the conversation start and announc…
Shabonasar Oct 14, 2026
aa2ad5c
fix(client): let the server resolve promptPrefix variables for agents…
Shabonasar Oct 14, 2026
8d0438f
fix(agents): keep the cacheable prompt prefix stable across the turns…
Shabonasar Oct 15, 2026
7e6ba39
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
ea5cd8c
refactor(agents): assemble the system prompt from structured, kind-cl…
Shabonasar Oct 15, 2026
59cbdfe
fix(agents): anchor the memory of handoff targets with the turn context
Shabonasar Oct 19, 2026
9e25fe4
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
9239552
fix(agents): list attached workspace attachments in upload order so t…
Shabonasar Oct 25, 2026
2731bb8
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
d361a58
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
39e7663
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
f06ccdd
test(agents): cover the anchored memory of a handoff target in the ca…
Shabonasar Oct 19, 2026
8a23302
fix(agents): leave MCP servers hidden from the user out of the anchor…
Shabonasar Oct 22, 2026
b569ecd
fix(agents): re-anchor when an anchored MCP server becomes hidden fro…
Shabonasar Oct 22, 2026
21495fb
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 25, 2026
a7041bf
fix(agents): skip the empty-memory notice for agents that cannot use …
Shabonasar Oct 26, 2026
59b05df
feat(agents): plan the turn context at send time and freeze the syste…
Shabonasar Oct 15, 2026
715101c
fix(agents): anchor the memory of handoff targets with the turn context
Shabonasar Oct 19, 2026
f0e4641
fix(agents): list attached workspace attachments in upload order so t…
Shabonasar Oct 25, 2026
65449d1
fix(agents): anchor no empty-memory notice for an agent that cannot u…
Shabonasar Oct 26, 2026
a8ba56c
feat(agents): keep the tool set fixed per segment with tool groups an…
Shabonasar Oct 14, 2026
4fd6c67
feat(agents): route built-in capabilities through tool groups and dec…
Shabonasar Oct 14, 2026
fdd85ae
feat(agents): store provider output items beside a response and repla…
Shabonasar Oct 15, 2026
f00627f
feat(agents): pass the stable context as the SDK's conversation block
Shabonasar Oct 15, 2026
6038b00
test(agents): cover the anchored memory of a handoff target in the ca…
Shabonasar Oct 19, 2026
297fdb2
fix(agents): leave MCP servers hidden from the user out of the anchor…
Shabonasar Oct 22, 2026
a411a6c
fix(agents): re-anchor when an anchored MCP server becomes hidden fro…
Shabonasar Oct 22, 2026
6ec9f9e
fix(agents): frame the per-turn tail the SDK relocates as system-supp…
Shabonasar Oct 26, 2026
c9788d6
test(agents): cover the Bedrock request with the tail behind the hist…
Shabonasar Oct 26, 2026
15ed355
Merge branch 'feat/stable-builtin-tools' into feat/provider-output-items
Shabonasar Oct 26, 2026
c7b5aef
chore: join the shared history of the prompt-cache series
Shabonasar Oct 28, 2026
58bc38b
chore: join the shared history of the prompt-cache series (#17146)
Shabonasar Oct 28, 2026
5b0c5a3
chore: join the shared history of the prompt-cache series (#17147)
Shabonasar Oct 28, 2026
2e8e145
feat(agents): store and replay the provider output items of Responses…
Shabonasar Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,10 @@ class BaseClient {
}

const messages =
(await db.getMessages({ conversationId, user: this.user }, '+privateTextTokens')) ?? [];
(await db.getMessages(
{ conversationId, user: this.user },
'+privateTextTokens +providerItems',
)) ?? [];
/** A client that reads beyond the walk below (which stops at a checkpoint
* summary) receives every row here; the rest keep nothing. */
this.onHistoryLoaded?.(messages);
Expand Down
5 changes: 4 additions & 1 deletion api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1600,7 +1600,10 @@ describe('BaseClient', () => {

const chatMessages = await TestClient.loadHistory(conversationId, '1');

expect(getMessages).toHaveBeenCalledWith({ conversationId, user }, '+privateTextTokens');
expect(getMessages).toHaveBeenCalledWith(
{ conversationId, user },
'+privateTextTokens +providerItems',
);
expect(chatMessages).toHaveLength(1);
expect(chatMessages[0].text).toBe('Hello');
});
Expand Down
44 changes: 44 additions & 0 deletions api/server/controllers/agents/__tests__/callbacks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1898,3 +1898,47 @@ describe('isStreamWritable', () => {
expect(isStreamWritable({ headersSent: true, writableEnded: false }, null)).toBe(true);
});
});

describe('provider output items', () => {
it('stores the items on the tool call parts without emitting them', async () => {
const { GenerationJobManager, sendEvent } = require('@librechat/api');
const { GraphEvents, createContentAggregator } = jest.requireActual('@librechat/agents');
const { getDefaultHandlers } = require('../callbacks');
const { contentParts, stepMap, aggregateContent } = createContentAggregator();
const handlers = getDefaultHandlers({
res: { write: jest.fn() },
aggregateContent,
contentParts,
stepMap,
toolEndCallback: jest.fn(),
collectedUsage: [],
streamId: 'conversation-1',
jobCreatedAt: 1234,
});
await handlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, {
id: 'step-1',
index: 0,
stepDetails: {
type: 'tool_calls',
tool_calls: [{ id: 'call-1', name: 'lookup', args: '{}' }],
},
});
jest.clearAllMocks();

const providerItems = {
origin: 'origin-digest',
items: [
{ id: 'rs_1', type: 'reasoning', summary: [], encrypted_content: 'opaque' },
{ id: 'fc_1', type: 'function_call', status: 'completed', call_id: 'call-1' },
],
};
await handlers[GraphEvents.ON_PROVIDER_OUTPUT_ITEMS].handle(
GraphEvents.ON_PROVIDER_OUTPUT_ITEMS,
{ tool_calls: [{ id: 'call-1', providerItems }] },
);

expect(contentParts[0].tool_call.providerItems).toEqual(providerItems);
expect(GenerationJobManager.emitChunk).not.toHaveBeenCalled();
expect(sendEvent).not.toHaveBeenCalled();
});
});
13 changes: 13 additions & 0 deletions api/server/controllers/agents/callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,19 @@ function getDefaultHandlers({
}
},
},
/** Absent from an SDK that predates provider output items. */
...(GraphEvents.ON_PROVIDER_OUTPUT_ITEMS != null && {
[GraphEvents.ON_PROVIDER_OUTPUT_ITEMS]: {
/**
* Registry-only: the provider output items of a finished tool round (OpenAI Responses
* ids, status, reasoning). Stored on the tool call parts so the next turn replays the
* round as it was sent; they carry encrypted reasoning and are never forwarded.
* @param {string} event - The event name.
* @param {import('@librechat/agents').ProviderOutputItemsEvent} data - The event data.
*/
handle: (event, data) => aggregateContent({ event, data }),
},
}),
[GraphEvents.ON_MESSAGE_DELTA]: {
/**
* Handle ON_MESSAGE_DELTA event.
Expand Down
17 changes: 10 additions & 7 deletions api/server/controllers/agents/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const {
agentHasInlineMemoryTools,
loadAgent: loadAgentFn,
createMultiAgentMapper,
attachProviderItems,
filterMalformedContentParts,
countFormattedMessageTokens,
prependFileContext,
Expand Down Expand Up @@ -2372,13 +2373,15 @@ class AgentClient extends BaseClient {
this.compactionSemanticIndexSnapshot = undefined;
}
/** Always pass mapMethod; getMessagesForConversation applies it only to messages with addedConvo flag */
const orderedMessages = this.constructor.getMessagesForConversation({
messages,
parentMessageId,
summary: this.shouldSummarize,
mapMethod: createMultiAgentMapper(this.options.agent, this.agentConfigs),
mapCondition: (message) => message.addedConvo === true,
});
const orderedMessages = this.constructor
.getMessagesForConversation({
messages,
parentMessageId,
summary: this.shouldSummarize,
mapMethod: createMultiAgentMapper(this.options.agent, this.agentConfigs),
mapCondition: (message) => message.addedConvo === true,
})
.map(attachProviderItems);
/**
* Answers the user gave to earlier `ask_user_question` calls. Read from the
* rows before `messages` is narrowed to `orderedMessages`; when those rows
Expand Down
84 changes: 84 additions & 0 deletions api/server/controllers/agents/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7931,6 +7931,90 @@ describe('AgentClient - titleConvo', () => {
limitType: 'bytes',
});
});

describe('provider items', () => {
const toolRound = (extras = {}) => [
{
messageId: 'msg-1',
parentMessageId: null,
sender: 'User',
text: 'Look it up',
isCreatedByUser: true,
},
{
messageId: 'msg-2',
parentMessageId: 'msg-1',
sender: 'Assistant',
text: '',
isCreatedByUser: false,
content: [
{ type: ContentTypes.TEXT, text: 'Looking it up.' },
{
type: ContentTypes.TOOL_CALL,
tool_call: { id: 'call_1', name: 'lookup', args: '{}', output: 'ok', progress: 1 },
},
],
...extras,
},
{
messageId: 'msg-3',
parentMessageId: 'msg-2',
sender: 'User',
text: 'Thanks',
isCreatedByUser: true,
},
];

it('builds the prompt of a tool round as upstream when no provider items are stored', async () => {
const result = await client.buildMessages(toolRound(), 'msg-3', {});

expect(result.prompt).toMatchInlineSnapshot(`
[
{
"content": "Look it up",
"messageId": "msg-1",
"role": "user",
},
{
"content": [
{
"text": "Looking it up.",
"type": "text",
},
{
"tool_call": {
"args": "{}",
"id": "call_1",
"name": "lookup",
"output": "ok",
"progress": 1,
},
"type": "tool_call",
},
],
"messageId": "msg-2",
"role": "assistant",
},
{
"content": "Thanks",
"messageId": "msg-3",
"role": "user",
},
]
`);
});

it('puts the stored provider items back on their tool calls', async () => {
const providerItems = { origin: 'digest', items: [{ id: 'rs_1', type: 'reasoning' }] };
const history = toolRound({ providerItems: { call_1: providerItems } });

const result = await client.buildMessages(history, 'msg-3', {});

const toolCall = result.prompt[1].content.find((part) => part.tool_call)?.tool_call;
expect(toolCall.providerItems).toEqual(providerItems);
expect(history[1].content[1].tool_call).not.toHaveProperty('providerItems');
});
});
});

describe('provider-native YouTube file preflight', () => {
Expand Down
4 changes: 3 additions & 1 deletion api/server/middleware/abortMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
sanitizeMessageForTransmit,
buildAbortedResponseMetadata,
announceReply,
stripProviderItems,
} = require('@librechat/api');
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
const clearPendingReq = require('~/cache/clearPendingReq');
Expand Down Expand Up @@ -131,7 +132,8 @@ async function abortMessage(req, res) {
isCreatedByUser: true,
})
: null,
responseMessage,
/** The live content still carries provider items; they are server state. */
responseMessage: { ...responseMessage, content: stripProviderItems(responseMessage.content) },
};

logger.debug(`[abortMessage] ID: ${userId} | Aborted request: ${conversationId}`);
Expand Down
22 changes: 22 additions & 0 deletions api/server/middleware/abortMiddleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jest.mock('@librechat/api', () => ({
hasPersistableAbortContent: jest.requireActual('@librechat/api').hasPersistableAbortContent,
announceReply: jest.requireActual('@librechat/api').announceReply,
abortOwnedGeneration: jest.requireActual('@librechat/api').abortOwnedGeneration,
stripProviderItems: jest.requireActual('@librechat/api').stripProviderItems,
countTokens: jest.fn().mockResolvedValue(100),
isEnabled: jest.fn().mockReturnValue(false),
sendEvent: jest.fn(),
Expand Down Expand Up @@ -327,6 +328,27 @@ describe('abortMiddleware - handleAbort billing', () => {
expect(finalEvent.responseMessage.contextMeta).toEqual(contextMeta);
});

it('keeps provider items out of the response sent to the client', async () => {
const part = {
type: 'tool_call',
tool_call: { id: 'call_1', name: 'x', providerItems: { origin: 'o', items: [] } },
};
GenerationJobManager.abortJob.mockResolvedValue({
success: true,
jobData: buildJobData(),
content: [part],
text: 'partial',
collectedUsage: [],
});
const res = buildRes();

await handleAbort()(buildReq(), res);

const finalEvent = JSON.parse(res.send.mock.calls[0][0]);
expect(finalEvent.responseMessage.content[0].tool_call.providerItems).toBeUndefined();
expect(finalEvent.responseMessage.content[0].tool_call.id).toBe('call_1');
});

it('unsets context meta on a stopped response when the job carries none', async () => {
GenerationJobManager.abortJob.mockResolvedValue({
success: true,
Expand Down
34 changes: 34 additions & 0 deletions api/server/routes/__tests__/messages-get.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ jest.mock('@librechat/api', () => {
stripPrivateMessageFields: jest.requireActual(
'../../../../packages/api/src/protection/private/view',
).stripPrivateMessageFields,
stripProviderItems: jest.requireActual('../../../../packages/api/src/utils/message')
.stripProviderItems,
createContentFilter: jest.fn(() => (req, res, next) => next()),
inspectContent,
extractChatContent,
Expand Down Expand Up @@ -590,6 +592,38 @@ describe('message route conversation ownership filters', () => {
);
});

it('does not accept tool call provider items from a client', async () => {
const conversationId = '11111111-1111-4111-8111-111111111111';
const toolCall = (providerItems) => ({
type: 'tool_call',
agentId: 'agent-1',
tool_call: {
id: 'call_1',
name: 'lookup',
args: '{}',
...(providerItems && { providerItems }),
},
});
const providerItems = {
origin: 'digest',
items: [{ id: 'rs_1', encrypted_content: 'opaque' }],
};
saveMessage.mockImplementation(async (_ctx, message) => message);
saveConvo.mockResolvedValue({ conversationId });

const saved = await request(app)
.post(`/api/messages/${conversationId}`)
.send({
messageId: 'message-1',
text: '',
content: [toolCall(providerItems)],
providerItems: { call_1: providerItems },
});
expect(saved.status).toBe(201);
expect(saveMessage.mock.calls[0][1].content).toEqual([toolCall()]);
expect(JSON.stringify(saved.body)).not.toContain('opaque');
});

it('should filter conversation message reads by authenticated user', async () => {
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);

Expand Down
2 changes: 2 additions & 0 deletions api/server/routes/__tests__/messages-retention.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jest.mock('@librechat/api', () => ({
stripPrivateMessageFields: jest.requireActual(
'../../../../packages/api/src/protection/private/view',
).stripPrivateMessageFields,
stripProviderItems: jest.requireActual('../../../../packages/api/src/utils/message')
.stripProviderItems,
unescapeLaTeX: jest.fn((value) => value),
countTokens: jest.fn().mockResolvedValue(10),
createContentFilter: jest.fn(() => (_req, _res, next) => next()),
Expand Down
55 changes: 55 additions & 0 deletions api/server/routes/agents/__tests__/streamTenant.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,38 @@ describe('SSE stream tenant isolation', () => {
);
});

it('writes stream events and the final event without tool call provider items', async () => {
const toolCall = { id: 'call_1', name: 'lookup', args: '{}' };
const content = [
{
type: 'tool_call',
tool_call: { ...toolCall, providerItems: { origin: 'digest', items: [] } },
},
];
mockGenerationJobManager.subscribe.mockImplementation((_streamId, writeEvent, onDone) => {
process.nextTick(() => {
writeEvent({ sync: true, resumeState: { aggregatedContent: content } });
onDone({ final: true, responseMessage: { content } });
});
return { unsubscribe: jest.fn() };
});
mockGenerationJobManager.getJob.mockResolvedValue({
metadata: { userId: 'user-123' },
status: 'running',
createdAt: 1000,
});

const res = await request(app).get('/agents/chat/stream/stream-123?generationCreatedAt=1000');

expect(res.status).toBe(200);
const [sync, final] = res.text
.split('\n\n')
.filter((frame) => frame.includes('data: '))
.map((frame) => JSON.parse(frame.split('data: ')[1]));
expect(sync.resumeState.aggregatedContent[0].tool_call).toEqual(toolCall);
expect(final.responseMessage.content[0].tool_call).toEqual(toolCall);
});

it('pins an unfenced legacy subscription to the generation that passed authorization', async () => {
mockSubscribeSuccess();
mockGenerationJobManager.getJob.mockResolvedValue({
Expand Down Expand Up @@ -640,6 +672,29 @@ describe('SSE stream tenant isolation', () => {
expect(res.body.active).toBe(true);
});

it('leaves the provider items of live tool calls out of the status', async () => {
mockUserId = 'user-123';
mockTenantId = 'tenant-a';
mockGenerationJobManager.getJob.mockResolvedValue({
metadata: { userId: 'user-123', tenantId: 'tenant-a' },
status: 'running',
createdAt: 1000,
});
const toolCall = { id: 'call_1', name: 'lookup', args: '{}' };
const providerItems = { origin: 'digest', items: [{ id: 'rs_1', encrypted_content: 'x' }] };
mockGenerationJobManager.getResumeState.mockResolvedValue({
runSteps: [],
aggregatedContent: [{ type: 'tool_call', tool_call: { ...toolCall, providerItems } }],
});

const res = await request(app).get('/agents/chat/status/conv-123');

expect(res.status).toBe(200);
const expected = [{ type: 'tool_call', tool_call: toolCall }];
expect(res.body.aggregatedContent).toEqual(expected);
expect(res.body.resumeState.aggregatedContent).toEqual(expected);
});

it.each([true, false])(
'reports the run temporary state %s recorded at admission',
async (isTemporary) => {
Expand Down
Loading