From c68cc0a550033a4369a4e6ffc523ab3bac4a0559 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 25 May 2025 23:40:37 -0400 Subject: [PATCH] =?UTF-8?q?=E2=8C=9A=20fix:=20Debounce=20`setUserContext`?= =?UTF-8?q?=20and=20Default=20State=20Param=20for=20OpenID=20Auth=20(#7559?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- api/app/clients/AnthropicClient.js | 12 ++--- api/app/clients/specs/AnthropicClient.test.js | 28 ++++++++++ api/server/cleanup.js | 3 -- api/server/index.spec.js | 4 ++ api/server/routes/oauth.js | 13 ++--- api/strategies/openidStrategy.js | 7 +++ .../SidePanel/Agents/ModelPanel.tsx | 6 +-- .../components/SidePanel/Parameters/Panel.tsx | 6 +-- client/src/hooks/AuthContext.tsx | 51 ++++++++++--------- .../data-provider/src/parameterSettings.ts | 8 +-- 10 files changed, 90 insertions(+), 48 deletions(-) diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js index 2b832f2e0c..0da331ced5 100644 --- a/api/app/clients/AnthropicClient.js +++ b/api/app/clients/AnthropicClient.js @@ -74,9 +74,6 @@ class AnthropicClient extends BaseClient { /** Whether to use Messages API or Completions API * @type {boolean} */ 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 * @type {boolean} */ this.supportsCacheControl; @@ -118,13 +115,16 @@ class AnthropicClient extends BaseClient { const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic); this.isClaudeLatest = /claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch); - this.isLegacyOutput = !( - /claude-3[-.]5-sonnet/.test(modelMatch) || /claude-3[-.]7/.test(modelMatch) + const isLegacyOutput = !( + /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); if ( - this.isLegacyOutput && + isLegacyOutput && this.modelOptions.maxOutputTokens && this.modelOptions.maxOutputTokens > legacy.maxOutputTokens.default ) { diff --git a/api/app/clients/specs/AnthropicClient.test.js b/api/app/clients/specs/AnthropicClient.test.js index 5f331e8d3c..9867859087 100644 --- a/api/app/clients/specs/AnthropicClient.test.js +++ b/api/app/clients/specs/AnthropicClient.test.js @@ -514,6 +514,34 @@ describe('AnthropicClient', () => { 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', () => { const client = new AnthropicClient('test-api-key'); const highTokenValue = anthropicSettings.legacy.maxOutputTokens.default * 2; diff --git a/api/server/cleanup.js b/api/server/cleanup.js index 93ae8d805e..5bf336eed5 100644 --- a/api/server/cleanup.js +++ b/api/server/cleanup.js @@ -140,9 +140,6 @@ function disposeClient(client) { if (client.useMessages !== undefined) { client.useMessages = null; } - if (client.isLegacyOutput !== undefined) { - client.isLegacyOutput = null; - } if (client.supportsCacheControl !== undefined) { client.supportsCacheControl = null; } diff --git a/api/server/index.spec.js b/api/server/index.spec.js index 493229c2f4..25b5ab9f03 100644 --- a/api/server/index.spec.js +++ b/api/server/index.spec.js @@ -4,6 +4,10 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); const mongoose = require('mongoose'); +jest.mock('~/server/services/Config/loadCustomConfig', () => { + return jest.fn(() => Promise.resolve({})); +}); + describe('Server Configuration', () => { // Increase the default timeout to allow for Mongo cleanup jest.setTimeout(30_000); diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index 2336ac023d..9915390a5d 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -1,6 +1,7 @@ // file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware const express = require('express'); const passport = require('passport'); +const { randomState } = require('openid-client'); const { checkBan, logHeaders, @@ -9,8 +10,8 @@ const { checkDomainAllowed, } = require('~/server/middleware'); const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService'); -const { logger } = require('~/config'); const { isEnabled } = require('~/server/utils'); +const { logger } = require('~/config'); const router = express.Router(); @@ -103,12 +104,12 @@ router.get( /** * OpenID Routes */ -router.get( - '/openid', - passport.authenticate('openid', { +router.get('/openid', (req, res, next) => { + return passport.authenticate('openid', { session: false, - }), -); + state: randomState(), + })(req, res, next); +}); router.get( '/openid/callback', diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 92b225b20a..ea109358d7 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -28,6 +28,13 @@ class CustomOpenIDStrategy extends OpenIDStrategy { const hostAndProtocol = process.env.DOMAIN_SERVER; 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; + } } /** diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx index 234cccbaad..9b4b12cf67 100644 --- a/client/src/components/SidePanel/Agents/ModelPanel.tsx +++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx @@ -2,10 +2,10 @@ import React, { useMemo, useEffect } from 'react'; import { ChevronLeft, RotateCcw } from 'lucide-react'; import { useFormContext, useWatch, Controller } from 'react-hook-form'; import { - getSettingsKeys, alternateName, - agentParamSettings, + getSettingsKeys, SettingDefinition, + agentParamSettings, } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; @@ -211,7 +211,7 @@ export default function ModelPanel({ {/* Model Parameters */} {parameters && (
-
+
{/* 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 */} {parameters.map((setting) => { diff --git a/client/src/components/SidePanel/Parameters/Panel.tsx b/client/src/components/SidePanel/Parameters/Panel.tsx index 6217228b51..64742a7167 100644 --- a/client/src/components/SidePanel/Parameters/Panel.tsx +++ b/client/src/components/SidePanel/Parameters/Panel.tsx @@ -2,10 +2,10 @@ import { RotateCcw } from 'lucide-react'; import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { excludedKeys, - getSettingsKeys, - tConvoUpdateSchema, paramSettings, + getSettingsKeys, SettingDefinition, + tConvoUpdateSchema, } from 'librechat-data-provider'; import type { TPreset } from 'librechat-data-provider'; import { SaveAsPresetDialog } from '~/components/Endpoints'; @@ -140,7 +140,7 @@ export default function Parameters() { return (
-
+
{' '} {/* 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 */} diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index e21d19ebf1..d9d583783a 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -1,4 +1,5 @@ import { + useRef, useMemo, useState, useEffect, @@ -6,10 +7,10 @@ import { useContext, useCallback, createContext, - useRef, } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { debounce } from 'lodash'; import { useRecoilState } from 'recoil'; +import { useNavigate } from 'react-router-dom'; import { setTokenHeader, SystemRoles } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import { @@ -47,27 +48,31 @@ const AuthContextProvider = ({ const navigate = useNavigate(); - const setUserContext = useCallback( - (userContext: TUserContext) => { - const { token, isAuthenticated, user, redirect } = userContext; - setUser(user); - setToken(token); - //@ts-ignore - ok for token to be undefined initially - setTokenHeader(token); - setIsAuthenticated(isAuthenticated); - // Use a custom redirect if set - const finalRedirect = logoutRedirectRef.current || redirect; - // Clear the stored redirect - logoutRedirectRef.current = undefined; - if (finalRedirect == null) { - return; - } - if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) { - window.location.href = finalRedirect; - } else { - navigate(finalRedirect, { replace: true }); - } - }, + const setUserContext = useMemo( + () => + debounce((userContext: TUserContext) => { + const { token, isAuthenticated, user, redirect } = userContext; + setUser(user); + setToken(token); + //@ts-ignore - ok for token to be undefined initially + setTokenHeader(token); + setIsAuthenticated(isAuthenticated); + + // Use a custom redirect if set + const finalRedirect = logoutRedirectRef.current || redirect; + // Clear the stored redirect + logoutRedirectRef.current = undefined; + + if (finalRedirect == null) { + return; + } + + if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) { + window.location.href = finalRedirect; + } else { + navigate(finalRedirect, { replace: true }); + } + }, 50), [navigate, setUser], ); const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index 88fc61e200..1098bbbb79 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -682,9 +682,9 @@ const bedrockGeneralColumns = { export const presetSettings: Record< string, | { - col1: SettingsConfiguration; - col2: SettingsConfiguration; -} + col1: SettingsConfiguration; + col2: SettingsConfiguration; + } | undefined > = { [EModelEndpoint.openAI]: openAIColumns, @@ -723,4 +723,4 @@ export const agentParamSettings: Record