🔍 fix: Prefer LibreChat Web Search over Anthropic's when Both Selected (#13166)

This commit is contained in:
Danny Avila 2026-05-18 15:35:29 -04:00 committed by GitHub
parent 6466263493
commit 394839a76b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 210 additions and 8 deletions

View file

@ -33,7 +33,7 @@ jest.mock('@librechat/agents', () => ({
}));
import { Providers } from '@librechat/agents';
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, Tools } from 'librechat-data-provider';
import type { Agent } from 'librechat-data-provider';
import type { ServerRequest, InitializeResultBase, EndpointTokenConfig } from '~/types';
import type { InitializeAgentDbMethods } from '../initialize';
@ -126,6 +126,13 @@ function createMocks(overrides?: {
maxOutputTokens?: number;
endpointTokenConfig?: EndpointTokenConfig;
useRealTokenLookup?: boolean;
providerTools?: unknown[];
loadedToolDefinitions?: Array<{
name: string;
description?: string;
parameters?: object;
}>;
structuredTools?: unknown[];
}) {
const {
provider = Providers.OPENAI,
@ -136,6 +143,9 @@ function createMocks(overrides?: {
maxOutputTokens = 4096,
endpointTokenConfig,
useRealTokenLookup = false,
providerTools,
loadedToolDefinitions = [],
structuredTools = [],
} = overrides ?? {};
const resolvedOverrideProvider = overrideProvider ?? provider;
@ -158,6 +168,7 @@ function createMocks(overrides?: {
const mockGetOptions = jest.fn().mockResolvedValue({
llmConfig: { model, maxTokens: maxOutputTokens },
endpointTokenConfig,
...(providerTools !== undefined ? { tools: providerTools } : {}),
} satisfies InitializeResultBase);
mockGetProviderConfig.mockReturnValue({
@ -181,12 +192,12 @@ function createMocks(overrides?: {
mockOptionalChainWithEmptyCheck.mockImplementation(realUtils.optionalChainWithEmptyCheck);
const loadTools = jest.fn().mockResolvedValue({
tools: [],
tools: structuredTools,
toolContextMap: {},
dynamicToolContextMap: {},
userMCPAuthMap: undefined,
toolRegistry: undefined,
toolDefinitions: [],
toolDefinitions: loadedToolDefinitions,
hasDeferredTools: false,
});
@ -202,6 +213,27 @@ function createMocks(overrides?: {
return { agent, req, res, loadTools, db };
}
function countNamedWebSearchTools(tools: unknown[] | undefined): number {
return (
tools?.filter((tool) => {
if (tool == null || typeof tool !== 'object') {
return false;
}
const { name } = tool as { name?: unknown };
return name === Tools.web_search;
}).length ?? 0
);
}
function countWebSearchDefinitions(
toolDefinitions: Array<{ name: string }> | undefined,
): number {
return (
toolDefinitions?.filter((toolDefinition) => toolDefinition.name === Tools.web_search).length ??
0
);
}
describe('initializeAgent — custom provider token lookup', () => {
const CUSTOM_PROVIDER = 'EduGPT';
@ -276,6 +308,123 @@ describe('initializeAgent — custom provider token lookup', () => {
});
});
describe('initializeAgent — Anthropic web_search precedence', () => {
const nativeWebSearchTool = {
type: 'web_search_20250305',
name: Tools.web_search,
};
const libreChatWebSearchDefinition = {
name: Tools.web_search,
description: 'Search the web',
parameters: { type: 'object', properties: {} },
};
beforeEach(() => {
jest.clearAllMocks();
});
it('keeps Anthropic native web_search when LibreChat search is not selected', async () => {
const { agent, req, res, loadTools, db } = createMocks({
provider: Providers.ANTHROPIC,
providerTools: [nativeWebSearchTool],
});
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.ANTHROPIC]),
isInitialAgent: true,
},
db,
);
expect(result.tools).toEqual([nativeWebSearchTool]);
expect(countNamedWebSearchTools(result.tools)).toBe(1);
expect(countWebSearchDefinitions(result.toolDefinitions)).toBe(0);
});
it('keeps LibreChat web_search definitions when native Anthropic search is not enabled', async () => {
const { agent, req, res, loadTools, db } = createMocks({
provider: Providers.ANTHROPIC,
loadedToolDefinitions: [libreChatWebSearchDefinition],
});
agent.tools = [Tools.web_search];
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.ANTHROPIC]),
isInitialAgent: true,
},
db,
);
expect(result.tools).toEqual([]);
expect(countNamedWebSearchTools(result.tools)).toBe(0);
expect(countWebSearchDefinitions(result.toolDefinitions)).toBe(1);
});
it('prefers LibreChat web_search when Anthropic native search is also enabled', async () => {
const { agent, req, res, loadTools, db } = createMocks({
provider: Providers.ANTHROPIC,
providerTools: [nativeWebSearchTool],
loadedToolDefinitions: [libreChatWebSearchDefinition],
});
agent.tools = [Tools.web_search];
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.ANTHROPIC]),
isInitialAgent: true,
},
db,
);
expect(result.tools).toEqual([]);
expect(countNamedWebSearchTools(result.tools)).toBe(0);
expect(countWebSearchDefinitions(result.toolDefinitions)).toBe(1);
});
it('leaves non-Anthropic providers unchanged', async () => {
const { agent, req, res, loadTools, db } = createMocks({
provider: Providers.OPENAI,
providerTools: [nativeWebSearchTool],
loadedToolDefinitions: [libreChatWebSearchDefinition],
});
agent.tools = [Tools.web_search];
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.OPENAI]),
isInitialAgent: true,
},
db,
);
expect(result.tools).toEqual([nativeWebSearchTool]);
expect(countNamedWebSearchTools(result.tools)).toBe(1);
expect(countWebSearchDefinitions(result.toolDefinitions)).toBe(1);
});
});
describe('initializeAgent — stable and dynamic instruction fields', () => {
beforeEach(() => {
jest.clearAllMocks();

View file

@ -65,6 +65,53 @@ function appendAdditionalInstructions(agent: Agent, text?: string | null): void
.join('\n\n');
}
function getToolName(tool: unknown): string | undefined {
if (tool == null || typeof tool !== 'object') {
return undefined;
}
const { name } = tool as { name?: unknown };
return typeof name === 'string' ? name : undefined;
}
function hasToolDefinition(toolDefinitions: LCTool[] | undefined, name: string): boolean {
return toolDefinitions?.some((toolDefinition) => toolDefinition.name === name) === true;
}
function resolveAnthropicToolConflicts({
provider,
tools,
toolDefinitions,
}: {
provider?: string;
tools?: unknown[];
toolDefinitions?: LCTool[];
}): unknown[] | undefined {
if (provider !== Providers.ANTHROPIC || !tools?.length) {
return tools;
}
if (!hasToolDefinition(toolDefinitions, Tools.web_search)) {
return tools;
}
let removed = 0;
const resolvedTools = tools.filter((tool) => {
const shouldRemove = getToolName(tool) === Tools.web_search;
if (shouldRemove) {
removed += 1;
}
return !shouldRemove;
});
if (removed > 0) {
logger.debug(
`[initializeAgent] Removed ${removed} Anthropic native web_search tool(s); LibreChat web_search is enabled.`,
);
}
return resolvedTools;
}
/**
* Extended agent type with additional fields needed after initialization
*/
@ -825,14 +872,20 @@ export async function initializeAgent(
/** Check for tool presence from either full instances or definitions (event-driven mode) */
const hasAgentTools = (structuredTools?.length ?? 0) > 0 || (toolDefinitions?.length ?? 0) > 0;
const providerTools = resolveAnthropicToolConflicts({
provider: agent.provider,
tools: options.tools,
toolDefinitions,
});
const hasProviderTools = (providerTools?.length ?? 0) > 0;
let tools: GenericTool[] = options.tools?.length
? (options.tools as GenericTool[])
let tools: GenericTool[] = hasProviderTools
? (providerTools as GenericTool[])
: (structuredTools ?? []);
if (
(agent.provider === Providers.GOOGLE || agent.provider === Providers.VERTEXAI) &&
options.tools?.length &&
hasProviderTools &&
hasAgentTools
) {
throw new Error(`{ "type": "${ErrorTypes.GOOGLE_TOOL_CONFLICT}"}`);
@ -840,10 +893,10 @@ export async function initializeAgent(
(agent.provider === Providers.OPENAI ||
agent.provider === Providers.AZURE ||
agent.provider === Providers.ANTHROPIC) &&
options.tools?.length &&
hasProviderTools &&
structuredTools?.length
) {
tools = structuredTools.concat(options.tools as GenericTool[]);
tools = structuredTools.concat(providerTools as GenericTool[]);
}
agent.model_parameters = { ...options.llmConfig } as Agent['model_parameters'];