Skip to content

Commit de6cd1f

Browse files
authored
Merge pull request #4 from knacklabs/fix/ui-changes
Fix/UI changes
2 parents 101ea27 + 7278a28 commit de6cd1f

File tree

19 files changed

+486
-22
lines changed

19 files changed

+486
-22
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,6 @@ helm/**/.values.yaml
138138
/.tabnine/
139139
/.codeium
140140
*.local.md
141+
142+
/litellm/
143+
docker-compose.yml

api/app/clients/OpenAIClient.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,16 @@ ${convo}
12221222
});
12231223
}
12241224

1225+
// Handle guardrails parameter
1226+
const guardrails = this.options.req?.body?.guardrails;
1227+
if (guardrails && Array.isArray(guardrails) && guardrails.length > 0) {
1228+
modelOptions.guardrails = guardrails;
1229+
logger.debug('[OpenAIClient] chatCompletion: added guardrails', {
1230+
guardrails: guardrails,
1231+
modelOptions,
1232+
});
1233+
}
1234+
12251235
/** Note: OpenAI Web Search models do not support any known parameters besdies `max_tokens` */
12261236
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
12271237
const searchExcludeParams = [

api/server/controllers/auth/LogoutController.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,33 @@ const { logoutUser } = require('~/server/services/AuthService');
55
const { getOpenIdConfig } = require('~/strategies');
66

77
const logoutController = async (req, res) => {
8+
// Entry log to confirm controller is invoked
9+
try {
10+
logger.info('[logoutController] invoked', {
11+
method: req.method,
12+
path: req.originalUrl || req.url,
13+
hasCookiesHeader: Boolean(req.headers.cookie),
14+
hasAuthHeader: Boolean(req.headers.authorization),
15+
userId: req.user?.id || req.user?._id || null,
16+
openidId: req.user?.openidId || null,
17+
query: req.query || {},
18+
});
19+
} catch (e) {
20+
console.error('[logoutController] Logging failed during invocation:', e.message);
21+
}
822
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
923
try {
1024
const logout = await logoutUser(req, refreshToken);
1125
const { status, message } = logout;
1226
res.clearCookie('refreshToken');
1327
res.clearCookie('token_provider');
1428
const response = { message };
29+
// Log any incoming redirect hints on request
30+
if (req.query && (req.query.redirect || req.query.redirect_uri)) {
31+
logger.info('[logoutController] query redirect detected', {
32+
redirect: req.query.redirect || req.query.redirect_uri,
33+
});
34+
}
1535
if (
1636
req.user.openidId != null &&
1737
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
@@ -27,14 +47,29 @@ const logoutController = async (req, res) => {
2747
? openIdConfig.serverMetadata().end_session_endpoint
2848
: null;
2949
if (endSessionEndpoint) {
30-
response.redirect = endSessionEndpoint;
50+
const postLogoutRedirect = process.env.OPENID_POST_LOGOUT_REDIRECT_URI;
51+
const clientId = process.env.OPENID_CLIENT_ID;
52+
let logoutUrl = `${endSessionEndpoint}`;
53+
if (clientId) {
54+
logoutUrl += `${logoutUrl.includes('?') ? '&' : '?'}client_id=${encodeURIComponent(clientId)}`;
55+
}
56+
if (postLogoutRedirect) {
57+
logoutUrl += `${logoutUrl.includes('?') ? '&' : '?'}post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirect)}`;
58+
}
59+
response.redirect = logoutUrl;
60+
// logger.info('[logoutController] end_session_endpoint', { endSessionEndpoint, logoutUrl });
3161
} else {
3262
logger.warn(
3363
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
3464
);
3565
}
3666
}
3767
}
68+
try {
69+
logger.info('[logoutController] responding', { status, response });
70+
} catch (e) {
71+
console.error('[logoutController] Logging failed during response:', e.message);
72+
}
3873
return res.status(status).send(response);
3974
} catch (err) {
4075
logger.error('[logoutController]', err);

api/server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ const startServer = async () => {
144144

145145
app.use('/api/tags', routes.tags);
146146
app.use('/api/mcp', routes.mcp);
147+
app.use('/api/guardrails', routes.guardrails);
147148

148149
app.use(ErrorController);
149150

api/server/routes/guardrails.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const express = require('express');
2+
const { fetchGuardrails } = require('~/server/services/GuardrailsService');
3+
const { logger } = require('~/utils');
4+
const configMiddleware = require('~/server/middleware/config/app');
5+
6+
const router = express.Router();
7+
8+
router.use(configMiddleware);
9+
10+
/**
11+
* GET /api/guardrails
12+
* Fetches available guardrails from LiteLLM
13+
*/
14+
router.get('/', async (req, res) => {
15+
try {
16+
// Resolve LiteLLM from custom endpoints by name (e.g., name: "litellm")
17+
const litellmConfig = req.config?.endpoints?.custom?.find(
18+
(ep) => (ep?.name || '').toLowerCase() === 'litellm'
19+
);
20+
21+
22+
if (!litellmConfig) {
23+
return res
24+
.status(400)
25+
.json({ error: 'LiteLLM endpoint not configured in LibreChat config' });
26+
}
27+
28+
const baseURL = litellmConfig.baseURL;
29+
const apiKey = litellmConfig.apiKey;
30+
31+
if (!baseURL) {
32+
return res.status(400).json({ error: 'LiteLLM base URL not configured' });
33+
}
34+
35+
const headers = {};
36+
37+
// Use authorization from request or config
38+
if (req.headers.authorization) {
39+
headers.Authorization = req.headers.authorization;
40+
} else if (apiKey) {
41+
headers.Authorization = `Bearer ${apiKey}`;
42+
}
43+
44+
const guardrails = await fetchGuardrails(baseURL, headers);
45+
46+
res.json({ guardrails });
47+
} catch (error) {
48+
logger.error('[GuardrailsRoute] Error fetching guardrails:', error);
49+
res.status(500).json({ error: 'Failed to fetch guardrails' });
50+
}
51+
});
52+
53+
module.exports = router;

api/server/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ const edit = require('./edit');
2727
const keys = require('./keys');
2828
const user = require('./user');
2929
const mcp = require('./mcp');
30+
const guardrails = require('./guardrails');
3031

3132
module.exports = {
3233
mcp,
34+
guardrails,
3335
edit,
3436
auth,
3537
keys,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { logger } = require('~/utils');
2+
3+
/**
4+
* Fetches available guardrails from LiteLLM endpoint
5+
* @param {string} baseURL - The LiteLLM base URL
6+
* @param {Object} headers - Request headers
7+
* @returns {Promise<Array>} Array of available guardrails
8+
*/
9+
async function fetchGuardrails(baseURL, headers = {}) {
10+
try {
11+
const response = await fetch(`${baseURL}/guardrails/list`, {
12+
method: 'GET',
13+
headers: {
14+
'Content-Type': 'application/json',
15+
...headers,
16+
},
17+
});
18+
19+
if (!response.ok) {
20+
logger.warn('[GuardrailsService] Failed to fetch guardrails:', response.status, response.statusText);
21+
return [];
22+
}
23+
24+
const data = await response.json();
25+
return data.guardrails || [];
26+
} catch (error) {
27+
logger.error('[GuardrailsService] Error fetching guardrails:', error);
28+
return [];
29+
}
30+
}
31+
32+
module.exports = {
33+
fetchGuardrails,
34+
};

client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ import React, { useMemo } from 'react';
22
import type { ModelSelectorProps } from '~/common';
33
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
44
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
5-
import {
6-
renderModelSpecs,
7-
renderEndpoints,
8-
renderSearchResults,
9-
renderCustomGroups,
10-
} from './components';
5+
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
116
import { getSelectedIcon, getDisplayValue } from './utils';
127
import { CustomMenu as Menu } from './CustomMenu';
138
import DialogManager from './DialogManager';
149
import { useLocalize } from '~/hooks';
10+
import GuardrailsSelect from '~/components/Input/GuardrailsSelect';
11+
1512

1613
function ModelSelectorContent() {
1714
const localize = useLocalize();
@@ -81,6 +78,7 @@ function ModelSelectorContent() {
8178
endpoint: values.endpoint || '',
8279
model: values.model || '',
8380
modelSpec: values.modelSpec || '',
81+
guardrails: values.guardrails || [],
8482
});
8583
}}
8684
onSearch={(value) => setSearchValue(value)}
@@ -91,18 +89,34 @@ function ModelSelectorContent() {
9189
renderSearchResults(searchResults, localize, searchValue)
9290
) : (
9391
<>
94-
{/* Render ungrouped modelSpecs (no group field) */}
95-
{renderModelSpecs(
96-
modelSpecs?.filter((spec) => !spec.group) || [],
97-
selectedValues.modelSpec || '',
98-
)}
99-
{/* Render endpoints (will include grouped specs matching endpoint names) */}
92+
{renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')}
10093
{renderEndpoints(mappedEndpoints ?? [])}
101-
{/* Render custom groups (specs with group field not matching any endpoint) */}
102-
{renderCustomGroups(modelSpecs || [], mappedEndpoints ?? [])}
94+
{/* Guardrails section - only show when a model is selected */}
95+
{selectedValues.model && (
96+
<div className="border-t border-gray-200 dark:border-gray-600">
97+
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400">
98+
Guardrails
99+
</div>
100+
<GuardrailsSelect
101+
conversation={{
102+
guardrails: selectedValues.guardrails || []
103+
}}
104+
setOption={(key) => (value) => {
105+
console.log('setOption called with:', key, value);
106+
setSelectedValues({
107+
...selectedValues,
108+
[key]: value
109+
});
110+
}}
111+
showAbove={false}
112+
/>
113+
</div>
114+
)}
103115
</>
104116
)}
105117
</Menu>
118+
119+
106120
<DialogManager
107121
keyDialogOpen={keyDialogOpen}
108122
onOpenChange={onOpenChange}

0 commit comments

Comments
 (0)