Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
963068b
🧬 feat: Scaffold Skills CRUD with ACL Sharing and File Schema (#12613)
danny-avila Apr 11, 2026
f6ee2ea
📜 feat: Skills UI + Initial E2E CRUD / Sharing (#12580)
berry-13 Apr 13, 2026
64ec5f1
⚙️ feat: Skill runtime integration: catalog, tools, execution, file p…
danny-avila Apr 16, 2026
3b82041
🎭 feat: Custom UI Renderers for Skill Tool Calls (#12684)
danny-avila Apr 16, 2026
9b4ae06
💲 feat: Manual Skill Invocation via $ Command Popover (UI only) (#12690)
danny-avila Apr 16, 2026
3e064c2
🎯 feat: Per-Agent Skill Selection in Builder and Runtime Scoping (#12…
danny-avila Apr 16, 2026
9225a27
🎚️ feat: Per-User Skill Active/Inactive Toggle with Ownership-Aware D…
danny-avila Apr 17, 2026
3f1bde4
🪆 feat: Compose Agent Scope and Active-State Filters in $ Popover (#1…
danny-avila Apr 17, 2026
b88e347
📦 chore: bump `@librechat/agents` to dev latest
danny-avila Apr 17, 2026
539c4c7
🎬 feat: Prime Manually-Invoked Skills via $ Popover (#12709)
danny-avila Apr 19, 2026
bb13d5b
🪜 chore: Plumb `allowedTools` through `resolveManualSkills` (#12744)
danny-avila Apr 19, 2026
82173f7
🛡️ feat: Persist & enforce `disable-model-invocation` / `user-invocab…
danny-avila Apr 20, 2026
dfc3dfa
📍 feat: `always-apply` frontmatter: auto-prime skills every turn (#12…
danny-avila Apr 21, 2026
9e013be
📦 chore: bump `@librechat/agents` to v3.1.68-dev.0
danny-avila Apr 21, 2026
89b6bff
🧼 fix: Missing Enum imports
danny-avila Apr 21, 2026
5c69d1f
🩹 fix: define appConfig in Responses API createResponse
danny-avila Apr 21, 2026
7581540
🔌 refactor: Decouple bash_tool from Per-User CODE_API_KEY (#12712)
danny-avila Apr 21, 2026
91cd3f7
🧽 refactor: Skills polish: precedence-aware body validation, controll…
danny-avila Apr 21, 2026
ac913aa
🔐 chore: Skills Permissions Housekeeping, Reachable Admin Dialog + De…
danny-avila Apr 21, 2026
35bf04b
🧰 refactor: Unify code-execution tools (#12767)
danny-avila Apr 22, 2026
d83cb84
🪆 feat: Subagent configuration in Agent Builder (#12725)
danny-avila Apr 23, 2026
5239942
🌉 chore: Gate Skills UI by Agent Capability Checks (#12793)
danny-avila Apr 23, 2026
7f3d410
📦 chore: Update `@librechat/agents` to v3.1.70
danny-avila Apr 23, 2026
596f806
🛡️ fix: Strict Opt-In Skills Activation per Agent (#12823)
danny-avila Apr 25, 2026
8c073b4
📄 feat: Auto-render Text-Based Code Execution Artifacts Inline (#12829)
danny-avila Apr 26, 2026
f7e47f6
🪢 feat: Enable Tool-Output References for Bash Tool (#12830)
danny-avila Apr 26, 2026
24e29aa
🌱 fix: Inject Code-Tool Files Into Graph Sessions on First Call (+ re…
danny-avila Apr 26, 2026
47f65fe
🪟 feat: Render Code-Execution Text Artifacts as Side-Panel Artifacts …
danny-avila Apr 27, 2026
2624c18
🚫 fix: Reject Binary Files in read_file Sandbox Fallback (No More Moj…
danny-avila Apr 28, 2026
7070eb7
🔧 fix: Replace Literal NUL Bytes in handlers.spec Test Fixture + Norm…
danny-avila Apr 28, 2026
c9dee96
📂 fix: Preserve Nested Folder Paths for Code-Execution Artifacts (#12…
danny-avila Apr 28, 2026
f69e8e2
🪟 feat: Render Source-Code Artifacts in the Side Panel (#12854)
danny-avila Apr 28, 2026
46a86d8
🛂 fix: Skip Inherited / Mark Skill Files Read-Only in Code-Env Pipeli…
danny-avila Apr 28, 2026
89bf2ab
💎 fix: Stop Double-Counting Cache Tokens for Gemini/OpenAI in Usage S…
danny-avila Apr 28, 2026
f2df0ea
🛡️ fix: Filter `user_provided` Sentinel in Tool Credential Loading (#…
Falenos Apr 29, 2026
8404343
🧹 fix: Graceful MCP OAuth Revoke Cleanup When Tokens Are Missing (#12…
gaurav0107 Apr 29, 2026
2503365
🚫 feat: Add Support for `none` Reranker Type in Web Search Config (#1…
dlew Apr 29, 2026
1f37ec8
🔌 fix: Prevent Repeated Idle Check Triggers for Users With Failed MCP…
darthhexx Apr 29, 2026
85894c1
🧜‍♂️ fix: Preserve Mermaid `foreignObject` HTML in Sanitized SVG (#12…
ethanlaj Apr 29, 2026
61b9b1d
🩹 fix(SSE): Treat `responseCode === 0` as Transport Failure, Not Serv…
derhelge Apr 29, 2026
915b30c
📦 chore: update @librechat/agents to v3.1.74 (#12869)
danny-avila Apr 29, 2026
4a5fc70
📂 fix: Preserve Nested Skill Paths in Code-Env Uploads (#12877)
danny-avila Apr 29, 2026
756530c
🩹 fix: Polish code-execution attachment UX (#12870)
danny-avila Apr 29, 2026
3758380
🔌 fix: Follow 307/308 redirects in MCP streamable HTTP transport (#12…
ontl Apr 29, 2026
65990a3
📥 fix: Resolve Imported-Conversation Default Model From Runtime model…
danny-avila Apr 30, 2026
781bfb8
🩹 fix: Sync ControlCombobox popover width with trigger after layout c…
ethanlaj Apr 30, 2026
74307e6
💭 feat: Require Explicit Auto-agent Enablement for Memories (#12886)
danny-avila May 1, 2026
5b5e2b0
🛡️ fix: Handle MCP Tool Cache Lookup Failures (#12910)
danny-avila May 2, 2026
f3e1201
📌 fix: Stabilize Agent Prompt Cache Prefix (#12907)
danny-avila May 2, 2026
eb22bb6
🧭 fix: Migrate Anthropic Long Context (#12911)
danny-avila May 2, 2026
4e45e8e
🧹 fix: Clear MCP OAuth Tokens On Revoke
danny-avila May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 1 addition & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ PROXY=
#============#

ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_MODELS=claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_REVERSE_PROXY=

# Set to true to use Anthropic models through Google Vertex AI instead of direct API
Expand Down Expand Up @@ -810,13 +810,6 @@ HELP_AND_FAQ_URL=https://librechat.ai
#=====================================================#
OPENWEATHER_API_KEY=

#====================================#
# LibreChat Code Interpreter API #
#====================================#

# https://code.librechat.ai
# LIBRECHAT_CODE_API_KEY=your-key

#======================#
# Web Search #
#======================#
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/backend-review.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: Backend Unit Tests
on:
pull_request:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'api/**'
- 'packages/**'
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/frontend-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ name: Frontend Unit Tests

on:
pull_request:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'client/**'
- 'packages/data-provider/**'
Expand Down
59 changes: 55 additions & 4 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,39 @@ class BaseClient {
}
delete userMessage.image_urls;
}
/**
* Persist the user's manual skill picks onto the user message so the
* frontend `SkillPills` component can render them in history
* after reload. UI-only metadata — the runtime skill resolution
* pipeline reads the top-level `req.body.manualSkills` separately.
* Filter is defense-in-depth on top of Mongoose schema validation:
* keeps the DB row free of empty/non-string entries even if a
* crafted payload slips past schema checks upstream.
*/
const rawManualSkills = this.options.req?.body?.manualSkills;
if (Array.isArray(rawManualSkills) && rawManualSkills.length > 0) {
const skills = rawManualSkills.filter((s) => typeof s === 'string' && s.length > 0);
if (skills.length > 0) {
userMessage.manualSkills = skills;
}
}
/**
* Persist the names of skills auto-primed this turn via `always-apply`
* frontmatter so `SkillPills` can render pinned-variant badges
* on the user bubble that survive reload and history render. Frozen
* at turn time (not reconstructed from `Skill.alwaysApply` at render
* time) because the flag is mutable — historical turns must keep
* their audit trail even if an admin flips `alwaysApply` off later.
*/
const alwaysApplySkillPrimes = this.options.agent?.alwaysApplySkillPrimes;
if (Array.isArray(alwaysApplySkillPrimes) && alwaysApplySkillPrimes.length > 0) {
const names = alwaysApplySkillPrimes
.map((p) => p?.name)
.filter((n) => typeof n === 'string' && n.length > 0);
if (names.length > 0) {
userMessage.alwaysAppliedSkills = names;
}
}
userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user).catch(
(err) => {
logger.error('[BaseClient] Failed to save user message:', err);
Expand Down Expand Up @@ -780,11 +813,28 @@ class BaseClient {
endpointType: options.endpointType,
...endpointOptions,
};
const conversationCreatedAt = options?.req?.conversationCreatedAt;
const createdAtOnInsert =
conversationCreatedAt != null ? new Date(conversationCreatedAt) : undefined;
const validCreatedAtOnInsert =
createdAtOnInsert && !Number.isNaN(createdAtOnInsert.getTime())
? createdAtOnInsert
: undefined;

const existingConvo =
this.fetchedConvo === true
? null
: await db.getConvo(options?.req?.user?.id, message.conversationId);
const req = options?.req;
const skippedExistingConvoLookup = this.fetchedConvo === true;
const hasResolvedConversation =
req != null && Object.prototype.hasOwnProperty.call(req, 'resolvedConversation');
let existingConvo = null;
if (!skippedExistingConvoLookup && hasResolvedConversation) {
existingConvo = req.resolvedConversation;
} else if (!skippedExistingConvoLookup) {
existingConvo = await db.getConvo(req?.user?.id, message.conversationId);
}
if (hasResolvedConversation) {
delete req.resolvedConversation;
}
const shouldSetCreatedAtOnInsert = !skippedExistingConvoLookup && existingConvo == null;

const unsetFields = {};
const exceptions = new Set(['spec', 'iconURL']);
Expand Down Expand Up @@ -814,6 +864,7 @@ class BaseClient {
const conversation = await db.saveConvo(reqCtx, fieldsToKeep, {
context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo',
unsetFields,
createdAtOnInsert: shouldSetCreatedAtOnInsert ? validCreatedAtOnInsert : undefined,
});

return { message: savedMessage, conversation };
Expand Down
39 changes: 39 additions & 0 deletions api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,45 @@ describe('BaseClient', () => {
saveConvo.mockReset();
});

test('saveMessageToDatabase reuses conversation resolved on the request', async () => {
const existingConvo = {
conversationId: 'cached-convo-id',
endpoint: 'openai',
endpointType: 'openai',
temperature: 0.7,
};
const user = { id: 'user-id' };
const req = { user, resolvedConversation: existingConvo };

getConvo.mockClear();
saveMessage.mockResolvedValue({ messageId: 'msg-1' });
saveConvo.mockResolvedValue(existingConvo);

TestClient = initializeFakeClient(apiKey, { ...options, endpoint: 'openai', req }, []);

await TestClient.saveMessageToDatabase(
{
messageId: 'msg-1',
conversationId: existingConvo.conversationId,
isCreatedByUser: true,
text: 'hi',
},
{ endpoint: 'openai' },
user,
);

expect(getConvo).not.toHaveBeenCalled();
expect(req).not.toHaveProperty('resolvedConversation');
expect(TestClient.fetchedConvo).toBe(true);
expect(saveConvo).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ conversationId: existingConvo.conversationId }),
expect.objectContaining({
unsetFields: expect.objectContaining({ temperature: 1 }),
}),
);
});

test('userMessagePromise is awaited before saving response message', async () => {
// Mock the saveMessageToDatabase method
TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => {
Expand Down
65 changes: 32 additions & 33 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const {
EnvVar,
Calculator,
createSearchTool,
createCodeExecutionTool,
} = require('@librechat/agents');
const { Calculator, createSearchTool, createCodeExecutionTool } = require('@librechat/agents');
const {
checkAccess,
toolkitParent,
Expand All @@ -13,6 +8,7 @@ const {
loadWebSearchAuth,
buildImageToolContext,
buildWebSearchContext,
buildWebSearchDynamicContext,
} = require('@librechat/api');
const {
Tools,
Expand Down Expand Up @@ -155,7 +151,7 @@ const getAuthFields = (toolKey) => {
* @param {AppConfig['webSearch']} [params.webSearch]
* @param {AppConfig['fileStrategy']} [params.fileStrategy]
* @param {AppConfig['imageOutputType']} [params.imageOutputType]
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any>, dynamicToolContextMap?: Object<string, any> } | Record<string,Tool>>}
*/
const loadTools = async ({
user,
Expand Down Expand Up @@ -185,7 +181,7 @@ const loadTools = async ({
};

const customConstructors = {
image_gen_oai: async (toolContextMap) => {
image_gen_oai: async (_toolContextMap, dynamicToolContextMap) => {
const authFields = getAuthFields('image_gen_oai');
const authValues = await loadAuthValues({ userId: user, authFields });
const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? [];
Expand All @@ -195,7 +191,7 @@ const loadTools = async ({
contextDescription: 'image editing',
});
if (toolContext) {
toolContextMap.image_edit_oai = toolContext;
dynamicToolContextMap.image_edit_oai = toolContext;
}
return createOpenAIImageTools({
...authValues,
Expand All @@ -206,7 +202,7 @@ const loadTools = async ({
imageFiles,
});
},
gemini_image_gen: async (toolContextMap) => {
gemini_image_gen: async (_toolContextMap, dynamicToolContextMap) => {
const authFields = getAuthFields('gemini_image_gen');
const authValues = await loadAuthValues({ userId: user, authFields, throwError: false });
const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? [];
Expand All @@ -216,7 +212,7 @@ const loadTools = async ({
contextDescription: 'image context',
});
if (toolContext) {
toolContextMap.gemini_image_gen = toolContext;
dynamicToolContextMap.gemini_image_gen = toolContext;
}
return createGeminiImageTool({
...authValues,
Expand Down Expand Up @@ -254,6 +250,17 @@ const loadTools = async ({

/** @type {Record<string, string>} */
const toolContextMap = {};
/** @type {Record<string, string>} */
const dynamicToolContextMap = {};
/**
* @type {import('@librechat/agents').CodeEnvFile[] | undefined}
* Captured by the `execute_code` factory when files are primed. Surfaced
* out of `loadTools` so client.js can seed `Graph.sessions[EXECUTE_CODE]`
* before run start — without that seed, the first `execute_code` /
* `bash_tool` call lands with empty `_injected_files` and the sandbox
* can't see the prior turn's generated artifacts.
*/
let primedCodeFiles;
const requestedMCPTools = {};

/** Resolve config-source servers for the current user/tenant context */
Expand All @@ -265,28 +272,17 @@ const loadTools = async ({
for (const tool of tools) {
if (tool === Tools.execute_code) {
requestedTools[tool] = async () => {
const authValues = await loadAuthValues({
userId: user,
authFields: [EnvVar.CODE_API_KEY],
const { files, toolContext } = await primeCodeFiles({
...options,
agentId: agent?.id,
});
const codeApiKey = authValues[EnvVar.CODE_API_KEY];
const { files, toolContext } = await primeCodeFiles(
{
...options,
agentId: agent?.id,
},
codeApiKey,
);
if (toolContext) {
toolContextMap[tool] = toolContext;
dynamicToolContextMap[tool] = toolContext;
}
const CodeExecutionTool = createCodeExecutionTool({
user_id: user,
files,
...authValues,
});
CodeExecutionTool.apiKey = codeApiKey;
return CodeExecutionTool;
if (files?.length) {
primedCodeFiles = files;
}
return createCodeExecutionTool({ user_id: user, files });
};
continue;
} else if (tool === Tools.file_search) {
Expand All @@ -296,7 +292,7 @@ const loadTools = async ({
agentId: agent?.id,
});
if (toolContext) {
toolContextMap[tool] = toolContext;
dynamicToolContextMap[tool] = toolContext;
}

/** @type {boolean | undefined} Check if user has FILE_CITATIONS permission */
Expand Down Expand Up @@ -332,6 +328,9 @@ const loadTools = async ({
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
requestedTools[tool] = async () => {
toolContextMap[tool] = buildWebSearchContext();
dynamicToolContextMap[tool] = buildWebSearchDynamicContext(
options.req?.conversationCreatedAt,
);
return createSearchTool({
...result.authResult,
onSearchResults,
Expand Down Expand Up @@ -381,7 +380,7 @@ const loadTools = async ({
if (!requestedTools[toolKey]) {
let cached;
requestedTools[toolKey] = async () => {
cached ??= customConstructors[toolKey](toolContextMap);
cached ??= customConstructors[toolKey](toolContextMap, dynamicToolContextMap);
return cached;
};
}
Expand Down Expand Up @@ -493,7 +492,7 @@ const loadTools = async ({
}
}
loadedTools.push(...(await Promise.all(mcpToolPromises)).flatMap((plugin) => plugin || []));
return { loadedTools, toolContextMap };
return { loadedTools, toolContextMap, dynamicToolContextMap, primedCodeFiles };
};

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@google/genai": "^1.19.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.80",
"@librechat/agents": "^3.1.68",
"@librechat/agents": "^3.1.75",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down
Loading
Loading