const { nanoid } = require('nanoid'); const { v4: uuidv4 } = require('uuid'); const { logger } = require('@librechat/data-schemas'); const { Callback, ToolEndHandler, formatAgentMessages } = require('@librechat/agents'); const { EModelEndpoint, ResourceType, PermissionBits, hasPermissions, AgentCapabilities, } = require('librechat-data-provider'); const { createRun, applyContextToAgent, buildInitialToolSessions, buildToolSet, AgentRunEnvelopeError, createAgentRunEnvelope, createMCPRuntimeRequestBody, buildAgentScopedContext, buildInlineMemoryContext, buildAgentContextAttachmentsByAgentId, createSafeUser, initializeAgent, loadSkillStates, getBalanceConfig, injectSkillPrimes, extractManualSkills, recordCollectedUsage, createSubagentUsageSink, getTransactionsConfig, resolveAgentTokenConfig, resolveSubagentGraphs, inspectContent, extractAgentContent, extractFileContent, extractMessageContent, extractModelParameterContent, extractSkillContent, extractToolArgumentContent, contentFilterBlockResponse, contentFilterUninspectableResponse, discoverConnectedAgents, getBlockedOpaqueFileField, getContentTraversalFragments, isContentTraversalProtected, isContentTraversalLimitError, prependContentTraversalFragments, assertModelBoundContent, hasModelBoundContentProtection, isContentFilterError, getSafeErrorMetadata, createToolExecuteHandler, getRemoteAgentPermissions, resolveAgentScopedSkillIds, // Responses API writeDone, buildResponse, generateResponseId, isValidationFailure, emitResponseCreated, createResponseContext, createResponseTracker, setupStreamingResponse, emitResponseInProgress, convertInputToMessages, validateResponseRequest, buildAggregatedResponse, buildResponsesUsage, createResponseAggregator, sendResponsesErrorResponse, createResponsesEventHandlers, createAggregatorEventHandlers, getLangfuseTraceMessageFields, stripActivityLabelParts, CHILD_THREAD_READ_ONLY_ERROR, } = require('@librechat/api'); const { createResponsesToolEndCallback, buildSummarizationHandlers, contextualizeModelUsage, createToolEndCallback, agentLogHandlerObj, } = require('~/server/controllers/agents/callbacks'); const { loadAgentTools, loadToolsForExecution, isFatalAgentInitializationError, } = require('~/server/services/ToolService'); const { findAccessibleResources, getEffectivePermissions, } = require('~/server/services/PermissionService'); const { getSkillToolDeps, getSkillDbMethods, canAuthorSkillFiles, withDeploymentSkillIds, buildAgentToolContext, resolveMemoryAvailability, enrichLoadedToolsWithAgentContext, } = require('~/server/services/Endpoints/agents/skillDeps'); const { getModelsConfig } = require('~/server/controllers/ModelController'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { resolveConfigServers, getAccessibleMcpServerNames } = require('~/server/services/MCP'); const { resolveConversationTitle } = require('~/server/services/Endpoints/titlePolicy'); const { getMCPManager } = require('~/config'); const { logViolation } = require('~/cache'); const db = require('~/models'); const filterFilesByRemoteAgentAccess = (params) => filterFilesByAgentAccess({ ...params, resourceType: ResourceType.REMOTE_AGENT }); const GENERIC_PROVIDER_ERROR = 'An error occurred while processing the request'; function getUserFacingProviderError(error, protectionEnabled) { if (protectionEnabled) { return GENERIC_PROVIDER_ERROR; } return error instanceof Error ? error.message : 'An error occurred'; } /** * Creates a tool loader function for the agent. * @param {AbortSignal} signal - The abort signal * @param {boolean} [definitionsOnly=true] - When true, returns only serializable * tool definitions without creating full tool instances (for event-driven mode) */ function createToolLoader(signal, definitionsOnly = true) { return async function loadTools({ req, res, tools, model, agentId, provider, tool_options, tool_resources, requestBody, codeExecutionContext, accessibleMcpServerNames, }) { const agent = { id: agentId, tools, provider, model, tool_options }; try { return await loadAgentTools({ req, res, agent, signal, requestBody, tool_resources, codeExecutionContext, agentResourceType: ResourceType.REMOTE_AGENT, definitionsOnly, accessibleMcpServerNames, streamId: null, }); } catch (error) { if (isFatalAgentInitializationError(error) || isContentFilterError(error)) { throw error; } logger.error('Error loading tools for agent ' + agentId, getSafeErrorMetadata(error)); } }; } /** * Convert Open Responses input items to internal messages * @param {import('@librechat/api').InputItem[]} input * @returns {Array} Internal messages */ function convertToInternalMessages(input) { return convertInputToMessages(input); } /** * Collect file-derived context exactly as it will be exposed to the model. * Dynamic tool context uses the same synthesis as packages/api/src/agents/run.ts. * @param {Array} agents * @returns {Array} */ function collectModelBoundAgentFiles(agents) { const files = []; const seenFiles = new Set(); for (const agent of agents) { for (const attachment of [ ...(agent?.attachments ?? []), ...(agent?.requestAttachments ?? []), ...(agent?.agentContextAttachments ?? []), ]) { if (attachment == null || seenFiles.has(attachment)) { continue; } seenFiles.add(attachment); files.push(attachment); } const dynamicToolInstructions = Object.values(agent?.dynamicToolContextMap ?? {}) .filter((value) => typeof value === 'string' && value !== '') .join('\n') .trim(); if (dynamicToolInstructions !== '') { files.push({ content: dynamicToolInstructions }); } } return files; } function extractResponseRequestContent(request, messageFragments) { const fragments = [ ...extractAgentContent({ instructions: request.instructions }), ...messageFragments, ]; if (Array.isArray(request.input)) { for (const item of request.input) { if (item?.type !== 'message' || !Array.isArray(item.content)) { continue; } for (const part of item.content) { if (part?.type === 'input_file') { fragments.push(...extractFileContent({ name: part.filename })); continue; } if ( part?.type === 'input_image' && typeof part.image_url === 'string' && !part.image_url.startsWith('data:') ) { fragments.push(...extractFileContent({ uri: part.image_url })); } } } } for (const tool of request.tools ?? []) { if (tool?.type !== 'function') { continue; } fragments.push( ...extractAgentContent({ name: tool.name, description: tool.description, }), ); try { fragments.push(...extractToolArgumentContent({ arguments: tool.parameters })); } catch (error) { if (isContentTraversalLimitError(error)) { prependContentTraversalFragments(error, fragments); } throw error; } } try { fragments.push( ...extractModelParameterContent({ metadata: request.metadata, response_format: request.text?.format, additionalModelRequestFields: { user: request.user, tool_choice: request.tool_choice, reasoning: request.reasoning, }, }), ); } catch (error) { if (isContentTraversalLimitError(error)) { prependContentTraversalFragments(error, fragments); } throw error; } return fragments; } /** * Load messages from a previous response/conversation * @param {string} conversationId - The conversation/response ID * @param {string} userId - The user ID * @returns {Promise} Messages from the conversation */ async function loadPreviousMessages(conversationId, userId) { try { const messages = await db.getMessages({ conversationId, user: userId }); if (!messages || messages.length === 0) { return []; } // Convert stored messages to internal format return messages.map((msg) => { let text; if (typeof msg.text === 'string') { text = msg.text; } else if (msg.text != null) { text = String(msg.text); } const internalMsg = { role: msg.isCreatedByUser ? 'user' : 'assistant', content: Array.isArray(msg.content) ? msg.content : (text ?? ''), messageId: msg.messageId, isCreatedByUser: msg.isCreatedByUser === true, ...(text !== undefined && { text }), ...(typeof msg.isUserSubmitted === 'boolean' && { isUserSubmitted: msg.isUserSubmitted, }), ...(Array.isArray(msg.userSubmittedPaths) && { userSubmittedPaths: msg.userSubmittedPaths, }), ...(Array.isArray(msg.userSubmittedMessageFieldPaths) && { userSubmittedMessageFieldPaths: msg.userSubmittedMessageFieldPaths, }), }; return internalMsg; }); } catch (error) { logger.error('[Responses API] Error loading previous messages:', getSafeErrorMetadata(error)); return []; } } /** * Save input messages to database * @param {import('express').Request} req * @param {string} conversationId * @param {Array} inputMessages - Internal format messages * @param {string} agentId * @returns {Promise} */ async function saveInputMessages(req, conversationId, inputMessages, agentId) { for (const msg of inputMessages) { if (msg.role === 'user') { await db.saveMessage( req, { messageId: msg.messageId || nanoid(), conversationId, parentMessageId: null, isCreatedByUser: true, text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), sender: 'User', endpoint: EModelEndpoint.agents, model: agentId, }, { context: 'Responses API - save user input' }, ); } } } /** * Save response output to database * @param {import('express').Request} req * @param {string} conversationId * @param {string} responseId * @param {import('@librechat/api').Response} response * @param {string} agentId * @param {number | undefined} visibleOutputTokens * @returns {Promise} */ async function saveResponseOutput( req, conversationId, responseId, response, agentId, visibleOutputTokens, ) { // Extract text content from output items let responseText = ''; for (const item of response.output) { if (item.type === 'message' && item.content) { for (const part of item.content) { if (part.type === 'output_text' && part.text) { responseText += part.text; } } } } const langfuseTraceFields = await getLangfuseTraceMessageFields(req.config, responseId); // Save the assistant message await db.saveMessage( req, { messageId: responseId, conversationId, parentMessageId: null, isCreatedByUser: false, ...langfuseTraceFields, text: responseText, sender: 'Agent', endpoint: EModelEndpoint.agents, model: agentId, finish_reason: response.status === 'completed' ? 'stop' : response.status, tokenCount: visibleOutputTokens ?? response.usage?.output_tokens, }, { context: 'Responses API - save assistant response' }, ); } /** * Save or update conversation * @param {import('express').Request} req * @param {string} conversationId * @param {string} agentId * @param {object} agent * @returns {Promise} */ async function saveConversation(req, conversationId, agentId, agent) { const title = resolveConversationTitle(req, agent?.name || 'Open Responses Conversation'); await db.saveConvo( { userId: req?.user?.id, isTemporary: req?.body?.isTemporary, interfaceConfig: req?.config?.interfaceConfig, }, { conversationId, endpoint: EModelEndpoint.agents, agentId, ...(title != null && { title }), model: agent?.model, }, { context: 'Responses API - save conversation' }, ); } /** * Convert stored messages to Open Responses output format * @param {Array} messages - Stored messages * @returns {Array} Output items */ function convertMessagesToOutputItems(messages) { const output = []; for (const msg of messages) { if (!msg.isCreatedByUser) { output.push({ type: 'message', id: msg.messageId, role: 'assistant', status: 'completed', content: [ { type: 'output_text', text: msg.text || '', annotations: [], }, ], }); } } return output; } /** * Runs a validated Responses envelope in the current process. * Express remains runtime-only state while the envelope is the portable run input. * * @param {import('@librechat/api').ResponsesRunEnvelope} envelope * @param {{req: import('express').Request, res: import('express').Response}} runtime */ const executeResponse = async (envelope, { req, res }) => { const appConfig = req.config; const requestStartTime = envelope.receivedAt; const request = envelope.payload; const { principal } = envelope; // The local executor keeps the current Express-dependent initialization path, // but all request-body reads now observe the detached envelope payload. req.body = request; const agentId = request.model; const manualSkills = extractManualSkills(req.body); const isStreaming = request.stream === true; const summarizationConfig = appConfig?.summarization; const uninspectableField = getBlockedOpaqueFileField(appConfig?.filters, request.input); if (uninspectableField != null) { const blockResponse = contentFilterUninspectableResponse(uninspectableField); return sendResponsesErrorResponse( res, 400, blockResponse.message, 'invalid_request', blockResponse.error, ); } const inputMessages = convertToInternalMessages( typeof request.input === 'string' ? request.input : request.input, ); const messageFragments = []; const traversalErrors = []; try { for (const fragment of extractMessageContent(inputMessages)) { messageFragments.push(fragment); } } catch (error) { if (!isContentTraversalLimitError(error)) { throw error; } messageFragments.push(...getContentTraversalFragments(error)); traversalErrors.push(error); } let requestFragments; try { requestFragments = extractResponseRequestContent(request, messageFragments); } catch (error) { if (!isContentTraversalLimitError(error)) { throw error; } requestFragments = getContentTraversalFragments(error); traversalErrors.push(error); } const contentFinding = inspectContent( [...requestFragments, ...(manualSkills ?? []).flatMap((name) => extractSkillContent({ name }))], { filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, }, ); if (contentFinding != null) { const isLegacyFilter = contentFinding.detectorId === 'legacy-pattern'; const blockResponse = contentFilterBlockResponse(contentFinding); return sendResponsesErrorResponse( res, 400, isLegacyFilter ? `Message contains a ${contentFinding.label}. Remove it and try again.` : blockResponse.message, 'invalid_request', isLegacyFilter ? 'message_filter_pii_block' : blockResponse.error, ); } const traversalError = traversalErrors.find((error) => isContentTraversalProtected({ error, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, roles: inputMessages.map((message) => message?.role), }), ); if (traversalError != null) { return sendResponsesErrorResponse( res, traversalError.statusCode, traversalError.body.message, 'invalid_request', traversalError.body.error, ); } // Look up the agent const agent = await db.getAgent({ id: agentId }); if (!agent) { return sendResponsesErrorResponse( res, 404, `Agent not found: ${agentId}`, 'not_found', 'model_not_found', ); } // Generate IDs const responseId = generateResponseId(); const context = createResponseContext(request, responseId); logger.debug( `[Responses API] Request ${responseId} started for agent ${agentId}, stream: ${isStreaming}`, ); // Set up abort controller const abortController = new AbortController(); // Handle client disconnect req.on('close', () => { if (!abortController.signal.aborted) { abortController.abort(); logger.debug('[Responses API] Client disconnected, aborting'); } }); try { if (request.previous_response_id != null) { if (typeof request.previous_response_id !== 'string') { return sendResponsesErrorResponse( res, 400, 'previous_response_id must be a string', 'invalid_request', ); } const previousConversation = await db.getConvo( principal.userId, request.previous_response_id, ); if (!previousConversation) { return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found'); } if (previousConversation.subagentThread != null) { return sendResponsesErrorResponse( res, 409, CHILD_THREAD_READ_ONLY_ERROR, 'invalid_request', 'conversation_read_only', ); } } const conversationId = request.previous_response_id ?? uuidv4(); const parentMessageId = null; const mcpRequestBody = createMCPRuntimeRequestBody({ messageId: responseId, conversationId }); const agentsEConfig = appConfig?.endpoints?.[EModelEndpoint.agents]; const previousMessages = request.previous_response_id ? await loadPreviousMessages(request.previous_response_id, principal.userId) : []; if (request.previous_response_id) { assertModelBoundContent({ filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, storedMessages: previousMessages, }); } // Build allowed providers set const allowedProviders = new Set(agentsEConfig?.allowedProviders); // Create tool loader const loadTools = createToolLoader(abortController.signal); const skillDbMethods = getSkillDbMethods(); // Initialize the agent first to check for disableStreaming const endpointOption = { endpoint: agent.provider, model_parameters: agent.model_parameters ?? {}, }; const dbMethods = { getConvoFiles: db.getConvoFiles, getFiles: db.getFiles, filterFilesByAgentAccess: filterFilesByRemoteAgentAccess, getUserKey: db.getUserKey, getMessages: db.getMessages, getAccessibleMcpServerNames, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, getUserCodeFiles: db.getUserCodeFiles, getToolFilesByIds: db.getToolFilesByIds, getCodeGeneratedFiles: db.getCodeGeneratedFiles, listSkillsByAccess: skillDbMethods.listSkillsByAccess, listAlwaysApplySkills: skillDbMethods.listAlwaysApplySkills, getSkillByName: skillDbMethods.getSkillByName, }; const enabledCapabilities = new Set(agentsEConfig?.capabilities); const memoryAvailable = await resolveMemoryAvailability({ enabledCapabilities, memoryConfig: appConfig?.memory, user: req.user, getRoleByName: db.getRoleByName, }); const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills); const ephemeralSkillsToggle = request.ephemeralAgent?.skills === true; const accessibleSkillIds = skillsCapabilityEnabled ? withDeploymentSkillIds( await findAccessibleResources({ userId: principal.userId, role: principal.role, resourceType: ResourceType.SKILL, requiredPermissions: PermissionBits.VIEW, }), ) : []; const editableSkillIds = skillsCapabilityEnabled ? await findAccessibleResources({ userId: principal.userId, role: principal.role, resourceType: ResourceType.SKILL, requiredPermissions: PermissionBits.EDIT, }) : []; const skillCreateAllowed = skillsCapabilityEnabled ? await getSkillToolDeps().canCreateSkill({ req }) : false; const { skillStates, defaultActiveOnShare } = await loadSkillStates({ userId: principal.userId, appConfig, getUserById: db.getUserById, accessibleSkillIds, }); const primaryScopedSkillIds = resolveAgentScopedSkillIds({ agent, accessibleSkillIds, skillsCapabilityEnabled, ephemeralSkillsToggle, }); const primaryScopedEditableSkillIds = resolveAgentScopedSkillIds({ agent, accessibleSkillIds: editableSkillIds, skillsCapabilityEnabled, ephemeralSkillsToggle, }); const primaryConfig = await initializeAgent( { req, res, loadTools, requestFiles: [], conversationId, parentMessageId, requestBody: mcpRequestBody, agent, endpointOption, allowedProviders, isInitialAgent: true, accessibleSkillIds: primaryScopedSkillIds, skillAuthoringAvailable: canAuthorSkillFiles({ agent, scopedEditableSkillIds: primaryScopedEditableSkillIds, skillCreateAllowed, skillsCapabilityEnabled, ephemeralSkillsToggle, }), codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), toolIntentsAvailable: enabledCapabilities.has(AgentCapabilities.tool_intents), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), allowedStatefulCodeEnvironments: agentsEConfig?.statefulCodeSessions?.allowedEnvironments, memoryAvailable, skillStates, defaultActiveOnShare, manualSkills, }, dbMethods, ); /** * Per-agent tool-execution context map, keyed by agentId. Ensures the * ON_TOOL_EXECUTE callback routes each sub-agent's tool calls to the * correct toolRegistry / userMCPAuthMap / tool_resources. * @type {Map>, * tool_resources?: object, * actionsEnabled?: boolean, * }>} */ const agentToolContexts = new Map(); agentToolContexts.set( primaryConfig.id, buildAgentToolContext({ agent, config: primaryConfig }), ); let handoffAgentConfigs = new Map(); let discoveredEdges = []; let discoveredMCPAuthMap; const subagentsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.subagents); const primaryHasGraphSubagents = subagentsCapabilityEnabled && primaryConfig.subagents?.enabled === true && (primaryConfig.subagents.graphs?.length ?? 0) > 0; if (primaryConfig.edges?.length || primaryHasGraphSubagents) { const modelsConfig = await getModelsConfig(req); const discoveryParams = { req, res, primaryConfig, endpointOption, allowedProviders, modelsConfig, loadTools, requestFiles: [], conversationId, parentMessageId, requestBody: mcpRequestBody, resourceType: ResourceType.REMOTE_AGENT, computeAccessibleSkillIds: (handoffAgent) => resolveAgentScopedSkillIds({ agent: handoffAgent, accessibleSkillIds, skillsCapabilityEnabled, ephemeralSkillsToggle, }), computeSkillAuthoringAvailable: (handoffAgent) => canAuthorSkillFiles({ agent: handoffAgent, scopedEditableSkillIds: resolveAgentScopedSkillIds({ agent: handoffAgent, accessibleSkillIds: editableSkillIds, skillsCapabilityEnabled, ephemeralSkillsToggle, }), skillCreateAllowed, skillsCapabilityEnabled, ephemeralSkillsToggle, }), skillStates, defaultActiveOnShare, codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), backgroundToolsAvailable: enabledCapabilities.has(AgentCapabilities.run_in_background), toolIntentsAvailable: enabledCapabilities.has(AgentCapabilities.tool_intents), statefulSessionsAvailable: enabledCapabilities.has( AgentCapabilities.stateful_code_sessions, ), allowedStatefulCodeEnvironments: agentsEConfig?.statefulCodeSessions?.allowedEnvironments, memoryAvailable, }; const discoveryDeps = { getAgent: db.getAgent, checkPermission: async ({ userId, role, resourceId, requiredPermission }) => { const permissions = await getRemoteAgentPermissions( { getEffectivePermissions }, userId, role, resourceId, ); return hasPermissions(permissions, requiredPermission); }, logViolation, db: dbMethods, onAgentInitialized: (loadedAgentId, loadedAgent, config) => { agentToolContexts.set( loadedAgentId, buildAgentToolContext({ agent: loadedAgent, config }), ); }, initializeAgent, }; if (primaryConfig.edges?.length) { ({ agentConfigs: handoffAgentConfigs, edges: discoveredEdges, userMCPAuthMap: discoveredMCPAuthMap, } = await discoverConnectedAgents(discoveryParams, discoveryDeps)); } if (subagentsCapabilityEnabled) { discoveredMCPAuthMap = await resolveSubagentGraphs( { ...discoveryParams, rootConfigs: [primaryConfig, ...handoffAgentConfigs.values()], }, discoveryDeps, ); } } primaryConfig.edges = discoveredEdges; const endpointTokenConfigByAgentId = new Map(); for (const [agentId, context] of agentToolContexts) { endpointTokenConfigByAgentId.set(agentId, context.endpointTokenConfig); } const resolveEndpointTokenConfig = (usage) => resolveAgentTokenConfig({ agentId: usage?.agentId, byAgentId: endpointTokenConfigByAgentId, fallback: primaryConfig.endpointTokenConfig, }); const runAgents = [primaryConfig, ...handoffAgentConfigs.values()]; const initialSessions = buildInitialToolSessions({ agents: runAgents }); const modelBoundAgentsById = new Map(); const pendingModelBoundAgents = [...runAgents]; for (let index = 0; index < pendingModelBoundAgents.length; index++) { const runAgent = pendingModelBoundAgents[index]; if (!runAgent?.id || modelBoundAgentsById.has(runAgent.id)) { continue; } modelBoundAgentsById.set(runAgent.id, runAgent); for (const subagent of runAgent.subagentAgentConfigs?.values?.() ?? []) { pendingModelBoundAgents.push(subagent); } for (const graph of runAgent.subagentGraphConfigs ?? []) { pendingModelBoundAgents.push(...graph.memberConfigs); } } const modelBoundAgents = [...modelBoundAgentsById.values()]; const mergedMCPAuthMap = discoveredMCPAuthMap ?? primaryConfig.userMCPAuthMap; assertModelBoundContent({ filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, agents: modelBoundAgents, }); const agentContextAttachmentsByAgentId = buildAgentContextAttachmentsByAgentId(modelBoundAgents); const agentScopedContext = await buildAgentScopedContext({ agentIds: modelBoundAgents.map(({ id }) => id), attachmentsByAgentId: agentContextAttachmentsByAgentId, req, }); const mcpManager = getMCPManager(); const configServers = await resolveConfigServers(req); await Promise.all( modelBoundAgents.map(async (runAgent) => { const memoryContext = await buildInlineMemoryContext({ agent: runAgent, req, userId: principal.userId, memoryAvailable, getFormattedMemories: db.getFormattedMemories, }); return applyContextToAgent({ agent: runAgent, agentId: runAgent.id, logger, mcpManager, configServers, sharedRunContext: [memoryContext, agentScopedContext.get(runAgent.id)] .filter(Boolean) .join('\n\n'), }); }), ); // Determine if streaming is enabled (check both request and agent config) const streamingDisabled = !!primaryConfig.model_parameters?.disableStreaming; const actuallyStreaming = isStreaming && !streamingDisabled; // Merge previous messages with new input const allMessages = [...previousMessages, ...inputMessages]; const toolSet = buildToolSet(primaryConfig); const formatted = formatAgentMessages(stripActivityLabelParts(allMessages), {}, toolSet); const formattedMessages = formatted.messages; const initialSummary = formatted.summary; let indexTokenCountMap = formatted.indexTokenCountMap; /** * Inject manual + always-apply skill primes so the model sees SKILL.md * bodies for this turn — parity with AgentClient's chat path. The * Responses API uses its own response-builder shape, so LibreChat- * style card SSE events don't apply; only the message-context part * carries over. */ const manualSkillPrimes = primaryConfig.manualSkillPrimes; const alwaysApplySkillPrimes = primaryConfig.alwaysApplySkillPrimes; if ( (manualSkillPrimes && manualSkillPrimes.length > 0) || (alwaysApplySkillPrimes && alwaysApplySkillPrimes.length > 0) ) { const primeResult = injectSkillPrimes({ initialMessages: formattedMessages, indexTokenCountMap, manualSkillPrimes, alwaysApplySkillPrimes, }); indexTokenCountMap = primeResult.indexTokenCountMap; /* Surface the cap-driven always-apply truncation at the controller layer too — `injectSkillPrimes` already logs internally, but the controller-level warn includes endpoint context so operators can tell at a glance which path hit the cap. Mirrors AgentClient's warn in `client.js`. */ if (primeResult.alwaysApplyDropped > 0) { logger.warn( `[Responses API] Dropped ${primeResult.alwaysApplyDropped} always-apply prime(s) to stay within MAX_PRIMED_SKILLS_PER_TURN.`, ); } } assertModelBoundContent({ filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, submittedMessages: inputMessages, agents: modelBoundAgents, skills: [...(manualSkillPrimes ?? []), ...(alwaysApplySkillPrimes ?? [])], files: collectModelBoundAgentFiles(modelBoundAgents), }); /* Stable for the turn: the primary prime list is fixed once `initializeAgent` resolves and is used as the fallback when a specific agent context is unavailable. `codeEnvAvailable` is read per-agent from the stored tool context (admin cap AND that agent's `tools` list includes `execute_code`) — a skills-only agent never gains sandbox access even if the admin enabled the capability globally. */ // Create tracker for streaming or aggregator for non-streaming const tracker = actuallyStreaming ? createResponseTracker() : null; const aggregator = actuallyStreaming ? null : createResponseAggregator(); // Set up response for streaming if (actuallyStreaming) { setupStreamingResponse(res); // Create handler config const handlerConfig = { res, context, tracker, }; // Emit response.created then response.in_progress per Open Responses spec emitResponseCreated(handlerConfig); emitResponseInProgress(handlerConfig); // Create event handlers const { handlers: responsesHandlers, finalizeStream } = createResponsesEventHandlers(handlerConfig); // Collect usage for balance tracking const collectedUsage = []; // Artifact promises for processing tool outputs /** @type {Promise[]} */ const artifactPromises = []; // Use Responses API-specific callback that emits librechat:attachment events const toolEndCallback = createResponsesToolEndCallback({ req, res, tracker, artifactPromises, }); // Create tool execute options for event-driven tool execution const toolExecuteOptions = { loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => { const ctx = agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {}; const result = await loadToolsForExecution({ req, res, agentResourceType: ResourceType.REMOTE_AGENT, conversationId, requestBody: mcpRequestBody, toolNames, agent: ctx.agent ?? agent, signal: abortController.signal, toolRegistry: ctx.toolRegistry, callerCapabilityProjection, backgroundToolNames: ctx.backgroundToolNames, intentToolNames: ctx.intentToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, tool_resources: ctx.tool_resources, actionsEnabled: ctx.actionsEnabled, accessibleMcpServerNames: ctx.accessibleMcpServerNames, }); return enrichLoadedToolsWithAgentContext({ result, req, ctx, }); }, toolEndCallback, ...getSkillToolDeps(), }; // Combine handlers const handlers = { on_message_delta: responsesHandlers.on_message_delta, on_reasoning_delta: responsesHandlers.on_reasoning_delta, on_run_step: responsesHandlers.on_run_step, on_run_step_delta: responsesHandlers.on_run_step_delta, on_chat_model_end: { handle: (event, data, metadata, graph) => { responsesHandlers.on_chat_model_end.handle(event, data); const usage = data?.output?.usage_metadata; if (usage) { const agentContext = graph?.getAgentContext?.(metadata); const taggedUsage = contextualizeModelUsage(usage, metadata, agentContext); collectedUsage.push(taggedUsage); } }, }, on_tool_end: new ToolEndHandler(toolEndCallback, logger), on_run_step_completed: { handle: () => {} }, on_chain_stream: { handle: () => {} }, on_chain_end: { handle: () => {} }, on_agent_update: { handle: () => {} }, on_custom_event: { handle: () => {} }, on_tool_execute: createToolExecuteHandler(toolExecuteOptions), on_agent_log: agentLogHandlerObj, ...(summarizationConfig?.enabled !== false ? buildSummarizationHandlers({ isStreaming: actuallyStreaming, res }) : {}), }; // Create and run the agent const userId = principal.userId; const userMCPAuthMap = mergedMCPAuthMap; const run = await createRun({ agents: runAgents, messages: formattedMessages, indexTokenCountMap, initialSummary, runId: responseId, summarizationConfig, appConfig, signal: abortController.signal, customHandlers: handlers, initialSessions, requestBody: mcpRequestBody, user: { id: userId }, tenantId: principal.tenantId, /** Bills subagent child-run model calls (reported outside the * streamEvents loop) into the same collectedUsage array. */ subagentUsageSink: createSubagentUsageSink(collectedUsage), }); if (!run) { throw new Error('Failed to create agent run'); } // Process the stream const config = { runName: 'AgentRun', configurable: { thread_id: conversationId, user_id: userId, user: createSafeUser(req.user), requestBody: mcpRequestBody, ...(userMCPAuthMap != null && { userMCPAuthMap }), }, signal: abortController.signal, streamMode: 'values', version: 'v2', }; await run.processStream({ messages: formattedMessages }, config, { callbacks: { [Callback.TOOL_ERROR]: (graph, error, toolId) => { logger.error(`[Responses API] Tool Error "${toolId}"`, getSafeErrorMetadata(error)); }, }, }); // Record token usage against balance const balanceConfig = getBalanceConfig(appConfig); const transactionsConfig = getTransactionsConfig(appConfig); recordCollectedUsage( { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { user: userId, conversationId, collectedUsage, context: 'message', messageId: responseId, balance: balanceConfig, transactions: transactionsConfig, model: primaryConfig.model || agent.model_parameters?.model, endpointTokenConfig: primaryConfig.endpointTokenConfig, resolveEndpointTokenConfig, }, ).catch((err) => { logger.error('[Responses API] Error recording usage:', getSafeErrorMetadata(err)); }); const usage = buildResponsesUsage(collectedUsage); // Finalize the stream finalizeStream(usage); res.end(); const duration = Date.now() - requestStartTime; logger.debug(`[Responses API] Request ${responseId} completed in ${duration}ms (streaming)`); // Save to database if store: true if (request.store === true) { try { // Save conversation await saveConversation(req, conversationId, agentId, agent); // Save input messages await saveInputMessages(req, conversationId, inputMessages, agentId); // Build response for saving (use tracker with buildResponse for streaming) const finalResponse = buildResponse(context, tracker, 'completed'); await saveResponseOutput( req, conversationId, responseId, finalResponse, agentId, tracker.usage.outputTokens, ); logger.debug( `[Responses API] Stored response ${responseId} in conversation ${conversationId}`, ); } catch (saveError) { logger.error('[Responses API] Error saving response:', getSafeErrorMetadata(saveError)); // Don't fail the request if saving fails } } // Wait for artifact processing after response ends (non-blocking) if (artifactPromises.length > 0) { Promise.all(artifactPromises).catch((artifactError) => { logger.warn( '[Responses API] Error processing artifacts:', getSafeErrorMetadata(artifactError), ); }); } } else { const aggregatorHandlers = createAggregatorEventHandlers(aggregator); // Collect usage for balance tracking const collectedUsage = []; /** @type {Promise[]} */ const artifactPromises = []; const toolEndCallback = createToolEndCallback({ req, res, artifactPromises, streamId: null }); const toolExecuteOptions = { loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => { const ctx = agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {}; const result = await loadToolsForExecution({ req, res, agentResourceType: ResourceType.REMOTE_AGENT, conversationId, requestBody: mcpRequestBody, toolNames, agent: ctx.agent ?? agent, signal: abortController.signal, toolRegistry: ctx.toolRegistry, callerCapabilityProjection, backgroundToolNames: ctx.backgroundToolNames, intentToolNames: ctx.intentToolNames, mcpAvailableTools: ctx.mcpAvailableTools, requestScopedConnections: ctx.requestScopedConnections, userMCPAuthMap: ctx.userMCPAuthMap, tool_resources: ctx.tool_resources, actionsEnabled: ctx.actionsEnabled, accessibleMcpServerNames: ctx.accessibleMcpServerNames, }); return enrichLoadedToolsWithAgentContext({ result, req, ctx, }); }, toolEndCallback, ...getSkillToolDeps(), }; const handlers = { on_message_delta: aggregatorHandlers.on_message_delta, on_reasoning_delta: aggregatorHandlers.on_reasoning_delta, on_run_step: aggregatorHandlers.on_run_step, on_run_step_delta: aggregatorHandlers.on_run_step_delta, on_chat_model_end: { handle: (event, data, metadata, graph) => { aggregatorHandlers.on_chat_model_end.handle(event, data); const usage = data?.output?.usage_metadata; if (usage) { const agentContext = graph?.getAgentContext?.(metadata); const taggedUsage = contextualizeModelUsage(usage, metadata, agentContext); collectedUsage.push(taggedUsage); } }, }, on_tool_end: new ToolEndHandler(toolEndCallback, logger), on_run_step_completed: { handle: () => {} }, on_chain_stream: { handle: () => {} }, on_chain_end: { handle: () => {} }, on_agent_update: { handle: () => {} }, on_custom_event: { handle: () => {} }, on_tool_execute: createToolExecuteHandler(toolExecuteOptions), on_agent_log: agentLogHandlerObj, ...(summarizationConfig?.enabled !== false ? buildSummarizationHandlers({ isStreaming: false, res }) : {}), }; const userId = principal.userId; const userMCPAuthMap = mergedMCPAuthMap; const run = await createRun({ agents: runAgents, messages: formattedMessages, indexTokenCountMap, initialSummary, runId: responseId, summarizationConfig, appConfig, signal: abortController.signal, customHandlers: handlers, initialSessions, requestBody: mcpRequestBody, user: { id: userId }, tenantId: principal.tenantId, /** Bills subagent child-run model calls (reported outside the * streamEvents loop) into the same collectedUsage array. */ subagentUsageSink: createSubagentUsageSink(collectedUsage), }); if (!run) { throw new Error('Failed to create agent run'); } const config = { runName: 'AgentRun', configurable: { thread_id: conversationId, user_id: userId, user: createSafeUser(req.user), requestBody: mcpRequestBody, ...(userMCPAuthMap != null && { userMCPAuthMap }), }, signal: abortController.signal, streamMode: 'values', version: 'v2', }; await run.processStream({ messages: formattedMessages }, config, { callbacks: { [Callback.TOOL_ERROR]: (graph, error, toolId) => { logger.error(`[Responses API] Tool Error "${toolId}"`, getSafeErrorMetadata(error)); }, }, }); // Record token usage against balance const balanceConfig = getBalanceConfig(appConfig); const transactionsConfig = getTransactionsConfig(appConfig); recordCollectedUsage( { spendTokens: db.spendTokens, spendStructuredTokens: db.spendStructuredTokens, pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier }, bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, }, { user: userId, conversationId, collectedUsage, context: 'message', messageId: responseId, balance: balanceConfig, transactions: transactionsConfig, model: primaryConfig.model || agent.model_parameters?.model, endpointTokenConfig: primaryConfig.endpointTokenConfig, resolveEndpointTokenConfig, }, ).catch((err) => { logger.error('[Responses API] Error recording usage:', getSafeErrorMetadata(err)); }); if (artifactPromises.length > 0) { try { await Promise.all(artifactPromises); } catch (artifactError) { logger.warn( '[Responses API] Error processing artifacts:', getSafeErrorMetadata(artifactError), ); } } const response = buildAggregatedResponse( context, aggregator, buildResponsesUsage(collectedUsage), ); if (request.store === true) { try { await saveConversation(req, conversationId, agentId, agent); await saveInputMessages(req, conversationId, inputMessages, agentId); await saveResponseOutput( req, conversationId, responseId, response, agentId, aggregator.usage.outputTokens, ); logger.debug( `[Responses API] Stored response ${responseId} in conversation ${conversationId}`, ); } catch (saveError) { logger.error('[Responses API] Error saving response:', getSafeErrorMetadata(saveError)); // Don't fail the request if saving fails } } res.json(response); const duration = Date.now() - requestStartTime; logger.debug( `[Responses API] Request ${responseId} completed in ${duration}ms (non-streaming)`, ); } } catch (error) { logger.error('[Responses API] Error:', getSafeErrorMetadata(error)); const protectionEnabled = hasModelBoundContentProtection( appConfig?.filters, appConfig?.messageFilter?.pii, ); const errorMessage = getUserFacingProviderError(error, protectionEnabled); // Check if we already started streaming (headers sent) if (res.headersSent) { // Headers already sent, write error event and close writeDone(res); res.end(); } else { if (isContentFilterError(error)) { return sendResponsesErrorResponse( res, error.statusCode, error.body.message, 'invalid_request', error.body.error, ); } // Forward upstream provider status codes (e.g., Anthropic 400s) instead of masking as 500 const statusCode = typeof error?.status === 'number' && error.status >= 400 && error.status < 600 ? error.status : 500; const errorType = statusCode >= 400 && statusCode < 500 ? 'invalid_request' : 'server_error'; const errorCode = !protectionEnabled && typeof error?.code === 'string' ? error.code : undefined; if (errorCode === undefined) { sendResponsesErrorResponse(res, statusCode, errorMessage, errorType); } else { sendResponsesErrorResponse(res, statusCode, errorMessage, errorType, errorCode); } } } }; /** * Open Responses ingress adapter for agents. * Authentication and remote-agent authorization have already run in route middleware. * * POST /v1/responses * * @param {import('express').Request} req * @param {import('express').Response} res */ const createResponse = async (req, res) => { const receivedAt = Date.now(); const validation = validateResponseRequest(req.body); if (isValidationFailure(validation)) { return sendResponsesErrorResponse(res, 400, validation.error); } let envelope; try { envelope = createAgentRunEnvelope({ protocol: 'responses', requestId: req.requestId ?? req.id ?? `agent-run-${nanoid()}`, receivedAt, principal: req.user, payload: validation.request, }); } catch (error) { if (error instanceof AgentRunEnvelopeError) { return sendResponsesErrorResponse(res, 400, error.message, 'invalid_request'); } throw error; } return executeResponse(envelope, { req, res }); }; /** * List available agents as models - GET /v1/models (also works with /v1/responses/models) * * Returns a list of available agents the user has remote access to. * * @param {import('express').Request} req * @param {import('express').Response} res */ const listModels = async (req, res) => { try { const userId = req.user?.id; const userRole = req.user?.role; if (!userId) { return sendResponsesErrorResponse(res, 401, 'Authentication required', 'auth_error'); } // Find agents the user has remote access to (VIEW permission on REMOTE_AGENT) const accessibleAgentIds = await findAccessibleResources({ userId, role: userRole, resourceType: ResourceType.REMOTE_AGENT, requiredPermissions: PermissionBits.VIEW, }); // Get the accessible agents let agents = []; if (accessibleAgentIds.length > 0) { agents = await db.getAgents({ _id: { $in: accessibleAgentIds } }); } // Convert to models format const models = agents.map((agent) => ({ id: agent.id, object: 'model', created: Math.floor(new Date(agent.createdAt).getTime() / 1000), owned_by: agent.author ?? 'librechat', // Additional metadata name: agent.name, description: agent.description, provider: agent.provider, })); res.json({ object: 'list', data: models, }); } catch (error) { logger.error('[Responses API] Error listing models:', getSafeErrorMetadata(error)); sendResponsesErrorResponse( res, 500, error instanceof Error ? error.message : 'Failed to list models', 'server_error', ); } }; /** * Get Response - GET /v1/responses/:id * * Retrieves a stored response by its ID. * The response ID maps to a conversationId in LibreChat's storage. * * @param {import('express').Request} req * @param {import('express').Response} res */ const getResponse = async (req, res) => { try { const responseId = req.params.id; const userId = req.user?.id; if (!responseId) { return sendResponsesErrorResponse(res, 400, 'Response ID is required'); } // The responseId could be either the response ID or the conversation ID // Try to find a conversation with this ID const conversation = await db.getConvo(userId, responseId); if (!conversation) { return sendResponsesErrorResponse( res, 404, `Response not found: ${responseId}`, 'not_found', 'response_not_found', ); } // Load messages for this conversation const messages = await db.getMessages({ conversationId: responseId, user: userId }); if (!messages || messages.length === 0) { return sendResponsesErrorResponse( res, 404, `No messages found for response: ${responseId}`, 'not_found', 'response_not_found', ); } // Convert messages to Open Responses output format const output = convertMessagesToOutputItems(messages); // Find the last assistant message for usage info const lastAssistantMessage = messages.filter((m) => !m.isCreatedByUser).pop(); // Build the response object const response = { id: responseId, object: 'response', created_at: Math.floor(new Date(conversation.createdAt || Date.now()).getTime() / 1000), completed_at: Math.floor(new Date(conversation.updatedAt || Date.now()).getTime() / 1000), status: 'completed', incomplete_details: null, model: conversation.agentId || conversation.model || 'unknown', previous_response_id: null, instructions: null, output, error: null, tools: [], tool_choice: 'auto', truncation: 'disabled', parallel_tool_calls: true, text: { format: { type: 'text' } }, temperature: 1, top_p: 1, presence_penalty: 0, frequency_penalty: 0, top_logprobs: null, reasoning: null, user: userId, usage: lastAssistantMessage?.tokenCount ? { input_tokens: 0, output_tokens: lastAssistantMessage.tokenCount, total_tokens: lastAssistantMessage.tokenCount, } : null, max_output_tokens: null, max_tool_calls: null, store: true, background: false, service_tier: 'default', metadata: {}, safety_identifier: null, prompt_cache_key: null, }; res.json(response); } catch (error) { logger.error('[Responses API] Error getting response:', getSafeErrorMetadata(error)); sendResponsesErrorResponse( res, 500, error instanceof Error ? error.message : 'Failed to get response', 'server_error', ); } }; module.exports = { createResponse, getResponse, listModels, };