diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 3d19f65ad6..81873225f5 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -353,6 +353,8 @@ "com_endpoint_use_active_assistant": "Use Active Assistant", "com_endpoint_use_responses_api": "Use Responses API", "com_endpoint_use_search_grounding": "Grounding with Google Search", + "com_endpoint_use_url_context": "URL Context", + "com_endpoint_google_use_url_context": "Use Gemini's URL context feature to fetch and use content from URLs in your prompt. Supports up to 20 URLs per request.", "com_endpoint_verbosity": "Verbosity", "com_error_endpoint_models_not_loaded": "Models for {{0}} could not be loaded. Please refresh the page and try again.", "com_error_expired_user_key": "Provided key for {{0}} expired at {{1}}. Please provide a new key and try again.", diff --git a/packages/api/src/endpoints/google/llm.spec.ts b/packages/api/src/endpoints/google/llm.spec.ts index 6e2a8ddb25..15a4d47fa5 100644 --- a/packages/api/src/endpoints/google/llm.spec.ts +++ b/packages/api/src/endpoints/google/llm.spec.ts @@ -634,6 +634,107 @@ describe('getGoogleConfig', () => { }); }); + describe('URL Context Functionality', () => { + it('should enable url_context when url_context is true', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + url_context: true, + }, + }); + + expect(result.tools).toContainEqual({ urlContext: {} }); + }); + + it('should not enable url_context when url_context is false', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + url_context: false, + }, + }); + + expect(result.tools).not.toContainEqual({ urlContext: {} }); + }); + + it('should handle url_context with web_search together', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + web_search: true, + url_context: true, + }, + }); + + expect(result.tools).toContainEqual({ googleSearch: {} }); + expect(result.tools).toContainEqual({ urlContext: {} }); + expect(result.tools).toHaveLength(2); + }); + + it('should enable url_context via defaultParams', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + }, + defaultParams: { + url_context: true, + }, + }); + + expect(result.tools).toContainEqual({ urlContext: {} }); + }); + + it('should enable url_context via addParams when initially disabled', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + url_context: false, + }, + addParams: { + url_context: true, + }, + }); + + expect(result.tools).toContainEqual({ urlContext: {} }); + }); + + it('should disable url_context via dropParams', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + url_context: true, + }, + dropParams: ['url_context'], + }); + + expect(result.tools).not.toContainEqual({ urlContext: {} }); + }); + }); + describe('Default and Add Parameters', () => { it('should apply default parameters when fields are undefined', () => { const credentials = { diff --git a/packages/api/src/endpoints/google/llm.ts b/packages/api/src/endpoints/google/llm.ts index 83951f9e0c..165a1cc0b0 100644 --- a/packages/api/src/endpoints/google/llm.ts +++ b/packages/api/src/endpoints/google/llm.ts @@ -150,6 +150,7 @@ export function getGoogleConfig( const { web_search, + url_context, thinkingLevel, thinking = googleSettings.thinking.default, thinkingBudget = googleSettings.thinkingBudget.default, @@ -157,6 +158,7 @@ export function getGoogleConfig( } = options.modelOptions || {}; let enableWebSearch = web_search; + let enableUrlContext = url_context; const llmConfig: GoogleClientOptions | VertexAIClientOptions = removeNullishValues( { @@ -282,6 +284,14 @@ export function getGoogleConfig( continue; } + /** Handle url_context separately - don't add to config */ + if (key === 'url_context') { + if (enableUrlContext === undefined && typeof value === 'boolean') { + enableUrlContext = value; + } + continue; + } + if (knownGoogleParams.has(key)) { /** Route known Google params to llmConfig only if undefined */ applyDefaultParams(llmConfig as Record, { [key]: value }); @@ -301,6 +311,14 @@ export function getGoogleConfig( continue; } + /** Handle url_context separately - don't add to config */ + if (key === 'url_context') { + if (typeof value === 'boolean') { + enableUrlContext = value; + } + continue; + } + if (knownGoogleParams.has(key)) { /** Route known Google params to llmConfig */ (llmConfig as Record)[key] = value; @@ -317,6 +335,11 @@ export function getGoogleConfig( return; } + if (param === 'url_context') { + enableUrlContext = false; + return; + } + if (param in llmConfig) { delete (llmConfig as Record)[param]; } @@ -329,6 +352,10 @@ export function getGoogleConfig( tools.push({ googleSearch: {} }); } + if (enableUrlContext) { + tools.push({ urlContext: {} }); + } + // Return the final shape return { /** @type {GoogleAIToolType[]} */ diff --git a/packages/api/src/endpoints/openai/transform.ts b/packages/api/src/endpoints/openai/transform.ts index c65e2cd6f5..2cbd1b6e3d 100644 --- a/packages/api/src/endpoints/openai/transform.ts +++ b/packages/api/src/endpoints/openai/transform.ts @@ -16,7 +16,7 @@ const googleExcludeParams = new Set([ ]); /** Google-specific tool types that have no OpenAI-compatible equivalent */ -const googleToolsToFilter = new Set(['googleSearch']); +const googleToolsToFilter = new Set(['googleSearch', 'urlContext']); export type ConfigTools = Array> | Array; @@ -93,8 +93,8 @@ export function transformToOpenAIConfig({ if (addParams && typeof addParams === 'object') { for (const [key, value] of Object.entries(addParams)) { - /** Skip web_search - it's handled separately as a tool */ - if (key === 'web_search') { + /** Skip web_search and url_context - they're handled separately as tools */ + if (key === 'web_search' || key === 'url_context') { continue; } @@ -113,8 +113,8 @@ export function transformToOpenAIConfig({ if (dropParams && Array.isArray(dropParams)) { dropParams.forEach((param) => { - /** Skip web_search - handled separately */ - if (param === 'web_search') { + /** Skip web_search and url_context - handled separately as tools */ + if (param === 'web_search' || param === 'url_context') { return; } @@ -134,11 +134,13 @@ export function transformToOpenAIConfig({ /** * Filter out provider-specific tools that have no OpenAI equivalent. - * Exception: If web_search was explicitly enabled via addParams or defaultParams, - * preserve googleSearch tools (pass through in Google-native format). + * Exception: If web_search or url_context was explicitly enabled via addParams or defaultParams, + * preserve the corresponding Google-native tools (googleSearch, urlContext). */ const webSearchExplicitlyEnabled = addParams?.web_search === true || defaultParams?.web_search === true; + const urlContextExplicitlyEnabled = + addParams?.url_context === true || defaultParams?.url_context === true; const filterGoogleTool = (tool: unknown): boolean => { if (!isGoogle) { @@ -149,11 +151,18 @@ export function transformToOpenAIConfig({ } const toolKeys = Object.keys(tool as Record); const isGoogleSpecificTool = toolKeys.some((key) => googleToolsToFilter.has(key)); - /** Preserve googleSearch if web_search was explicitly enabled */ - if (isGoogleSpecificTool && webSearchExplicitlyEnabled) { - return true; + if (isGoogleSpecificTool) { + const hasGoogleSearch = toolKeys.includes('googleSearch'); + const hasUrlContext = toolKeys.includes('urlContext'); + if (hasGoogleSearch && webSearchExplicitlyEnabled) { + return true; + } + if (hasUrlContext && urlContextExplicitlyEnabled) { + return true; + } + return false; } - return !isGoogleSpecificTool; + return true; }; const filteredTools = Array.isArray(tools) ? tools.filter(filterGoogleTool) : []; diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index d0cfdf210f..7578bee70e 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -712,6 +712,19 @@ const google: Record = { showDefault: false, columnSpan: 2, }, + url_context: { + key: 'url_context', + label: 'com_endpoint_use_url_context', + labelCode: true, + description: 'com_endpoint_google_use_url_context', + descriptionCode: true, + type: 'boolean', + default: false, + component: 'switch', + optionType: 'model', + showDefault: false, + columnSpan: 2, + }, }; const googleConfig: SettingsConfiguration = [ @@ -727,6 +740,7 @@ const googleConfig: SettingsConfiguration = [ google.thinkingBudget, google.thinkingLevel, google.web_search, + google.url_context, librechat.fileTokenLimit, ]; @@ -747,6 +761,7 @@ const googleCol2: SettingsConfiguration = [ google.thinkingBudget, google.thinkingLevel, google.web_search, + google.url_context, librechat.fileTokenLimit, ]; diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 084f74af86..9f48e0655b 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -774,6 +774,8 @@ export const tConversationSchema = z.object({ effort: eAnthropicEffortSchema.optional().nullable(), /* OpenAI Responses API / Anthropic API / Google API */ web_search: z.boolean().optional(), + /* Google API */ + url_context: z.boolean().optional(), /* disable streaming */ disableStreaming: z.boolean().optional(), /* assistant */ @@ -882,6 +884,8 @@ export const tQueryParamsSchema = tConversationSchema useResponsesApi: true, /** @endpoints openAI, anthropic, google */ web_search: true, + /** @endpoints google */ + url_context: true, /** @endpoints openAI, custom, azureOpenAI */ disableStreaming: true, /** @endpoints google, anthropic, bedrock */ @@ -996,6 +1000,7 @@ export const googleBaseSchema = tConversationSchema.pick({ thinkingBudget: true, thinkingLevel: true, web_search: true, + url_context: true, fileTokenLimit: true, iconURL: true, greeting: true, @@ -1030,6 +1035,7 @@ export const googleGenConfigSchema = z }) .optional(), web_search: z.boolean().optional(), + url_context: z.boolean().optional(), }) .strip() .optional(); diff --git a/packages/data-schemas/src/schema/defaults.ts b/packages/data-schemas/src/schema/defaults.ts index 9b50bceb1d..ca225ae354 100644 --- a/packages/data-schemas/src/schema/defaults.ts +++ b/packages/data-schemas/src/schema/defaults.ts @@ -143,6 +143,10 @@ export const conversationPreset = { web_search: { type: Boolean, }, + /** Google API */ + url_context: { + type: Boolean, + }, disableStreaming: { type: Boolean, }, diff --git a/packages/data-schemas/src/schema/preset.ts b/packages/data-schemas/src/schema/preset.ts index 5af5163fd3..3d1a72a6d6 100644 --- a/packages/data-schemas/src/schema/preset.ts +++ b/packages/data-schemas/src/schema/preset.ts @@ -51,6 +51,7 @@ export interface IPreset extends Document { verbosity?: string; useResponsesApi?: boolean; web_search?: boolean; + url_context?: boolean; disableStreaming?: boolean; fileTokenLimit?: number; tenantId?: string; diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts index c7888efba2..b5d50e78c3 100644 --- a/packages/data-schemas/src/types/convo.ts +++ b/packages/data-schemas/src/types/convo.ts @@ -49,6 +49,7 @@ export interface IConversation extends Document { verbosity?: string; useResponsesApi?: boolean; web_search?: boolean; + url_context?: boolean; disableStreaming?: boolean; fileTokenLimit?: number; // Additional fields