🌐 fix: Expose Gemini Models to Vertex AI Agents (#15234)

* 🌐 fix: Expose Gemini Models to Vertex AI Agents

* ♻️ refactor: Resolve Shared Vertex Model Catalogs

* fix: Preserve Exact Vertex Model Catalogs

* style: Format Agent Model Selection

* test: Preserve Native FS in Stable Diffusion Spec
This commit is contained in:
Danny Avila 2026-08-27 06:50:23 -04:00 committed by GitHub
parent 3d808dc906
commit 8b1fcc0fc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 287 additions and 34 deletions

View file

@ -1,7 +1,12 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, alternateName, isAgentsEndpoint } from 'librechat-data-provider';
import {
QueryKeys,
alternateName,
isAgentsEndpoint,
resolveModelCatalogKey,
} from 'librechat-data-provider';
import {
Input,
Label,
@ -77,7 +82,9 @@ const EditPresetDialog = ({
return;
}
const models = modelsConfig[presetEndpoint] as string[] | undefined;
const models = modelsConfig[resolveModelCatalogKey(presetEndpoint, modelsConfig)] as
| string[]
| undefined;
if (!models) {
return;
}

View file

@ -1,3 +1,4 @@
import { resolveModelCatalogKey } from 'librechat-data-provider';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TConversation } from 'librechat-data-provider';
import type { TSetOption } from '~/common';
@ -29,7 +30,7 @@ export default function ModelSelect({
}
const { endpoint: _endpoint, endpointType } = conversation;
const models = modelsQuery.data?.[_endpoint] ?? [];
const models = modelsQuery.data?.[resolveModelCatalogKey(_endpoint, modelsQuery.data)] ?? [];
const endpoint = endpointType ?? _endpoint;
const OptionComponent = multiChatOptions[endpoint];

View file

@ -13,6 +13,7 @@ import {
LocalStorageKeys,
PermissionBits,
removeCodeExecutionCaller,
resolveModelCatalogKey,
resolveStatefulCodeEnvironment,
isAssistantsEndpoint,
} from 'librechat-data-provider';
@ -613,7 +614,7 @@ export default function AgentPanel() {
status: 'error',
});
}
if (!(models[provider] ?? []).includes(model)) {
if (!(models[resolveModelCatalogKey(provider, models)] ?? []).includes(model)) {
return showToast({
message: localize('com_error_model_not_found'),
status: 'error',

View file

@ -2,6 +2,7 @@
* @jest-environment jsdom
*/
import React from 'react';
import { Providers } from 'librechat-data-provider';
import { FormProvider, useForm } from 'react-hook-form';
import { fireEvent, render } from '@testing-library/react';
import type { AgentForm } from '~/common';
@ -146,6 +147,54 @@ describe('ModelPanel', () => {
expect(localStorage.getItem('lastAgentModel')).toBe('alternate-model');
});
it('selects the Google catalog for a Vertex AI provider', () => {
const providers = [
{ label: 'Original', value: 'original' },
{ label: 'Vertex AI', value: Providers.VERTEXAI },
];
const { getByTestId } = render(
<TestForm
defaultProvider="original"
defaultModel="original-model"
models={{ original: ['original-model'], google: ['gemini-3.7-flash'] }}
modelsReady={true}
providers={providers}
/>,
);
fireEvent.click(getByTestId(`com_ui_provider-${Providers.VERTEXAI}`));
expect(getByTestId('com_ui_model-selected')).toHaveTextContent('gemini-3.7-flash');
expect(localStorage.getItem('lastAgentProvider')).toBe(Providers.VERTEXAI);
expect(localStorage.getItem('lastAgentModel')).toBe('gemini-3.7-flash');
});
it('selects an exact Vertex AI catalog when configured', () => {
const providers = [
{ label: 'Original', value: 'original' },
{ label: 'Vertex AI', value: Providers.VERTEXAI },
];
const { getByTestId } = render(
<TestForm
defaultProvider="original"
defaultModel="original-model"
models={{
original: ['original-model'],
google: ['gemini-3.7-flash'],
[Providers.VERTEXAI]: ['custom-vertex-model'],
}}
modelsReady={true}
providers={providers}
/>,
);
fireEvent.click(getByTestId(`com_ui_provider-${Providers.VERTEXAI}`));
expect(getByTestId('com_ui_model-selected')).toHaveTextContent('custom-vertex-model');
expect(localStorage.getItem('lastAgentProvider')).toBe(Providers.VERTEXAI);
expect(localStorage.getItem('lastAgentModel')).toBe('custom-vertex-model');
});
it('preserves the model when the current provider is selected again', () => {
const { getByTestId } = render(
<TestForm

View file

@ -8,6 +8,7 @@ import {
getSettingsKeys,
getEndpointField,
LocalStorageKeys,
resolveModelCatalogKey,
SettingDefinition,
agentParamSettings,
applyModelAwareDefaults,
@ -58,7 +59,7 @@ export default function ModelPanel({
return value ?? '';
}, [providerOption]);
const models = useMemo(
() => (provider ? (modelsData[provider] ?? []) : []),
() => (provider ? (modelsData[resolveModelCatalogKey(provider, modelsData)] ?? []) : []),
[modelsData, provider],
);
const modelsPending = !modelsReady && !modelsError;
@ -158,7 +159,8 @@ export default function ModelPanel({
if (value === provider) {
return;
}
const nextModel = modelsData[value]?.[0] ?? '';
const nextModel =
modelsData[resolveModelCatalogKey(value, modelsData)]?.[0] ?? '';
field.onChange(value);
setValue('model', nextModel);
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, value);

View file

@ -1,15 +1,9 @@
import { useMemo } from 'react';
import { Providers, EModelEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import { isAgentsEndpoint, resolveModelCatalogKey } from 'librechat-data-provider';
import type { TConversation, TModelTokenomics } from 'librechat-data-provider';
import { useGetStartupConfig, useTokenConfigQuery, useGetAgentByIdQuery } from '~/data-provider';
import { getModelSpec } from '~/utils';
/** Gemini tokenomics are advertised under the `google` endpoint, so a
* Vertex-backed agent (`provider: 'vertexai'`) must look up there. */
function normalizeTokenConfigKey(endpoint: string): string {
return endpoint === Providers.VERTEXAI ? EModelEndpoint.google : endpoint;
}
export interface TokenLimits {
/** Statically resolved max context; live snapshots override this at run time */
maxContextTokens?: number;
@ -52,7 +46,7 @@ export default function useTokenLimits(conversation: TConversation | null): Toke
lookupEndpoint = specPreset.endpoint ?? lookupEndpoint;
lookupModel = lookupModel || (specPreset.model ?? '');
}
lookupEndpoint = normalizeTokenConfigKey(lookupEndpoint);
lookupEndpoint = resolveModelCatalogKey(lookupEndpoint, tokenConfig);
const rates = tokenConfig?.[lookupEndpoint]?.[lookupModel];
const maxContextTokens =

View file

@ -1,6 +1,10 @@
import { useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { excludedKeys, getDefaultParamsEndpoint } from 'librechat-data-provider';
import {
excludedKeys,
getDefaultParamsEndpoint,
resolveModelCatalogKey,
} from 'librechat-data-provider';
import type {
TEndpointsConfig,
TModelsConfig,
@ -30,7 +34,7 @@ const useDefaultConvo = () => {
endpointsConfig,
});
const models = modelsConfig[endpoint ?? ''] || [];
const models = modelsConfig[resolveModelCatalogKey(endpoint, modelsConfig)] || [];
const conversation = { ..._convo };
if (cleanInput === true) {
for (const key in conversation) {

View file

@ -1,3 +1,5 @@
import { resolveModelCatalogKey } from 'librechat-data-provider';
type ProviderOption = string | { value?: string | number | null };
export function getAvailableModelSelection(model: string, models: readonly string[]): string {
@ -16,7 +18,7 @@ export function getAvailableAgentSelection({
models: Record<string, string[] | undefined>;
}): { provider: string; model: string } {
const providerExists =
models[provider] != null &&
models[resolveModelCatalogKey(provider, models)] != null &&
providers.some((option) =>
typeof option === 'string' ? option === provider : option.value === provider,
);
@ -27,6 +29,9 @@ export function getAvailableAgentSelection({
return {
provider,
model: getAvailableModelSelection(model, models[provider] ?? []),
model: getAvailableModelSelection(
model,
models[resolveModelCatalogKey(provider, models)] ?? [],
),
};
}