LibreChat/api/server/controllers/PluginController.js
Danny Avila f05f50460a
🔭 feat: Admin Config Read APIs (Resolved and Effective Config, Scope Catalog, Env Status) (#17383)
* 🔭 feat: Admin Config Read APIs (Resolved and Effective Config, Scope Catalog, Env Status)

Admin clients could read one config document at a time, or the deployment's merged config,
but nothing showed what a profile or a user resolves to: a client had to re-implement the
merge (Base-first precedence, merge keys, tombstones, the interface permission filter,
base-only fields, runtime stops, secret rebinding, the env allowlist) to preview it, and had
no way to see why part of a profile is ignored. Endpoint, model and tool pickers came from the
end-user routes, called as the signed-in admin, so they showed the admin's own scope and keys
and needed a browser User-Agent.

The admin config API now has four read routes. GET /resolved/:principalType/:principalId and
GET /effective?userId= return librechat.yaml, the documents that merge over it in merge order,
each with the parts the merge does not apply and why, and the result, in librechat.yaml shape.
They come from resolveConfigLayers, which tracks the same fold the served merge runs and is
pinned to it by parity tests, and from the stored documents rather than the override cache.
GET /catalog lists the endpoints (with the reason one is not served), models (served,
configured and defaults), MCP servers and tools a scope serves, resolved for the scope rather
than the caller and never with a user's stored key; GET /env-status reports whether each env
name the config uses is set, never its value. Every read is filtered to the sections the caller
can read, redacted as reads redact, and scoped to the caller's tenant. The admin Langfuse routes
now say why a deployment offers no connection settings. Meta reports resolvedApi, catalogApi,
envStatus and limits.catalogModelsTtlMs.

* 🪞 fix: Report Duplicate Custom Endpoints from the Served Definition

When librechat.yaml defines a custom endpoint name twice, or both `Ollama` and `ollama`, the
catalog kept the first definition for a name's env names, reason and models, while
`loadCustomEndpointsConfig` serves the last servable one. An entry could pair the served config
of one definition with the env names of another.

`resolveServedCustomEndpoints` now holds the runtime rule (the last definition with a name, key,
base URL and models wins under its served name), and `loadCustomEndpointsConfig`,
`loadConfigModels` and the catalog all use it. A name with no servable definition is reported
from its last one.

* ⏱️ feat: Make the Admin Catalog Model-List Lifetime Configurable

`GET /api/admin/config/catalog` reused a scope's model lists for a fixed two minutes. The new
librechat.yaml field `endpoints.catalogModelsTtlMs` sets that lifetime (milliseconds, `0` to one
hour, two minutes by default); `0` resolves the lists on every read. It is a base-only path, so a
profile cannot change it and `GET /meta` reports one value, now read from the deployment's config
on each request as `limits.catalogModelsTtlMs`.
2026-10-24 09:23:22 +02:00

56 lines
2 KiB
JavaScript

const { logger } = require('@librechat/data-schemas');
const { listAvailableTools } = require('@librechat/api');
const { getCachedTools, setCachedTools } = require('~/server/services/Config');
const { availableTools, toolkits } = require('~/app/clients/tools');
const { getAppConfig } = require('~/server/services/Config');
const getAvailableTools = async (req, res) => {
try {
const userId = req.user?.id;
if (!userId) {
logger.warn('[getAvailableTools] User ID not found in request');
return res.status(401).json({ message: 'Unauthorized' });
}
const appConfig =
req.config ??
(await getAppConfig({
role: req.user?.role,
userId: req.user?.id,
tenantId: req.user?.tenantId,
}));
let toolDefinitions = await getCachedTools();
if (toolDefinitions == null && appConfig?.availableTools != null) {
logger.warn('[getAvailableTools] Tool cache was empty, re-initializing from app config');
await setCachedTools(appConfig.availableTools);
toolDefinitions = appConfig.availableTools;
}
/**
* `getAvailableTools` serves BOTH tool dialogs — /api/agents/tools and
* /api/assistants/tools. Tools flagged `agentsOnly` in the manifest (e.g.
* ask_user_question, which pauses an agents run via a LangGraph interrupt)
* cannot work on the assistants runtime: it executes tools directly with no
* run to pause and no resume surface, so attaching one there guarantees a
* permanent tool error. Scope them out of the assistants listing by route.
*/
const toolsOutput = listAvailableTools({
manifest: availableTools,
toolkits,
toolDefinitions,
githubCompare: appConfig?.githubCompare,
forAssistants: req.baseUrl?.includes('/assistants') === true,
});
res.status(200).json(toolsOutput);
} catch (error) {
logger.error('[getAvailableTools]', error);
res.status(500).json({ message: error.message });
}
};
module.exports = {
getAvailableTools,
};