Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,6 @@ helm/**/.values.yaml
/.tabnine/
/.codeium
*.local.md

/litellm/
docker-compose.yml
10 changes: 10 additions & 0 deletions api/app/clients/OpenAIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,16 @@ ${convo}
});
}

// Handle guardrails parameter
const guardrails = this.options.req?.body?.guardrails;
if (guardrails && Array.isArray(guardrails) && guardrails.length > 0) {
modelOptions.guardrails = guardrails;
logger.debug('[OpenAIClient] chatCompletion: added guardrails', {
guardrails: guardrails,
modelOptions,
});
}

/** Note: OpenAI Web Search models do not support any known parameters besdies `max_tokens` */
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
const searchExcludeParams = [
Expand Down
37 changes: 36 additions & 1 deletion api/server/controllers/auth/LogoutController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,33 @@ const { logoutUser } = require('~/server/services/AuthService');
const { getOpenIdConfig } = require('~/strategies');

const logoutController = async (req, res) => {
// Entry log to confirm controller is invoked
try {
logger.info('[logoutController] invoked', {
method: req.method,
path: req.originalUrl || req.url,
hasCookiesHeader: Boolean(req.headers.cookie),
hasAuthHeader: Boolean(req.headers.authorization),
userId: req.user?.id || req.user?._id || null,
openidId: req.user?.openidId || null,
query: req.query || {},
});
} catch (e) {
console.error('[logoutController] Logging failed during invocation:', e.message);
}
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
try {
const logout = await logoutUser(req, refreshToken);
const { status, message } = logout;
res.clearCookie('refreshToken');
res.clearCookie('token_provider');
const response = { message };
// Log any incoming redirect hints on request
if (req.query && (req.query.redirect || req.query.redirect_uri)) {
logger.info('[logoutController] query redirect detected', {
redirect: req.query.redirect || req.query.redirect_uri,
});
}
if (
req.user.openidId != null &&
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
Expand All @@ -27,14 +47,29 @@ const logoutController = async (req, res) => {
? openIdConfig.serverMetadata().end_session_endpoint
: null;
if (endSessionEndpoint) {
response.redirect = endSessionEndpoint;
const postLogoutRedirect = process.env.OPENID_POST_LOGOUT_REDIRECT_URI;
const clientId = process.env.OPENID_CLIENT_ID;
let logoutUrl = `${endSessionEndpoint}`;
if (clientId) {
logoutUrl += `${logoutUrl.includes('?') ? '&' : '?'}client_id=${encodeURIComponent(clientId)}`;
}
if (postLogoutRedirect) {
logoutUrl += `${logoutUrl.includes('?') ? '&' : '?'}post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirect)}`;
}
response.redirect = logoutUrl;
// logger.info('[logoutController] end_session_endpoint', { endSessionEndpoint, logoutUrl });
} else {
logger.warn(
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
);
}
}
}
try {
logger.info('[logoutController] responding', { status, response });
} catch (e) {
console.error('[logoutController] Logging failed during response:', e.message);
}
return res.status(status).send(response);
} catch (err) {
logger.error('[logoutController]', err);
Expand Down
1 change: 1 addition & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const startServer = async () => {

app.use('/api/tags', routes.tags);
app.use('/api/mcp', routes.mcp);
app.use('/api/guardrails', routes.guardrails);

app.use(ErrorController);

Expand Down
53 changes: 53 additions & 0 deletions api/server/routes/guardrails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const express = require('express');
const { fetchGuardrails } = require('~/server/services/GuardrailsService');
const { logger } = require('~/utils');
const configMiddleware = require('~/server/middleware/config/app');

const router = express.Router();

router.use(configMiddleware);

/**
* GET /api/guardrails
* Fetches available guardrails from LiteLLM
*/
router.get('/', async (req, res) => {
try {
// Resolve LiteLLM from custom endpoints by name (e.g., name: "litellm")
const litellmConfig = req.config?.endpoints?.custom?.find(
(ep) => (ep?.name || '').toLowerCase() === 'litellm'
);


if (!litellmConfig) {
return res
.status(400)
.json({ error: 'LiteLLM endpoint not configured in LibreChat config' });
}

const baseURL = litellmConfig.baseURL;
const apiKey = litellmConfig.apiKey;

if (!baseURL) {
return res.status(400).json({ error: 'LiteLLM base URL not configured' });
}

const headers = {};

// Use authorization from request or config
if (req.headers.authorization) {
headers.Authorization = req.headers.authorization;
} else if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}

const guardrails = await fetchGuardrails(baseURL, headers);

res.json({ guardrails });
} catch (error) {
logger.error('[GuardrailsRoute] Error fetching guardrails:', error);
res.status(500).json({ error: 'Failed to fetch guardrails' });
}
});

module.exports = router;
2 changes: 2 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ const edit = require('./edit');
const keys = require('./keys');
const user = require('./user');
const mcp = require('./mcp');
const guardrails = require('./guardrails');

module.exports = {
mcp,
guardrails,
edit,
auth,
keys,
Expand Down
34 changes: 34 additions & 0 deletions api/server/services/GuardrailsService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { logger } = require('~/utils');

/**
* Fetches available guardrails from LiteLLM endpoint
* @param {string} baseURL - The LiteLLM base URL
* @param {Object} headers - Request headers
* @returns {Promise<Array>} Array of available guardrails
*/
async function fetchGuardrails(baseURL, headers = {}) {
try {
const response = await fetch(`${baseURL}/guardrails/list`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...headers,
},
});

if (!response.ok) {
logger.warn('[GuardrailsService] Failed to fetch guardrails:', response.status, response.statusText);
return [];
}

const data = await response.json();
return data.guardrails || [];
} catch (error) {
logger.error('[GuardrailsService] Error fetching guardrails:', error);
return [];
}
}

module.exports = {
fetchGuardrails,
};
42 changes: 28 additions & 14 deletions client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import React, { useMemo } from 'react';
import type { ModelSelectorProps } from '~/common';
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
import {
renderModelSpecs,
renderEndpoints,
renderSearchResults,
renderCustomGroups,
} from './components';
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
import { getSelectedIcon, getDisplayValue } from './utils';
import { CustomMenu as Menu } from './CustomMenu';
import DialogManager from './DialogManager';
import { useLocalize } from '~/hooks';
import GuardrailsSelect from '~/components/Input/GuardrailsSelect';


function ModelSelectorContent() {
const localize = useLocalize();
Expand Down Expand Up @@ -81,6 +78,7 @@ function ModelSelectorContent() {
endpoint: values.endpoint || '',
model: values.model || '',
modelSpec: values.modelSpec || '',
guardrails: values.guardrails || [],
});
}}
onSearch={(value) => setSearchValue(value)}
Expand All @@ -91,18 +89,34 @@ function ModelSelectorContent() {
renderSearchResults(searchResults, localize, searchValue)
) : (
<>
{/* Render ungrouped modelSpecs (no group field) */}
{renderModelSpecs(
modelSpecs?.filter((spec) => !spec.group) || [],
selectedValues.modelSpec || '',
)}
{/* Render endpoints (will include grouped specs matching endpoint names) */}
{renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')}
{renderEndpoints(mappedEndpoints ?? [])}
{/* Render custom groups (specs with group field not matching any endpoint) */}
{renderCustomGroups(modelSpecs || [], mappedEndpoints ?? [])}
{/* Guardrails section - only show when a model is selected */}
{selectedValues.model && (
<div className="border-t border-gray-200 dark:border-gray-600">
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400">
Guardrails
</div>
<GuardrailsSelect
conversation={{
guardrails: selectedValues.guardrails || []
}}
setOption={(key) => (value) => {
console.log('setOption called with:', key, value);
setSelectedValues({
...selectedValues,
[key]: value
});
}}
showAbove={false}
/>
</div>
)}
</>
)}
</Menu>


<DialogManager
keyDialogOpen={keyDialogOpen}
onOpenChange={onOpenChange}
Expand Down
Loading