mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-09 17:31:19 +00:00
🔍 fix: Prefer LibreChat Web Search over Anthropic's when Both Selected (#13166)
This commit is contained in:
parent
6466263493
commit
394839a76b
2 changed files with 210 additions and 8 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue