mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 23:18:09 +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
284
packages/api/src/agents/__tests__/initialize.test.ts
Normal file
284
packages/api/src/agents/__tests__/initialize.test.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { Providers } from '@librechat/agents';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
import type { ServerRequest, InitializeResultBase } from '~/types';
|
||||
import type { InitializeAgentDbMethods } from '../initialize';
|
||||
|
||||
// Mock logger
|
||||
jest.mock('winston', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
})),
|
||||
format: {
|
||||
combine: jest.fn(),
|
||||
colorize: jest.fn(),
|
||||
simple: jest.fn(),
|
||||
},
|
||||
transports: {
|
||||
Console: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockExtractLibreChatParams = jest.fn();
|
||||
const mockGetModelMaxTokens = jest.fn();
|
||||
const mockOptionalChainWithEmptyCheck = jest.fn();
|
||||
const mockGetThreadData = jest.fn();
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
extractLibreChatParams: (...args: unknown[]) => mockExtractLibreChatParams(...args),
|
||||
getModelMaxTokens: (...args: unknown[]) => mockGetModelMaxTokens(...args),
|
||||
optionalChainWithEmptyCheck: (...args: unknown[]) => mockOptionalChainWithEmptyCheck(...args),
|
||||
getThreadData: (...args: unknown[]) => mockGetThreadData(...args),
|
||||
}));
|
||||
|
||||
const mockGetProviderConfig = jest.fn();
|
||||
jest.mock('~/endpoints', () => ({
|
||||
getProviderConfig: (...args: unknown[]) => mockGetProviderConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/files', () => ({
|
||||
filterFilesByEndpointConfig: jest.fn(() => []),
|
||||
}));
|
||||
|
||||
jest.mock('~/prompts', () => ({
|
||||
generateArtifactsPrompt: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
jest.mock('../resources', () => ({
|
||||
primeResources: jest.fn().mockResolvedValue({
|
||||
attachments: [],
|
||||
tool_resources: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { initializeAgent } from '../initialize';
|
||||
|
||||
/**
|
||||
* Creates minimal mock objects for initializeAgent tests.
|
||||
*/
|
||||
function createMocks(overrides?: {
|
||||
maxContextTokens?: number;
|
||||
modelDefault?: number;
|
||||
maxOutputTokens?: number;
|
||||
}) {
|
||||
const { maxContextTokens, modelDefault = 200000, maxOutputTokens = 4096 } = overrides ?? {};
|
||||
|
||||
const agent = {
|
||||
id: 'agent-1',
|
||||
model: 'test-model',
|
||||
provider: Providers.OPENAI,
|
||||
tools: [],
|
||||
model_parameters: { model: 'test-model' },
|
||||
} as unknown as Agent;
|
||||
|
||||
const req = {
|
||||
user: { id: 'user-1' },
|
||||
config: {},
|
||||
} as unknown as ServerRequest;
|
||||
|
||||
const res = {} as unknown as import('express').Response;
|
||||
|
||||
const mockGetOptions = jest.fn().mockResolvedValue({
|
||||
llmConfig: {
|
||||
model: 'test-model',
|
||||
maxTokens: maxOutputTokens,
|
||||
},
|
||||
endpointTokenConfig: undefined,
|
||||
} satisfies InitializeResultBase);
|
||||
|
||||
mockGetProviderConfig.mockReturnValue({
|
||||
getOptions: mockGetOptions,
|
||||
overrideProvider: Providers.OPENAI,
|
||||
});
|
||||
|
||||
// extractLibreChatParams returns maxContextTokens when provided in model_parameters
|
||||
mockExtractLibreChatParams.mockReturnValue({
|
||||
resendFiles: false,
|
||||
maxContextTokens,
|
||||
modelOptions: { model: 'test-model' },
|
||||
});
|
||||
|
||||
// getModelMaxTokens returns the model's default context window
|
||||
mockGetModelMaxTokens.mockReturnValue(modelDefault);
|
||||
|
||||
// Implement real optionalChainWithEmptyCheck behavior
|
||||
mockOptionalChainWithEmptyCheck.mockImplementation(
|
||||
(...values: (string | number | undefined)[]) => {
|
||||
for (const v of values) {
|
||||
if (v !== undefined && v !== null && v !== '') {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return values[values.length - 1];
|
||||
},
|
||||
);
|
||||
|
||||
const loadTools = jest.fn().mockResolvedValue({
|
||||
tools: [],
|
||||
toolContextMap: {},
|
||||
userMCPAuthMap: undefined,
|
||||
toolRegistry: undefined,
|
||||
toolDefinitions: [],
|
||||
hasDeferredTools: false,
|
||||
});
|
||||
|
||||
const db: InitializeAgentDbMethods = {
|
||||
getFiles: jest.fn().mockResolvedValue([]),
|
||||
getConvoFiles: jest.fn().mockResolvedValue([]),
|
||||
updateFilesUsage: jest.fn().mockResolvedValue([]),
|
||||
getUserKey: jest.fn().mockResolvedValue('user-1'),
|
||||
getUserKeyValues: jest.fn().mockResolvedValue([]),
|
||||
getToolFilesByIds: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
return { agent, req, res, loadTools, db };
|
||||
}
|
||||
|
||||
describe('initializeAgent — maxContextTokens', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uses user-configured maxContextTokens when provided via model_parameters', async () => {
|
||||
const userValue = 50000;
|
||||
const { agent, req, res, loadTools, db } = createMocks({
|
||||
maxContextTokens: userValue,
|
||||
modelDefault: 200000,
|
||||
maxOutputTokens: 4096,
|
||||
});
|
||||
|
||||
const result = await initializeAgent(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
loadTools,
|
||||
endpointOption: {
|
||||
endpoint: EModelEndpoint.agents,
|
||||
model_parameters: { maxContextTokens: userValue },
|
||||
},
|
||||
allowedProviders: new Set([Providers.OPENAI]),
|
||||
isInitialAgent: true,
|
||||
},
|
||||
db,
|
||||
);
|
||||
|
||||
expect(result.maxContextTokens).toBe(userValue);
|
||||
});
|
||||
|
||||
it('falls back to formula when maxContextTokens is NOT provided', async () => {
|
||||
const modelDefault = 200000;
|
||||
const maxOutputTokens = 4096;
|
||||
const { agent, req, res, loadTools, db } = createMocks({
|
||||
maxContextTokens: undefined,
|
||||
modelDefault,
|
||||
maxOutputTokens,
|
||||
});
|
||||
|
||||
const result = await initializeAgent(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
loadTools,
|
||||
endpointOption: { endpoint: EModelEndpoint.agents },
|
||||
allowedProviders: new Set([Providers.OPENAI]),
|
||||
isInitialAgent: true,
|
||||
},
|
||||
db,
|
||||
);
|
||||
|
||||
const expected = Math.round((modelDefault - maxOutputTokens) * 0.9);
|
||||
expect(result.maxContextTokens).toBe(expected);
|
||||
});
|
||||
|
||||
it('falls back to formula when maxContextTokens is 0', async () => {
|
||||
const maxOutputTokens = 4096;
|
||||
const { agent, req, res, loadTools, db } = createMocks({
|
||||
maxContextTokens: 0,
|
||||
modelDefault: 200000,
|
||||
maxOutputTokens,
|
||||
});
|
||||
|
||||
const result = await initializeAgent(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
loadTools,
|
||||
endpointOption: {
|
||||
endpoint: EModelEndpoint.agents,
|
||||
model_parameters: { maxContextTokens: 0 },
|
||||
},
|
||||
allowedProviders: new Set([Providers.OPENAI]),
|
||||
isInitialAgent: true,
|
||||
},
|
||||
db,
|
||||
);
|
||||
|
||||
// 0 is not used as-is; the formula kicks in.
|
||||
// optionalChainWithEmptyCheck(0, 200000, 18000) returns 0 (not null/undefined),
|
||||
// then Number(0) || 18000 = 18000 (the fallback default).
|
||||
expect(result.maxContextTokens).not.toBe(0);
|
||||
const expected = Math.round((18000 - maxOutputTokens) * 0.9);
|
||||
expect(result.maxContextTokens).toBe(expected);
|
||||
});
|
||||
|
||||
it('falls back to formula when maxContextTokens is negative', async () => {
|
||||
const maxOutputTokens = 4096;
|
||||
const { agent, req, res, loadTools, db } = createMocks({
|
||||
maxContextTokens: -1,
|
||||
modelDefault: 200000,
|
||||
maxOutputTokens,
|
||||
});
|
||||
|
||||
const result = await initializeAgent(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
loadTools,
|
||||
endpointOption: {
|
||||
endpoint: EModelEndpoint.agents,
|
||||
model_parameters: { maxContextTokens: -1 },
|
||||
},
|
||||
allowedProviders: new Set([Providers.OPENAI]),
|
||||
isInitialAgent: true,
|
||||
},
|
||||
db,
|
||||
);
|
||||
|
||||
// -1 is not used as-is; the formula kicks in
|
||||
expect(result.maxContextTokens).not.toBe(-1);
|
||||
});
|
||||
|
||||
it('preserves small user-configured value (e.g. 1000 from modelSpec)', async () => {
|
||||
const userValue = 1000;
|
||||
const { agent, req, res, loadTools, db } = createMocks({
|
||||
maxContextTokens: userValue,
|
||||
modelDefault: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
});
|
||||
|
||||
const result = await initializeAgent(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
loadTools,
|
||||
endpointOption: {
|
||||
endpoint: EModelEndpoint.agents,
|
||||
model_parameters: { maxContextTokens: userValue },
|
||||
},
|
||||
allowedProviders: new Set([Providers.OPENAI]),
|
||||
isInitialAgent: true,
|
||||
},
|
||||
db,
|
||||
);
|
||||
|
||||
// Should NOT be overridden to Math.round((128000 - 4096) * 0.9) = 111,514
|
||||
expect(result.maxContextTokens).toBe(userValue);
|
||||
});
|
||||
});
|
||||
|
|
@ -413,7 +413,10 @@ export async function initializeAgent(
|
|||
toolContextMap: toolContextMap ?? {},
|
||||
useLegacyContent: !!options.useLegacyContent,
|
||||
tools: (tools ?? []) as GenericTool[] & string[],
|
||||
maxContextTokens: Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9),
|
||||
maxContextTokens:
|
||||
maxContextTokens != null && maxContextTokens > 0
|
||||
? maxContextTokens
|
||||
: Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9),
|
||||
};
|
||||
|
||||
return initializedAgent;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue