mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 15:08:10 +01:00
🎭 feat: Override Custom Endpoint Schema with Specified Params Endpoint (#11788)
* 🔧 refactor: Simplify payload parsing and enhance getSaveOptions logic - Removed unused bedrockInputSchema from payloadParser, streamlining the function. - Updated payloadParser to handle optional chaining for model parameters. - Enhanced getSaveOptions to ensure runOptions defaults to an empty object if parsing fails, improving robustness. - Adjusted the assignment of maxContextTokens to use the instance variable for consistency. * 🔧 fix: Update maxContextTokens assignment logic in initializeAgent function - Enhanced the maxContextTokens assignment to allow for user-defined values, ensuring it defaults to a calculated value only when not provided or invalid. This change improves flexibility in agent initialization. * 🧪 test: Add unit tests for initializeAgent function - Introduced comprehensive unit tests for the initializeAgent function, focusing on maxContextTokens behavior. - Tests cover scenarios for user-defined values, fallback calculations, and edge cases such as zero and negative values, enhancing overall test coverage and reliability of agent initialization logic. * refactor: default params Endpoint Configuration Handling - Integrated `getEndpointsConfig` to fetch endpoint configurations, allowing for dynamic handling of `defaultParamsEndpoint`. - Updated `buildEndpointOption` to pass `defaultParamsEndpoint` to `parseCompactConvo`, ensuring correct parameter handling based on endpoint type. - Added comprehensive unit tests for `buildDefaultConvo` and `cleanupPreset` to validate behavior with `defaultParamsEndpoint`, covering various scenarios and edge cases. - Refactored related hooks and utility functions to support the new configuration structure, improving overall flexibility and maintainability. * refactor: Centralize defaultParamsEndpoint retrieval - Introduced `getDefaultParamsEndpoint` function to streamline the retrieval of `defaultParamsEndpoint` across various hooks and middleware. - Updated multiple files to utilize the new function, enhancing code consistency and maintainability. - Removed redundant logic for fetching `defaultParamsEndpoint`, simplifying the codebase.
This commit is contained in:
parent
6cc6ee3207
commit
467df0f07a
19 changed files with 1234 additions and 45 deletions
|
|
@ -5,9 +5,11 @@ const {
|
|||
EModelEndpoint,
|
||||
isAgentsEndpoint,
|
||||
parseCompactConvo,
|
||||
getDefaultParamsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
|
||||
const assistants = require('~/server/services/Endpoints/assistants');
|
||||
const { getEndpointsConfig } = require('~/server/services/Config');
|
||||
const agents = require('~/server/services/Endpoints/agents');
|
||||
const { updateFilesUsage } = require('~/models');
|
||||
|
||||
|
|
@ -19,9 +21,24 @@ const buildFunction = {
|
|||
|
||||
async function buildEndpointOption(req, res, next) {
|
||||
const { endpoint, endpointType } = req.body;
|
||||
|
||||
let endpointsConfig;
|
||||
try {
|
||||
endpointsConfig = await getEndpointsConfig(req);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching endpoints config in buildEndpointOption', error);
|
||||
}
|
||||
|
||||
const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint);
|
||||
|
||||
let parsedBody;
|
||||
try {
|
||||
parsedBody = parseCompactConvo({ endpoint, endpointType, conversation: req.body });
|
||||
parsedBody = parseCompactConvo({
|
||||
endpoint,
|
||||
endpointType,
|
||||
conversation: req.body,
|
||||
defaultParamsEndpoint,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error parsing compact conversation for endpoint ${endpoint}`, error);
|
||||
logger.debug({
|
||||
|
|
@ -55,6 +72,7 @@ async function buildEndpointOption(req, res, next) {
|
|||
endpoint,
|
||||
endpointType,
|
||||
conversation: currentModelSpec.preset,
|
||||
defaultParamsEndpoint,
|
||||
});
|
||||
if (currentModelSpec.iconURL != null && currentModelSpec.iconURL !== '') {
|
||||
parsedBody.iconURL = currentModelSpec.iconURL;
|
||||
|
|
|
|||
237
api/server/middleware/buildEndpointOption.spec.js
Normal file
237
api/server/middleware/buildEndpointOption.spec.js
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Wrap parseCompactConvo: the REAL function runs, but jest can observe
|
||||
* calls and return values. Must be declared before require('./buildEndpointOption')
|
||||
* so the destructured reference in the middleware captures the wrapper.
|
||||
*/
|
||||
jest.mock('librechat-data-provider', () => {
|
||||
const actual = jest.requireActual('librechat-data-provider');
|
||||
return {
|
||||
...actual,
|
||||
parseCompactConvo: jest.fn((...args) => actual.parseCompactConvo(...args)),
|
||||
};
|
||||
});
|
||||
|
||||
const { EModelEndpoint, parseCompactConvo } = require('librechat-data-provider');
|
||||
|
||||
const mockBuildOptions = jest.fn((_endpoint, parsedBody) => ({
|
||||
...parsedBody,
|
||||
endpoint: _endpoint,
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Endpoints/azureAssistants', () => ({
|
||||
buildOptions: mockBuildOptions,
|
||||
}));
|
||||
jest.mock('~/server/services/Endpoints/assistants', () => ({
|
||||
buildOptions: mockBuildOptions,
|
||||
}));
|
||||
jest.mock('~/server/services/Endpoints/agents', () => ({
|
||||
buildOptions: mockBuildOptions,
|
||||
}));
|
||||
|
||||
jest.mock('~/models', () => ({
|
||||
updateFilesUsage: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetEndpointsConfig = jest.fn();
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
handleError: jest.fn(),
|
||||
}));
|
||||
|
||||
const buildEndpointOption = require('./buildEndpointOption');
|
||||
|
||||
const createReq = (body, config = {}) => ({
|
||||
body,
|
||||
config,
|
||||
baseUrl: '/api/chat',
|
||||
});
|
||||
|
||||
const createRes = () => ({
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
});
|
||||
|
||||
describe('buildEndpointOption - defaultParamsEndpoint parsing', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should pass defaultParamsEndpoint to parseCompactConvo and preserve maxOutputTokens', async () => {
|
||||
mockGetEndpointsConfig.mockResolvedValue({
|
||||
AnthropicClaude: {
|
||||
type: EModelEndpoint.custom,
|
||||
customParams: {
|
||||
defaultParamsEndpoint: EModelEndpoint.anthropic,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const req = createReq(
|
||||
{
|
||||
endpoint: 'AnthropicClaude',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
model: 'anthropic/claude-opus-4.5',
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 8192,
|
||||
topP: 0.9,
|
||||
maxContextTokens: 50000,
|
||||
},
|
||||
{ modelSpecs: null },
|
||||
);
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
expect(parseCompactConvo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultParamsEndpoint: EModelEndpoint.anthropic,
|
||||
}),
|
||||
);
|
||||
|
||||
const parsedResult = parseCompactConvo.mock.results[0].value;
|
||||
expect(parsedResult.maxOutputTokens).toBe(8192);
|
||||
expect(parsedResult.topP).toBe(0.9);
|
||||
expect(parsedResult.temperature).toBe(0.7);
|
||||
expect(parsedResult.maxContextTokens).toBe(50000);
|
||||
});
|
||||
|
||||
it('should strip maxOutputTokens when no defaultParamsEndpoint is configured', async () => {
|
||||
mockGetEndpointsConfig.mockResolvedValue({
|
||||
MyOpenRouter: {
|
||||
type: EModelEndpoint.custom,
|
||||
},
|
||||
});
|
||||
|
||||
const req = createReq(
|
||||
{
|
||||
endpoint: 'MyOpenRouter',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
model: 'gpt-4o',
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 8192,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
{ modelSpecs: null },
|
||||
);
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
expect(parseCompactConvo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultParamsEndpoint: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const parsedResult = parseCompactConvo.mock.results[0].value;
|
||||
expect(parsedResult.maxOutputTokens).toBeUndefined();
|
||||
expect(parsedResult.max_tokens).toBe(4096);
|
||||
expect(parsedResult.temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it('should strip bedrock region from custom endpoint without defaultParamsEndpoint', async () => {
|
||||
mockGetEndpointsConfig.mockResolvedValue({
|
||||
MyEndpoint: {
|
||||
type: EModelEndpoint.custom,
|
||||
},
|
||||
});
|
||||
|
||||
const req = createReq(
|
||||
{
|
||||
endpoint: 'MyEndpoint',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
model: 'gpt-4o',
|
||||
temperature: 0.7,
|
||||
region: 'us-east-1',
|
||||
},
|
||||
{ modelSpecs: null },
|
||||
);
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
const parsedResult = parseCompactConvo.mock.results[0].value;
|
||||
expect(parsedResult.region).toBeUndefined();
|
||||
expect(parsedResult.temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it('should pass defaultParamsEndpoint when re-parsing enforced model spec', async () => {
|
||||
mockGetEndpointsConfig.mockResolvedValue({
|
||||
AnthropicClaude: {
|
||||
type: EModelEndpoint.custom,
|
||||
customParams: {
|
||||
defaultParamsEndpoint: EModelEndpoint.anthropic,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const modelSpec = {
|
||||
name: 'claude-opus-4.5',
|
||||
preset: {
|
||||
endpoint: 'AnthropicClaude',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
model: 'anthropic/claude-opus-4.5',
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 8192,
|
||||
maxContextTokens: 50000,
|
||||
},
|
||||
};
|
||||
|
||||
const req = createReq(
|
||||
{
|
||||
endpoint: 'AnthropicClaude',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
spec: 'claude-opus-4.5',
|
||||
model: 'anthropic/claude-opus-4.5',
|
||||
},
|
||||
{
|
||||
modelSpecs: {
|
||||
enforce: true,
|
||||
list: [modelSpec],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
const enforcedCall = parseCompactConvo.mock.calls[1];
|
||||
expect(enforcedCall[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
defaultParamsEndpoint: EModelEndpoint.anthropic,
|
||||
}),
|
||||
);
|
||||
|
||||
const enforcedResult = parseCompactConvo.mock.results[1].value;
|
||||
expect(enforcedResult.maxOutputTokens).toBe(8192);
|
||||
expect(enforcedResult.temperature).toBe(0.7);
|
||||
expect(enforcedResult.maxContextTokens).toBe(50000);
|
||||
});
|
||||
|
||||
it('should fall back to OpenAI schema when getEndpointsConfig fails', async () => {
|
||||
mockGetEndpointsConfig.mockRejectedValue(new Error('Config unavailable'));
|
||||
|
||||
const req = createReq(
|
||||
{
|
||||
endpoint: 'AnthropicClaude',
|
||||
endpointType: EModelEndpoint.custom,
|
||||
model: 'anthropic/claude-opus-4.5',
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 8192,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
{ modelSpecs: null },
|
||||
);
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
expect(parseCompactConvo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultParamsEndpoint: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const parsedResult = parseCompactConvo.mock.results[0].value;
|
||||
expect(parsedResult.maxOutputTokens).toBeUndefined();
|
||||
expect(parsedResult.max_tokens).toBe(4096);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue