mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
⌚ fix: Debounce setUserContext
and Default State Param for OpenID Auth (#7559)
* fix: Add default random state parameter to OpenID auth request for providers that require it; ensure passport strategy uses it
* ⌚ refactor: debounce setUserContext to avoid race condition
* refactor: Update OpenID authentication to use randomState from openid-client
* chore: linting in presetSettings type definition
* chore: import order in ModelPanel
* refactor: remove `isLegacyOutput` property from AnthropicClient since only used where defined, add latest models to non-legacy patterns, and remove from client cleanup
* refactor: adjust grid layout in Parameters component for improved responsiveness
* refactor: adjust grid layout in ModelPanel for improved display of model parameters
* test: add cases for maxOutputTokens handling in Claude 4 Sonnet and Opus models
* ci: mock loadCustomConfig in server tests and refactor OpenID route for improved authentication handling
This commit is contained in:
parent
deb8a00e27
commit
c68cc0a550
10 changed files with 90 additions and 48 deletions
|
@ -74,9 +74,6 @@ class AnthropicClient extends BaseClient {
|
||||||
/** Whether to use Messages API or Completions API
|
/** Whether to use Messages API or Completions API
|
||||||
* @type {boolean} */
|
* @type {boolean} */
|
||||||
this.useMessages;
|
this.useMessages;
|
||||||
/** Whether or not the model is limited to the legacy amount of output tokens
|
|
||||||
* @type {boolean} */
|
|
||||||
this.isLegacyOutput;
|
|
||||||
/** Whether or not the model supports Prompt Caching
|
/** Whether or not the model supports Prompt Caching
|
||||||
* @type {boolean} */
|
* @type {boolean} */
|
||||||
this.supportsCacheControl;
|
this.supportsCacheControl;
|
||||||
|
@ -118,13 +115,16 @@ class AnthropicClient extends BaseClient {
|
||||||
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
|
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
|
||||||
this.isClaudeLatest =
|
this.isClaudeLatest =
|
||||||
/claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch);
|
/claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch);
|
||||||
this.isLegacyOutput = !(
|
const isLegacyOutput = !(
|
||||||
/claude-3[-.]5-sonnet/.test(modelMatch) || /claude-3[-.]7/.test(modelMatch)
|
/claude-3[-.]5-sonnet/.test(modelMatch) ||
|
||||||
|
/claude-3[-.]7/.test(modelMatch) ||
|
||||||
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) ||
|
||||||
|
/claude-[4-9]/.test(modelMatch)
|
||||||
);
|
);
|
||||||
this.supportsCacheControl = this.options.promptCache && checkPromptCacheSupport(modelMatch);
|
this.supportsCacheControl = this.options.promptCache && checkPromptCacheSupport(modelMatch);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.isLegacyOutput &&
|
isLegacyOutput &&
|
||||||
this.modelOptions.maxOutputTokens &&
|
this.modelOptions.maxOutputTokens &&
|
||||||
this.modelOptions.maxOutputTokens > legacy.maxOutputTokens.default
|
this.modelOptions.maxOutputTokens > legacy.maxOutputTokens.default
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -514,6 +514,34 @@ describe('AnthropicClient', () => {
|
||||||
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not cap maxOutputTokens for Claude 4 Sonnet models', () => {
|
||||||
|
const client = new AnthropicClient('test-api-key');
|
||||||
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 10; // 40,960 tokens
|
||||||
|
|
||||||
|
client.setOptions({
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-sonnet-4-20250514',
|
||||||
|
maxOutputTokens: highTokenValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cap maxOutputTokens for Claude 4 Opus models', () => {
|
||||||
|
const client = new AnthropicClient('test-api-key');
|
||||||
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 6; // 24,576 tokens (under 32K limit)
|
||||||
|
|
||||||
|
client.setOptions({
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-opus-4-20250514',
|
||||||
|
maxOutputTokens: highTokenValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.modelOptions.maxOutputTokens).toBe(highTokenValue);
|
||||||
|
});
|
||||||
|
|
||||||
it('should cap maxOutputTokens for Claude 3.5 Haiku models', () => {
|
it('should cap maxOutputTokens for Claude 3.5 Haiku models', () => {
|
||||||
const client = new AnthropicClient('test-api-key');
|
const client = new AnthropicClient('test-api-key');
|
||||||
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2;
|
const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2;
|
||||||
|
|
|
@ -140,9 +140,6 @@ function disposeClient(client) {
|
||||||
if (client.useMessages !== undefined) {
|
if (client.useMessages !== undefined) {
|
||||||
client.useMessages = null;
|
client.useMessages = null;
|
||||||
}
|
}
|
||||||
if (client.isLegacyOutput !== undefined) {
|
|
||||||
client.isLegacyOutput = null;
|
|
||||||
}
|
|
||||||
if (client.supportsCacheControl !== undefined) {
|
if (client.supportsCacheControl !== undefined) {
|
||||||
client.supportsCacheControl = null;
|
client.supportsCacheControl = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ const request = require('supertest');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config/loadCustomConfig', () => {
|
||||||
|
return jest.fn(() => Promise.resolve({}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('Server Configuration', () => {
|
describe('Server Configuration', () => {
|
||||||
// Increase the default timeout to allow for Mongo cleanup
|
// Increase the default timeout to allow for Mongo cleanup
|
||||||
jest.setTimeout(30_000);
|
jest.setTimeout(30_000);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
const { randomState } = require('openid-client');
|
||||||
const {
|
const {
|
||||||
checkBan,
|
checkBan,
|
||||||
logHeaders,
|
logHeaders,
|
||||||
|
@ -9,8 +10,8 @@ const {
|
||||||
checkDomainAllowed,
|
checkDomainAllowed,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { logger } = require('~/config');
|
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@ -103,12 +104,12 @@ router.get(
|
||||||
/**
|
/**
|
||||||
* OpenID Routes
|
* OpenID Routes
|
||||||
*/
|
*/
|
||||||
router.get(
|
router.get('/openid', (req, res, next) => {
|
||||||
'/openid',
|
return passport.authenticate('openid', {
|
||||||
passport.authenticate('openid', {
|
|
||||||
session: false,
|
session: false,
|
||||||
}),
|
state: randomState(),
|
||||||
);
|
})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/openid/callback',
|
'/openid/callback',
|
||||||
|
|
|
@ -28,6 +28,13 @@ class CustomOpenIDStrategy extends OpenIDStrategy {
|
||||||
const hostAndProtocol = process.env.DOMAIN_SERVER;
|
const hostAndProtocol = process.env.DOMAIN_SERVER;
|
||||||
return new URL(`${hostAndProtocol}${req.originalUrl ?? req.url}`);
|
return new URL(`${hostAndProtocol}${req.originalUrl ?? req.url}`);
|
||||||
}
|
}
|
||||||
|
authorizationRequestParams(req, options) {
|
||||||
|
const params = super.authorizationRequestParams(req, options);
|
||||||
|
if (options?.state && !params.has('state')) {
|
||||||
|
params.set('state', options.state);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,10 @@ import React, { useMemo, useEffect } from 'react';
|
||||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
getSettingsKeys,
|
|
||||||
alternateName,
|
alternateName,
|
||||||
agentParamSettings,
|
getSettingsKeys,
|
||||||
SettingDefinition,
|
SettingDefinition,
|
||||||
|
agentParamSettings,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||||
|
@ -211,7 +211,7 @@ export default function ModelPanel({
|
||||||
{/* Model Parameters */}
|
{/* Model Parameters */}
|
||||||
{parameters && (
|
{parameters && (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* This is the parent element containing all settings */}
|
{/* This is the parent element containing all settings */}
|
||||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||||
{parameters.map((setting) => {
|
{parameters.map((setting) => {
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { RotateCcw } from 'lucide-react';
|
||||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
excludedKeys,
|
excludedKeys,
|
||||||
getSettingsKeys,
|
|
||||||
tConvoUpdateSchema,
|
|
||||||
paramSettings,
|
paramSettings,
|
||||||
|
getSettingsKeys,
|
||||||
SettingDefinition,
|
SettingDefinition,
|
||||||
|
tConvoUpdateSchema,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
import { SaveAsPresetDialog } from '~/components/Endpoints';
|
import { SaveAsPresetDialog } from '~/components/Endpoints';
|
||||||
|
@ -140,7 +140,7 @@ export default function Parameters() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{' '}
|
{' '}
|
||||||
{/* This is the parent element containing all settings */}
|
{/* This is the parent element containing all settings */}
|
||||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -6,10 +7,10 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
createContext,
|
createContext,
|
||||||
useRef,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { debounce } from 'lodash';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { setTokenHeader, SystemRoles } from 'librechat-data-provider';
|
import { setTokenHeader, SystemRoles } from 'librechat-data-provider';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
@ -47,27 +48,31 @@ const AuthContextProvider = ({
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const setUserContext = useCallback(
|
const setUserContext = useMemo(
|
||||||
(userContext: TUserContext) => {
|
() =>
|
||||||
const { token, isAuthenticated, user, redirect } = userContext;
|
debounce((userContext: TUserContext) => {
|
||||||
setUser(user);
|
const { token, isAuthenticated, user, redirect } = userContext;
|
||||||
setToken(token);
|
setUser(user);
|
||||||
//@ts-ignore - ok for token to be undefined initially
|
setToken(token);
|
||||||
setTokenHeader(token);
|
//@ts-ignore - ok for token to be undefined initially
|
||||||
setIsAuthenticated(isAuthenticated);
|
setTokenHeader(token);
|
||||||
// Use a custom redirect if set
|
setIsAuthenticated(isAuthenticated);
|
||||||
const finalRedirect = logoutRedirectRef.current || redirect;
|
|
||||||
// Clear the stored redirect
|
// Use a custom redirect if set
|
||||||
logoutRedirectRef.current = undefined;
|
const finalRedirect = logoutRedirectRef.current || redirect;
|
||||||
if (finalRedirect == null) {
|
// Clear the stored redirect
|
||||||
return;
|
logoutRedirectRef.current = undefined;
|
||||||
}
|
|
||||||
if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) {
|
if (finalRedirect == null) {
|
||||||
window.location.href = finalRedirect;
|
return;
|
||||||
} else {
|
}
|
||||||
navigate(finalRedirect, { replace: true });
|
|
||||||
}
|
if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) {
|
||||||
},
|
window.location.href = finalRedirect;
|
||||||
|
} else {
|
||||||
|
navigate(finalRedirect, { replace: true });
|
||||||
|
}
|
||||||
|
}, 50),
|
||||||
[navigate, setUser],
|
[navigate, setUser],
|
||||||
);
|
);
|
||||||
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
||||||
|
|
|
@ -682,9 +682,9 @@ const bedrockGeneralColumns = {
|
||||||
export const presetSettings: Record<
|
export const presetSettings: Record<
|
||||||
string,
|
string,
|
||||||
| {
|
| {
|
||||||
col1: SettingsConfiguration;
|
col1: SettingsConfiguration;
|
||||||
col2: SettingsConfiguration;
|
col2: SettingsConfiguration;
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
> = {
|
> = {
|
||||||
[EModelEndpoint.openAI]: openAIColumns,
|
[EModelEndpoint.openAI]: openAIColumns,
|
||||||
|
@ -723,4 +723,4 @@ export const agentParamSettings: Record<string, SettingsConfiguration | undefine
|
||||||
acc[key] = value.col2;
|
acc[key] = value.col2;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue