feat: add URL trailing slash compatibility for system prompt mappings

- Add normalizeUrl() function to standardize URLs by removing trailing slashes
- Add urlMatches() function to compare URLs ignoring trailing slash differences
- Replace direct includes() comparison with urlMatches() for baseUrl matching
- Add comprehensive tests to verify URL matching with/without trailing slashes
- Fixes issue where URLs like 'https://api.example.com' and 'https://api.example.com/' were treated as different
This commit is contained in:
pomelo-nwu
2025-07-28 14:18:10 +08:00
parent 075f66fe13
commit beb5b7ff57
2 changed files with 110 additions and 2 deletions

View File

@@ -106,3 +106,96 @@ describe('Core System Prompt (prompts.ts)', () => {
expect(prompt).toMatchSnapshot(); expect(prompt).toMatchSnapshot();
}); });
}); });
describe('URL matching with trailing slash compatibility', () => {
it('should match URLs with and without trailing slash', () => {
const config = {
systemPromptMappings: [
{
baseUrls: ['https://api.example.com'],
modelNames: ['gpt-4'],
template: 'Custom template for example.com',
},
{
baseUrls: ['https://api.openai.com/'],
modelNames: ['gpt-3.5-turbo'],
template: 'Custom template for openai.com',
},
],
};
// Simulate environment variables
const originalEnv = process.env;
// Test case 1: No trailing slash in config, actual URL has trailing slash
process.env = {
...originalEnv,
OPENAI_BASE_URL: 'https://api.example.com/',
OPENAI_MODEL: 'gpt-4',
};
const result1 = getCoreSystemPrompt(undefined, config);
expect(result1).toContain('Custom template for example.com');
// Test case 2: Config has trailing slash, actual URL has no trailing slash
process.env = {
...originalEnv,
OPENAI_BASE_URL: 'https://api.openai.com',
OPENAI_MODEL: 'gpt-3.5-turbo',
};
const result2 = getCoreSystemPrompt(undefined, config);
expect(result2).toContain('Custom template for openai.com');
// Test case 3: No trailing slash in config, actual URL has no trailing slash
process.env = {
...originalEnv,
OPENAI_BASE_URL: 'https://api.example.com',
OPENAI_MODEL: 'gpt-4',
};
const result3 = getCoreSystemPrompt(undefined, config);
expect(result3).toContain('Custom template for example.com');
// Test case 4: Config has trailing slash, actual URL has trailing slash
process.env = {
...originalEnv,
OPENAI_BASE_URL: 'https://api.openai.com/',
OPENAI_MODEL: 'gpt-3.5-turbo',
};
const result4 = getCoreSystemPrompt(undefined, config);
expect(result4).toContain('Custom template for openai.com');
// Restore original environment variables
process.env = originalEnv;
});
it('should not match when URLs are different', () => {
const config = {
systemPromptMappings: [
{
baseUrls: ['https://api.example.com'],
modelNames: ['gpt-4'],
template: 'Custom template for example.com',
},
],
};
const originalEnv = process.env;
// Test case: URLs do not match
process.env = {
...originalEnv,
OPENAI_BASE_URL: 'https://api.different.com',
OPENAI_MODEL: 'gpt-4',
};
const result = getCoreSystemPrompt(undefined, config);
// Should return default template, not contain custom template
expect(result).not.toContain('Custom template for example.com');
// Restore original environment variables
process.env = originalEnv;
});
});

View File

@@ -28,6 +28,21 @@ export interface SystemPromptConfig {
systemPromptMappings?: ModelTemplateMapping[]; systemPromptMappings?: ModelTemplateMapping[];
} }
/**
* Normalizes a URL by removing trailing slash for consistent comparison
*/
function normalizeUrl(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
/**
* Checks if a URL matches any URL in the array, ignoring trailing slashes
*/
function urlMatches(urlArray: string[], targetUrl: string): boolean {
const normalizedTarget = normalizeUrl(targetUrl);
return urlArray.some((url) => normalizeUrl(url) === normalizedTarget);
}
export function getCoreSystemPrompt( export function getCoreSystemPrompt(
userMemory?: string, userMemory?: string,
config?: SystemPromptConfig, config?: SystemPromptConfig,
@@ -59,13 +74,13 @@ export function getCoreSystemPrompt(
if ( if (
baseUrls && baseUrls &&
modelNames && modelNames &&
baseUrls.includes(currentBaseUrl) && urlMatches(baseUrls, currentBaseUrl) &&
modelNames.includes(currentModel) modelNames.includes(currentModel)
) { ) {
return true; return true;
} }
if (baseUrls && baseUrls.includes(currentBaseUrl) && !modelNames) { if (baseUrls && urlMatches(baseUrls, currentBaseUrl) && !modelNames) {
return true; return true;
} }
if (modelNames && modelNames.includes(currentModel) && !baseUrls) { if (modelNames && modelNames.includes(currentModel) && !baseUrls) {