mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
📌 fix: Preserve Project Scope Through Enforced Model Specs (#13586)
This commit is contained in:
parent
209e8d1eb6
commit
4b699fb60f
7 changed files with 56 additions and 6 deletions
|
|
@ -78,7 +78,7 @@ async function buildEndpointOption(req, res, next) {
|
|||
try {
|
||||
const result = applyModelSpecPreset({
|
||||
modelSpec: currentModelSpec,
|
||||
parsedBody: currentModelSpec.preset,
|
||||
parsedBody,
|
||||
endpoint,
|
||||
endpointType,
|
||||
defaultParamsEndpoint,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,9 @@ describe('buildEndpointOption - defaultParamsEndpoint parsing', () => {
|
|||
endpointType: EModelEndpoint.custom,
|
||||
spec: 'claude-opus-4.5',
|
||||
model: 'anthropic/claude-opus-4.5',
|
||||
temperature: 0.1,
|
||||
topP: 0.2,
|
||||
chatProjectId: 'project-1',
|
||||
},
|
||||
{
|
||||
modelSpecs: {
|
||||
|
|
@ -197,6 +200,7 @@ describe('buildEndpointOption - defaultParamsEndpoint parsing', () => {
|
|||
},
|
||||
},
|
||||
);
|
||||
req.baseUrl = '/api/agents/chat';
|
||||
|
||||
await buildEndpointOption(req, createRes(), jest.fn());
|
||||
|
||||
|
|
@ -210,7 +214,10 @@ describe('buildEndpointOption - defaultParamsEndpoint parsing', () => {
|
|||
const enforcedResult = parseCompactConvo.mock.results[1].value;
|
||||
expect(enforcedResult.maxOutputTokens).toBe(8192);
|
||||
expect(enforcedResult.temperature).toBe(0.7);
|
||||
expect(enforcedResult.topP).toBeUndefined();
|
||||
expect(enforcedResult.maxContextTokens).toBe(50000);
|
||||
expect(enforcedResult.chatProjectId).toBe('project-1');
|
||||
expect(req.body.endpointOption.chatProjectId).toBe('project-1');
|
||||
});
|
||||
|
||||
it('should restore private model spec preset fields in non-enforced mode', async () => {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ endpoints:
|
|||
modelDisplayLabel: 'Mock Provider B'
|
||||
|
||||
modelSpecs:
|
||||
prioritize: true
|
||||
enforce: true
|
||||
list:
|
||||
- name: 'e2e-mock-provider-a'
|
||||
label: 'Mock Provider A'
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ test.describe('chat projects', () => {
|
|||
await expect(page.getByRole('button', { name }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('starts a project-scoped chat and files it under the project', async ({ page }) => {
|
||||
test('starts a project-scoped chat and persists it under the project', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
const name = uniqueName('E2E Project');
|
||||
const projectId = await createProject(page, name);
|
||||
|
|
@ -68,6 +68,18 @@ test.describe('chat projects', () => {
|
|||
await expect(
|
||||
page.getByTestId(`project-chats-${projectId}`).getByTestId('convo-item').first(),
|
||||
).toBeVisible();
|
||||
|
||||
const conversationUrl = page.url();
|
||||
await page.reload({ timeout: 10000 });
|
||||
await expect(page).toHaveURL(conversationUrl);
|
||||
|
||||
const reloadedProjectRow = page.getByRole('button', { name }).first();
|
||||
if ((await reloadedProjectRow.getAttribute('aria-expanded')) !== 'true') {
|
||||
await reloadedProjectRow.click();
|
||||
}
|
||||
await expect(
|
||||
page.getByTestId(`project-chats-${projectId}`).getByTestId('convo-item').first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('removes the project scope via the chip ×', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ test.describe('skill file authoring tools', () => {
|
|||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
await expect(page).toHaveURL(/\/c\/(?!new)/, { timeout: 15000 });
|
||||
const conversationUrl = page.url();
|
||||
const conversationPath = new URL(page.url()).pathname;
|
||||
await expect(
|
||||
page.getByTestId('messages-view').getByText(`${FINAL_TEXT}: ${skillName}`),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
|
|
@ -113,7 +113,7 @@ test.describe('skill file authoring tools', () => {
|
|||
expect(editedSkill.body).toContain(EDITED_DESCRIPTION);
|
||||
|
||||
await page.reload({ timeout: 10000 });
|
||||
await expect(page).toHaveURL(conversationUrl);
|
||||
await expect(page).toHaveURL((url) => url.pathname === conversationPath);
|
||||
await expect(
|
||||
page.getByTestId('messages-view').getByText(`${FINAL_TEXT}: ${skillName}`),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export type PrivateModelSpecPresetField = (typeof PRIVATE_MODEL_SPEC_PRESET_FIEL
|
|||
export type ModelSpecParsedBody = Partial<TConversation | TPreset | TModelSpecPreset> &
|
||||
Record<string, unknown>;
|
||||
|
||||
export const ENFORCED_MODEL_SPEC_REQUEST_FIELDS = [
|
||||
'chatProjectId',
|
||||
] as const satisfies readonly (keyof ModelSpecParsedBody)[];
|
||||
|
||||
export type ApplyModelSpecPresetParams = {
|
||||
modelSpec: TModelSpec;
|
||||
parsedBody: ModelSpecParsedBody;
|
||||
|
|
@ -57,15 +61,30 @@ function hasModelSpecValue(field: PrivateModelSpecPresetField, value: unknown):
|
|||
return value.length > 0;
|
||||
}
|
||||
|
||||
function pickEnforcedModelSpecRequestFields(parsedBody: ModelSpecParsedBody): ModelSpecParsedBody {
|
||||
const requestFields: ModelSpecParsedBody = {};
|
||||
|
||||
for (const field of ENFORCED_MODEL_SPEC_REQUEST_FIELDS) {
|
||||
if (parsedBody[field] !== undefined) {
|
||||
requestFields[field] = parsedBody[field];
|
||||
}
|
||||
}
|
||||
|
||||
return requestFields;
|
||||
}
|
||||
|
||||
function mergeModelSpecPreset(
|
||||
modelSpec: TModelSpec,
|
||||
parsedBody: ModelSpecParsedBody,
|
||||
{ includePresetDefaults = false }: Pick<ApplyModelSpecPresetParams, 'includePresetDefaults'> = {},
|
||||
): ApplyModelSpecPresetResult {
|
||||
const preset = modelSpec.preset;
|
||||
const requestFields = includePresetDefaults
|
||||
? pickEnforcedModelSpecRequestFields(parsedBody)
|
||||
: parsedBody;
|
||||
const merged = {
|
||||
...requestFields,
|
||||
...(includePresetDefaults ? preset : {}),
|
||||
...parsedBody,
|
||||
spec: modelSpec.name,
|
||||
} as ModelSpecParsedBody;
|
||||
const appliedPrivateFields = new Set<PrivateModelSpecPresetField>();
|
||||
|
|
|
|||
|
|
@ -92,14 +92,24 @@ describe('modelSpecs helpers', () => {
|
|||
|
||||
const { parsedBody } = applyModelSpecPreset({
|
||||
modelSpec,
|
||||
parsedBody: modelSpec.preset,
|
||||
parsedBody: {
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
spec: 'enforced-openai',
|
||||
model: 'client-model',
|
||||
temperature: 0.8,
|
||||
topP: 0.9,
|
||||
chatProjectId: 'project-1',
|
||||
},
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
includePresetDefaults: true,
|
||||
});
|
||||
|
||||
expect(parsedBody.spec).toBe('enforced-openai');
|
||||
expect(parsedBody.model).toBe('gpt-4o');
|
||||
expect(parsedBody.promptPrefix).toBe('private prompt prefix');
|
||||
expect(parsedBody.temperature).toBe(0.2);
|
||||
expect(parsedBody.topP).toBeUndefined();
|
||||
expect(parsedBody.chatProjectId).toBe('project-1');
|
||||
});
|
||||
|
||||
it('should restore private examples when parser supplies an empty default', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue