Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,5 @@ helm/**/.values.yaml
/.tabnine/
/.codeium
*.local.md

./litellm/
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
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');

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Delete ·

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'

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
);

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Delete ⏎

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

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace ⏎········.status(400)⏎········ with .status(400)
}

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

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

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

const headers = {};

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Delete ····
// 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);

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Delete ····
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);

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace '[GuardrailsService]·Failed·to·fetch·guardrails:',·response.status,·response.statusText with ⏎········'[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,
};
28 changes: 26 additions & 2 deletions client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import { CustomMenu as Menu } from './CustomMenu';
import DialogManager from './DialogManager';
import { useLocalize } from '~/hooks';
import GuardrailsSelect from '~/components/Input/GuardrailsSelect';

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Delete ·⏎

function ModelSelectorContent() {
const localize = useLocalize();
Expand Down Expand Up @@ -81,6 +83,7 @@
endpoint: values.endpoint || '',
model: values.model || '',
modelSpec: values.modelSpec || '',
guardrails: values.guardrails || [],
});
}}
onSearch={(value) => setSearchValue(value)}
Expand All @@ -98,11 +101,32 @@
)}
{/* Render endpoints (will include grouped specs matching endpoint names) */}
{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>

Check failure

Code scanning / ESLint

disallow literal string Error

disallow literal string:
Guardrails
<GuardrailsSelect
conversation={{
guardrails: selectedValues.guardrails || []

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
}}
setOption={(key) => (value) => {
console.log('setOption called with:', key, value);
setSelectedValues({
...selectedValues,
[key]: value

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
});
}}
showAbove={false}
/>
</div>
)}
</>
)}
</Menu>


<DialogManager
keyDialogOpen={keyDialogOpen}
onOpenChange={onOpenChange}
Expand Down
170 changes: 170 additions & 0 deletions client/src/components/Input/GuardrailsSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useLocalize } from '~/hooks';
import { useGuardrails } from '~/hooks/useGuardrails';
import { useChatContext } from '~/Providers';

Check warning

Code scanning / ESLint

Disallow unused variables Warning

'useChatContext' is defined but never used. Allowed unused vars must match /^_/u.
import { CheckMark, useOnClickOutside } from '@librechat/client';
import {
Listbox,
ListboxButton,
} from '@headlessui/react';

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace ⏎··Listbox,⏎··ListboxButton,⏎ with ·Listbox,·ListboxButton·
import { Shield, ChevronDown } from 'lucide-react';
import { cn } from '~/utils';

interface GuardrailsSelectProps {
conversation: any;
setOption: (key: string) => (value: any) => void;
showAbove?: boolean;
}

export default function GuardrailsSelect({
conversation,
setOption,
showAbove = false,

Check warning

Code scanning / ESLint

Disallow unused variables Warning

'showAbove' is assigned a value but never used. Allowed unused args must match /^_/u.
}: GuardrailsSelectProps) {

console.log("GuardrailsSelect rendered");

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace ⏎··console.log("GuardrailsSelect·rendered" with ··console.log('GuardrailsSelect·rendered'
console.log("Conversation:", conversation);

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace "Conversation:" with 'Conversation:'

const localize = useLocalize();

Check warning

Code scanning / ESLint

Disallow unused variables Warning

'localize' is assigned a value but never used. Allowed unused vars must match /^_/u.
const [isOpen, setIsOpen] = useState(false);
const [selectedGuardrails, setSelectedGuardrails] = useState<string[]>([]);
const menuRef = useRef<HTMLDivElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [buttonPosition, setButtonPosition] = useState({
top: 0,
left: 0,
width: 0,
height: 0,
});

const { data: guardrails = [], isLoading, error } = useGuardrails();

useOnClickOutside(menuRef, () => setIsOpen(false));

// Track button position for portal dropdown placement
useEffect(() => {
if (isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setButtonPosition({
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
height: rect.height,
});
}
}, [isOpen]);

// Initialize selected guardrails
useEffect(() => {
if (conversation?.guardrails) {
setSelectedGuardrails(conversation.guardrails);
}
}, [conversation?.guardrails]);

const handleGuardrailToggle = (guardrailName: string) => {
setSelectedGuardrails(prev => {

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace prev with (prev)
const newSelection = prev.includes(guardrailName)
? prev.filter(name => name !== guardrailName)

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Replace name with (name)
: [...prev, guardrailName];

console.log('Updating guardrails:', newSelection);
setOption('guardrails')(newSelection);
return newSelection;
});
};

if (isLoading) {
return (
<div className="flex items-center gap-2 text-sm text-gray-500">
<Shield className="h-4 w-4" />
<span>Loading guardrails...</span>
</div>
);
}

if (error) {
return (
<div className="flex items-center gap-2 text-sm text-red-500">
<Shield className="h-4 w-4" />
<span>Failed to load guardrails</span>

Check failure

Code scanning / ESLint

disallow literal string Error

disallow literal string: Failed to load guardrails
</div>
);
}

if (guardrails.length === 0) {
return null;
}

return (
<div className="relative w-full">
<Listbox value={selectedGuardrails} onChange={() => {}}>
{() => (
<>
<ListboxButton
ref={buttonRef}
className={cn(
'relative flex w-full cursor-default items-center justify-between rounded-md border border-black/10 bg-white py-2 pl-3 pr-3 text-left focus:outline-none dark:border-gray-600 dark:bg-gray-800 sm:text-sm'

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
)}
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-gray-500" />
<span className="text-gray-800 dark:text-white">
{selectedGuardrails.length === 0
? 'No guardrails selected'
: `${selectedGuardrails.length} guardrail${selectedGuardrails.length > 1 ? 's' : ''} selected`}
</span>
</div>
<ChevronDown
className={cn('h-4 w-4 text-gray-400 transition-transform', isOpen && 'rotate-180')}
/>
</ListboxButton>

{isOpen &&
createPortal(
<div
ref={menuRef}
className="fixed z-[99999] rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-800"
style={{
top: `${buttonPosition.top + buttonPosition.height + 8}px`,
left: `${buttonPosition.left}px`,
width: `${buttonPosition.width}px`,
maxHeight: '300px',
overflow: 'auto',
border: '1px solid #e5e7eb',
borderRadius: '8px',
}}
>
{guardrails.map((guardrailName: string) => {
const isSelected = selectedGuardrails.includes(guardrailName);
return (
<div
key={guardrailName}
className={cn(
'relative flex cursor-pointer select-none items-center px-3 py-2 text-sm',
isSelected
? 'bg-blue-50 text-blue-900 dark:bg-blue-900 dark:text-blue-100'
: 'text-gray-900 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-gray-700'

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
)}
onClick={() => handleGuardrailToggle(guardrailName)}
>
{/* <Shield className="h-4 w-4 text-gray-500" /> */}
<span className="ml-2 font-medium">{guardrailName}</span>
{isSelected && (
<span className="ml-auto text-blue-600">
<CheckMark />
</span>
)}
</div>
);
})}
</div>,
document.body

Check failure

Code scanning / ESLint

Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error

Insert ,
)}
</>
)}
</Listbox>
</div>
);
}
18 changes: 11 additions & 7 deletions client/src/components/Input/ModelSelect/ModelSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TConversation } from 'librechat-data-provider';
import type { TSetOption } from '~/common';
import { multiChatOptions } from './options';
import GuardrailsSelect from '~/components/Input/GuardrailsSelect';

type TGoogleProps = {
showExamples: boolean;
Expand Down Expand Up @@ -39,12 +40,15 @@ export default function ModelSelect({
}

return (
<OptionComponent
conversation={conversation}
setOption={setOption}
models={models}
showAbove={showAbove}
popover={popover}
/>
<>
<OptionComponent
conversation={conversation}
setOption={setOption}
models={models}
showAbove={showAbove}
popover={popover}
/>
<GuardrailsSelect conversation={conversation} setOption={setOption} showAbove={showAbove} />
</>
);
}
Loading
Loading