📌 fix: Preserve Project Scope Through Enforced Model Specs (#13586)

This commit is contained in:
Danny Avila 2026-06-08 08:41:27 -04:00 committed by GitHub
parent 209e8d1eb6
commit 4b699fb60f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 56 additions and 6 deletions

View file

@ -78,7 +78,7 @@ async function buildEndpointOption(req, res, next) {
try {
const result = applyModelSpecPreset({
modelSpec: currentModelSpec,
parsedBody: currentModelSpec.preset,
parsedBody,
endpoint,
endpointType,
defaultParamsEndpoint,

View file

@ -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 () => {

View file

@ -27,6 +27,8 @@ endpoints:
modelDisplayLabel: 'Mock Provider B'
modelSpecs:
prioritize: true
enforce: true
list:
- name: 'e2e-mock-provider-a'
label: 'Mock Provider A'

View file

@ -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 }) => {

View file

@ -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 });

View file

@ -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>();

View file

@ -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', () => {