mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 15:08:10 +01:00
238 lines
6.5 KiB
JavaScript
238 lines
6.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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);
|
||
|
|
});
|
||
|
|
});
|