mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
🛰️ fix: Validate Vertex Endpoint Overrides (#13054)
* fix: Validate Vertex endpoint overrides * fix: Allow Vertex PSC endpoint overrides * fix: Allow restricted Vertex PSC endpoints
This commit is contained in:
parent
5bab22d236
commit
846eb0aa2c
2 changed files with 196 additions and 12 deletions
|
|
@ -308,7 +308,7 @@ describe('getGoogleConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should preserve explicit Vertex AI endpoint overrides', () => {
|
||||
it('should preserve explicit Google Vertex AI endpoint overrides', () => {
|
||||
process.env.GOOGLE_LOC = 'us';
|
||||
|
||||
const credentials = {
|
||||
|
|
@ -323,13 +323,134 @@ describe('getGoogleConfig', () => {
|
|||
},
|
||||
addParams: {
|
||||
location: 'eu',
|
||||
endpoint: 'custom-aiplatform.example.com',
|
||||
endpoint: 'us-central1-aiplatform.googleapis.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
location: 'eu',
|
||||
endpoint: 'custom-aiplatform.example.com',
|
||||
endpoint: 'us-central1-aiplatform.googleapis.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve explicit Google Vertex AI Private Service Connect endpoints', () => {
|
||||
process.env.GOOGLE_LOC = 'us';
|
||||
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_SERVICE_KEY]: {
|
||||
project_id: 'test-project',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.1-flash-lite-preview',
|
||||
},
|
||||
addParams: {
|
||||
endpoint: 'aiplatform-genai1.p.googleapis.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
location: 'us',
|
||||
endpoint: 'aiplatform-genai1.p.googleapis.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve explicit Google Vertex AI restricted Private Service Connect endpoints', () => {
|
||||
process.env.GOOGLE_LOC = 'us';
|
||||
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_SERVICE_KEY]: {
|
||||
project_id: 'test-project',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.1-flash-lite-preview',
|
||||
},
|
||||
addParams: {
|
||||
endpoint: 'us-central1-aiplatform-restricted.p.googleapis.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
location: 'us',
|
||||
endpoint: 'us-central1-aiplatform-restricted.p.googleapis.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore model option Vertex AI endpoint overrides', () => {
|
||||
process.env.GOOGLE_LOC = 'eu';
|
||||
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_SERVICE_KEY]: {
|
||||
project_id: 'test-project',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.1-flash-lite-preview',
|
||||
endpoint: 'attacker.example.test',
|
||||
} as t.GoogleParameters,
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
location: 'eu',
|
||||
endpoint: 'aiplatform.eu.rep.googleapis.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore model option transport-level overrides', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_SERVICE_KEY]: {
|
||||
project_id: 'test-project',
|
||||
client_email: 'test@test-project.iam.gserviceaccount.com',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.1-flash-lite-preview',
|
||||
apiKey: 'attacker-api-key',
|
||||
authOptions: { projectId: 'attacker-project' },
|
||||
baseUrl: 'https://attacker.example.test',
|
||||
customHeaders: { Authorization: 'Bearer attacker' },
|
||||
} as t.GoogleParameters,
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('apiKey', 'attacker-api-key');
|
||||
expect(result.llmConfig).not.toHaveProperty('baseUrl');
|
||||
expect(result.llmConfig).not.toHaveProperty('customHeaders');
|
||||
expect((result.llmConfig as Record<string, unknown>).authOptions).toMatchObject({
|
||||
projectId: 'test-project',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore non-Google Vertex AI endpoint overrides from additional params', () => {
|
||||
process.env.GOOGLE_LOC = 'us';
|
||||
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_SERVICE_KEY]: {
|
||||
project_id: 'test-project',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.1-flash-lite-preview',
|
||||
},
|
||||
addParams: {
|
||||
location: 'eu',
|
||||
endpoint: 'attacker.example.test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
location: 'eu',
|
||||
endpoint: 'aiplatform.eu.rep.googleapis.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,20 @@ const vertexMultiRegionEndpoints = new Map([
|
|||
['global', 'aiplatform.googleapis.com'],
|
||||
]);
|
||||
|
||||
const blockedModelOptionParams = [
|
||||
'apiKey',
|
||||
'baseUrl',
|
||||
'baseURL',
|
||||
'endpoint',
|
||||
'authOptions',
|
||||
'customHeaders',
|
||||
'headers',
|
||||
] as const;
|
||||
|
||||
type BlockedModelOptionParam = (typeof blockedModelOptionParams)[number];
|
||||
type GoogleModelOptions = Partial<t.GoogleParameters> &
|
||||
Partial<Record<BlockedModelOptionParam, unknown>>;
|
||||
|
||||
/** Known Google/Vertex AI parameters that map directly to the client config */
|
||||
export const knownGoogleParams = new Set([
|
||||
'model',
|
||||
|
|
@ -106,8 +120,57 @@ function getVertexMultiRegionEndpoint(location: string): string | undefined {
|
|||
return vertexMultiRegionEndpoints.get(location);
|
||||
}
|
||||
|
||||
function hasStringEndpoint(config: Record<string, unknown>): boolean {
|
||||
return typeof config.endpoint === 'string' && config.endpoint.length > 0;
|
||||
function sanitizeModelOptions(modelOptions: Partial<t.GoogleParameters> | undefined) {
|
||||
const sanitizedOptions: GoogleModelOptions = { ...(modelOptions ?? {}) };
|
||||
blockedModelOptionParams.forEach((param) => {
|
||||
delete sanitizedOptions[param];
|
||||
});
|
||||
return sanitizedOptions;
|
||||
}
|
||||
|
||||
function isAllowedVertexEndpoint(endpoint: string): boolean {
|
||||
if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(endpoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (endpoint === 'aiplatform.googleapis.com') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^[a-z0-9-]+-aiplatform\.googleapis\.com$/.test(endpoint)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^aiplatform-[a-z0-9-]+\.p\.googleapis\.com$/.test(endpoint)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^[a-z0-9-]+-aiplatform-restricted\.p\.googleapis\.com$/.test(endpoint)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /^aiplatform\.[a-z0-9-]+\.rep\.googleapis\.com$/.test(endpoint);
|
||||
}
|
||||
|
||||
function hasAllowedVertexEndpoint(config: Record<string, unknown>): boolean {
|
||||
const endpoint = config.endpoint;
|
||||
if (endpoint === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint.length === 0) {
|
||||
delete config.endpoint;
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedEndpoint = endpoint.toLowerCase();
|
||||
if (!isAllowedVertexEndpoint(normalizedEndpoint)) {
|
||||
delete config.endpoint;
|
||||
return false;
|
||||
}
|
||||
|
||||
config.endpoint = normalizedEndpoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyVertexMultiRegionEndpoint(config: VertexAIClientOptions & { endpoint?: string }) {
|
||||
|
|
@ -205,11 +268,11 @@ export function getGoogleConfig(
|
|||
thinking = googleSettings.thinking.default,
|
||||
thinkingBudget = googleSettings.thinkingBudget.default,
|
||||
...modelOptions
|
||||
} = options.modelOptions || {};
|
||||
} = sanitizeModelOptions(options.modelOptions);
|
||||
|
||||
let enableWebSearch = web_search;
|
||||
|
||||
const llmConfig: GoogleClientOptions | VertexAIClientOptions = removeNullishValues(
|
||||
const llmConfig = removeNullishValues(
|
||||
{
|
||||
...(modelOptions || {}),
|
||||
model: modelOptions?.model ?? '',
|
||||
|
|
@ -220,9 +283,9 @@ export function getGoogleConfig(
|
|||
maxOutputTokens: modelOptions?.maxOutputTokens ?? undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
) as GoogleClientOptions | VertexAIClientOptions;
|
||||
const initialConfig = llmConfig as Record<string, unknown>;
|
||||
let hasCustomVertexEndpoint = hasStringEndpoint(initialConfig);
|
||||
let hasCustomVertexEndpoint = hasAllowedVertexEndpoint(initialConfig);
|
||||
let shouldSyncVertexEndpoint = true;
|
||||
|
||||
/** Used only for Safety Settings */
|
||||
|
|
@ -341,8 +404,8 @@ export function getGoogleConfig(
|
|||
if (knownGoogleParams.has(key)) {
|
||||
/** Route known Google params to llmConfig only if undefined */
|
||||
applyDefaultParams(llmConfig as Record<string, unknown>, { [key]: value });
|
||||
if (key === 'endpoint' && hasStringEndpoint(llmConfig as Record<string, unknown>)) {
|
||||
hasCustomVertexEndpoint = true;
|
||||
if (key === 'endpoint') {
|
||||
hasCustomVertexEndpoint = hasAllowedVertexEndpoint(llmConfig as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
|
|
@ -364,7 +427,7 @@ export function getGoogleConfig(
|
|||
/** Route known Google params to llmConfig */
|
||||
(llmConfig as Record<string, unknown>)[key] = value;
|
||||
if (key === 'endpoint') {
|
||||
hasCustomVertexEndpoint = hasStringEndpoint(llmConfig as Record<string, unknown>);
|
||||
hasCustomVertexEndpoint = hasAllowedVertexEndpoint(llmConfig as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue