revert: remove native web search approval handling

Native Anthropic web_search executes server-side before we can intercept
it, making pre-execution approval impossible. Removing all native web
search approval code (dropParams in OpenAI/Anthropic initialize,
handleNativeWebSearchApproval in callbacks, toolApprovalConfig in
getDefaultHandlers). Tool approval remains fully functional for MCP
tools and LangChain tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aron Gates 2026-03-09 14:10:24 +00:00
parent 5ee53e258f
commit f713f9f43a
No known key found for this signature in database
GPG key ID: 4F5BDD01E0CFE2A0
3 changed files with 1 additions and 132 deletions

View file

@ -1,27 +1,16 @@
const { nanoid } = require('nanoid');
const { logger } = require('@librechat/data-schemas');
const { Tools, StepTypes, FileContext, ErrorTypes, EModelEndpoint } = require('librechat-data-provider');
const {
EnvVar,
Constants,
GraphEvents,
GraphNodeKeys,
ToolEndHandler,
} = require('@librechat/agents');
const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider');
const {
sendEvent,
requiresApproval,
GenerationJobManager,
writeAttachmentEvent,
createToolExecuteHandler,
MCPToolCallValidationHandler,
} = require('@librechat/api');
const { processFileCitations } = require('~/server/services/Files/Citations');
const { processCodeOutput } = require('~/server/services/Files/Code/process');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { saveBase64Image } = require('~/server/services/Files/process');
const { getFlowStateManager } = require('~/config');
const { getLogStores } = require('~/cache');
class ModelEndHandler {
/**
@ -120,78 +109,6 @@ async function emitEvent(res, streamId, eventData) {
}
}
/**
* Checks if a tool call is a native Anthropic web search (server_tool_use).
* @param {Object} toolCall - The tool call object
* @returns {boolean}
*/
function isNativeWebSearch(toolCall) {
return toolCall?.name === Tools.web_search && toolCall?.id?.startsWith('srvtoolu_');
}
/**
* Handles approval flow for native web search tool calls.
* @param {Object} params
* @param {Object} params.toolCall - The tool call requiring approval
* @param {Object} params.toolApprovalConfig - The tool approval configuration
* @param {ServerResponse} params.res - The response object for SSE
* @param {string | null} params.streamId - The stream ID for resumable mode
* @param {string} params.stepId - The step ID
* @param {string} params.userId - The user ID
* @param {AbortSignal} [params.signal] - Optional abort signal
* @returns {Promise<void>}
*/
async function handleNativeWebSearchApproval({
toolCall,
toolApprovalConfig,
res,
streamId,
stepId,
userId,
signal,
}) {
const needsApproval = requiresApproval(Tools.web_search, toolApprovalConfig);
if (!needsApproval) {
return;
}
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
const derivedSignal = signal ? AbortSignal.any([signal]) : undefined;
const toolArgs = toolCall.args || {};
const { validationId, flowMetadata } =
await MCPToolCallValidationHandler.initiateValidationFlow(
userId,
'anthropic',
Tools.web_search,
typeof toolArgs === 'string' ? { input: toolArgs } : toolArgs,
);
const validationData = {
id: stepId,
delta: {
type: StepTypes.TOOL_CALLS,
tool_calls: [{ id: toolCall.id, name: toolCall.name, args: '' }],
validation: validationId,
expires_at: Date.now() + Time.TEN_MINUTES,
},
};
await emitEvent(res, streamId, { event: GraphEvents.ON_RUN_STEP_DELTA, data: validationData });
const validationFlowType = MCPToolCallValidationHandler.getFlowType();
await flowManager.createFlow(validationId, validationFlowType, flowMetadata, derivedSignal);
const successData = {
id: stepId,
delta: {
type: StepTypes.TOOL_CALLS,
tool_calls: [{ id: toolCall.id, name: toolCall.name }],
},
};
await emitEvent(res, streamId, { event: GraphEvents.ON_RUN_STEP_DELTA, data: successData });
}
/**
* @typedef {Object} ToolExecuteOptions
* @property {(toolNames: string[]) => Promise<{loadedTools: StructuredTool[]}>} loadTools - Function to load tools by name
@ -218,14 +135,12 @@ function getDefaultHandlers({
streamId = null,
toolExecuteOptions = null,
summarizationOptions = null,
toolApprovalConfig,
}) {
if (!res || !aggregateContent) {
throw new Error(
`[getDefaultHandlers] Missing required options: res: ${!res}, aggregateContent: ${!aggregateContent}`,
);
}
const pendingNativeWebSearchApprovals = new Set();
const handlers = {
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
[GraphEvents.TOOL_END]: new ToolEndHandler(toolEndCallback, logger),
@ -237,24 +152,6 @@ function getDefaultHandlers({
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: async (event, data, metadata) => {
if (data?.stepDetails?.type === StepTypes.TOOL_CALLS && toolApprovalConfig) {
const toolCalls = data.stepDetails.tool_calls || [];
for (const toolCall of toolCalls) {
if (isNativeWebSearch(toolCall) && !pendingNativeWebSearchApprovals.has(toolCall.id)) {
pendingNativeWebSearchApprovals.add(toolCall.id);
await handleNativeWebSearchApproval({
toolCall,
toolApprovalConfig,
res,
streamId,
stepId: data.id,
userId: metadata?.user_id || metadata?.user?.id,
signal: metadata?.signal,
});
}
}
}
aggregateContent({ event, data });
if (data?.stepDetails.type === StepTypes.TOOL_CALLS) {
await emitEvent(res, streamId, { event, data });

View file

@ -147,7 +147,6 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
const summarizationOptions =
appConfig?.summarization?.enabled === false ? { enabled: false } : { enabled: true };
const toolApprovalConfig = appConfig?.endpoints?.[EModelEndpoint.agents]?.toolApproval;
const eventHandlers = getDefaultHandlers({
res,
toolExecuteOptions,
@ -156,7 +155,6 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
toolEndCallback,
collectedUsage,
streamId,
toolApprovalConfig,
});
if (!endpointOption.agent) {

View file

@ -1,5 +1,4 @@
import { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } from 'librechat-data-provider';
import type { TToolApproval } from 'librechat-data-provider';
import type {
BaseInitializeParams,
InitializeResultBase,
@ -10,23 +9,6 @@ import { getAzureCredentials, resolveHeaders, isUserProvided, checkUserKeyExpiry
import { validateEndpointURL } from '~/auth';
import { getOpenAIConfig } from './config';
function shouldDisableNativeWebSearch(toolApproval: TToolApproval | undefined): boolean {
if (!toolApproval) {
return false;
}
const { required, excluded } = toolApproval;
if (excluded?.includes('web_search')) {
return false;
}
if (required === true) {
return true;
}
if (Array.isArray(required) && required.includes('web_search')) {
return true;
}
return false;
}
/**
* Initializes OpenAI options for agent usage. This function always returns configuration
* options and never creates a client instance (equivalent to optionsOnly=true behavior).
@ -151,14 +133,6 @@ export async function initializeOpenAI({
user: req.user?.id,
};
const toolApproval = appConfig?.endpoints?.[EModelEndpoint.agents]?.toolApproval;
if (shouldDisableNativeWebSearch(toolApproval)) {
clientOptions.dropParams = clientOptions.dropParams ?? [];
if (!clientOptions.dropParams.includes('web_search')) {
clientOptions.dropParams.push('web_search');
}
}
const finalClientOptions: OpenAIConfigOptions = {
...clientOptions,
modelOptions,