mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🪢 refactor: Move Agent Execution Seam Before Initialization (#14581)
* refactor: move agent execution seam before initialization * refactor: type librechat agent request extensions * refactor: read envelope values from descriptors * fix: preserve envelope types and validation errors * fix: bound agent envelope traversal
This commit is contained in:
parent
c2d8252b4f
commit
59395a6bf0
8 changed files with 683 additions and 59 deletions
|
|
@ -11,6 +11,26 @@ const mockRecordCollectedUsage = jest
|
|||
.mockResolvedValue({ input_tokens: 100, output_tokens: 50 });
|
||||
const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true });
|
||||
const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true });
|
||||
class MockAgentRunEnvelopeError extends TypeError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'AgentRunEnvelopeError';
|
||||
}
|
||||
}
|
||||
const mockCreateAgentRunEnvelope = jest.fn(
|
||||
({ protocol, requestId, receivedAt, principal, payload }) => ({
|
||||
version: 1,
|
||||
protocol,
|
||||
requestId,
|
||||
receivedAt,
|
||||
principal: {
|
||||
userId: principal.id,
|
||||
...(principal.role != null && { role: principal.role }),
|
||||
...(principal.tenantId != null && { tenantId: principal.tenantId }),
|
||||
},
|
||||
payload: JSON.parse(JSON.stringify(payload)),
|
||||
}),
|
||||
);
|
||||
const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => {
|
||||
const primed = {};
|
||||
for (const skill of alwaysApplySkillPrimes ?? []) {
|
||||
|
|
@ -88,6 +108,8 @@ jest.mock('@librechat/api', () => ({
|
|||
}),
|
||||
createChunk: jest.fn().mockReturnValue({}),
|
||||
buildToolSet: jest.fn().mockReturnValue(new Set()),
|
||||
AgentRunEnvelopeError: MockAgentRunEnvelopeError,
|
||||
createAgentRunEnvelope: (...args) => mockCreateAgentRunEnvelope(...args),
|
||||
scopeSkillIds: jest.fn().mockImplementation((ids) => ids),
|
||||
resolveAgentScopedSkillIds: jest
|
||||
.fn()
|
||||
|
|
@ -331,6 +353,60 @@ describe('OpenAIChatCompletionController', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('execution envelope', () => {
|
||||
it('creates the portable run input before agent initialization', async () => {
|
||||
req.user = {
|
||||
id: 'user-123',
|
||||
role: 'USER',
|
||||
tenantId: 'tenant-123',
|
||||
federatedTokens: { access_token: 'secret' },
|
||||
};
|
||||
const requestBody = {
|
||||
...req.body,
|
||||
ephemeralAgent: { skills: true },
|
||||
manualSkills: ['review-code'],
|
||||
timezone: 'America/New_York',
|
||||
};
|
||||
req.body = requestBody;
|
||||
const { validateRequest, initializeAgent } = require('@librechat/api');
|
||||
validateRequest.mockReturnValueOnce({ request: requestBody });
|
||||
|
||||
await OpenAIChatCompletionController(req, res);
|
||||
|
||||
expect(mockCreateAgentRunEnvelope).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
protocol: 'chat.completions',
|
||||
principal: req.user,
|
||||
payload: requestBody,
|
||||
requestId: expect.any(String),
|
||||
receivedAt: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
expect(mockCreateAgentRunEnvelope.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
initializeAgent.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(req.body).not.toBe(requestBody);
|
||||
expect(req.body).toEqual(requestBody);
|
||||
expect(JSON.stringify(mockCreateAgentRunEnvelope.mock.results[0].value)).not.toContain(
|
||||
'secret',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a protocol 400 when the envelope rejects a non-JSON payload', async () => {
|
||||
const message = 'payload.max_tokens must contain only finite numbers';
|
||||
const { createErrorResponse, initializeAgent } = require('@librechat/api');
|
||||
mockCreateAgentRunEnvelope.mockImplementationOnce(() => {
|
||||
throw new MockAgentRunEnvelopeError(message);
|
||||
});
|
||||
|
||||
await OpenAIChatCompletionController(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(createErrorResponse).toHaveBeenCalledWith(message, 'invalid_request_error', null);
|
||||
expect(initializeAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('token usage recording', () => {
|
||||
it('should call recordCollectedUsage after successful non-streaming completion', async () => {
|
||||
await OpenAIChatCompletionController(req, res);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,26 @@ const mockRecordCollectedUsage = jest
|
|||
.mockResolvedValue({ input_tokens: 100, output_tokens: 50 });
|
||||
const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true });
|
||||
const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true });
|
||||
class MockAgentRunEnvelopeError extends TypeError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'AgentRunEnvelopeError';
|
||||
}
|
||||
}
|
||||
const mockCreateAgentRunEnvelope = jest.fn(
|
||||
({ protocol, requestId, receivedAt, principal, payload }) => ({
|
||||
version: 1,
|
||||
protocol,
|
||||
requestId,
|
||||
receivedAt,
|
||||
principal: {
|
||||
userId: principal.id,
|
||||
...(principal.role != null && { role: principal.role }),
|
||||
...(principal.tenantId != null && { tenantId: principal.tenantId }),
|
||||
},
|
||||
payload: JSON.parse(JSON.stringify(payload)),
|
||||
}),
|
||||
);
|
||||
const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => {
|
||||
const primed = {};
|
||||
for (const skill of alwaysApplySkillPrimes ?? []) {
|
||||
|
|
@ -93,6 +113,8 @@ jest.mock('@librechat/api', () => ({
|
|||
}),
|
||||
applyContextToAgent: (...args) => mockApplyContextToAgent(...args),
|
||||
buildToolSet: jest.fn().mockReturnValue(new Set()),
|
||||
AgentRunEnvelopeError: MockAgentRunEnvelopeError,
|
||||
createAgentRunEnvelope: (...args) => mockCreateAgentRunEnvelope(...args),
|
||||
buildAgentScopedContext: (...args) => mockBuildAgentScopedContext(...args),
|
||||
buildAgentContextAttachmentsByAgentId: (...args) =>
|
||||
mockBuildAgentContextAttachmentsByAgentId(...args),
|
||||
|
|
@ -319,6 +341,60 @@ describe('createResponse controller', () => {
|
|||
};
|
||||
});
|
||||
|
||||
describe('execution envelope', () => {
|
||||
it('creates the portable run input before agent initialization', async () => {
|
||||
req.user = {
|
||||
id: 'user-123',
|
||||
role: 'USER',
|
||||
tenantId: 'tenant-123',
|
||||
federatedTokens: { access_token: 'secret' },
|
||||
};
|
||||
const requestBody = {
|
||||
...req.body,
|
||||
ephemeralAgent: { skills: true },
|
||||
manualSkills: ['review-code'],
|
||||
timezone: 'America/New_York',
|
||||
isTemporary: true,
|
||||
};
|
||||
req.body = requestBody;
|
||||
const { validateResponseRequest, initializeAgent } = require('@librechat/api');
|
||||
validateResponseRequest.mockReturnValueOnce({ request: requestBody });
|
||||
|
||||
await createResponse(req, res);
|
||||
|
||||
expect(mockCreateAgentRunEnvelope).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
protocol: 'responses',
|
||||
principal: req.user,
|
||||
payload: requestBody,
|
||||
requestId: expect.any(String),
|
||||
receivedAt: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
expect(mockCreateAgentRunEnvelope.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
initializeAgent.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(req.body).not.toBe(requestBody);
|
||||
expect(req.body).toEqual(requestBody);
|
||||
expect(JSON.stringify(mockCreateAgentRunEnvelope.mock.results[0].value)).not.toContain(
|
||||
'secret',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a protocol 400 when the envelope rejects a non-JSON payload', async () => {
|
||||
const message = 'payload.max_output_tokens must contain only finite numbers';
|
||||
const { sendResponsesErrorResponse, initializeAgent } = require('@librechat/api');
|
||||
mockCreateAgentRunEnvelope.mockImplementationOnce(() => {
|
||||
throw new MockAgentRunEnvelopeError(message);
|
||||
});
|
||||
|
||||
await createResponse(req, res);
|
||||
|
||||
expect(sendResponsesErrorResponse).toHaveBeenCalledWith(res, 400, message, 'invalid_request');
|
||||
expect(initializeAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('conversation ownership validation', () => {
|
||||
it('should skip ownership check when previous_response_id is not provided', async () => {
|
||||
const { getConvo } = require('~/models');
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ const {
|
|||
createRun,
|
||||
createChunk,
|
||||
buildToolSet,
|
||||
AgentRunEnvelopeError,
|
||||
createAgentRunEnvelope,
|
||||
loadSkillStates,
|
||||
sendFinalChunk,
|
||||
createSafeUser,
|
||||
|
|
@ -149,29 +151,20 @@ function sendErrorResponse(res, statusCode, message, type = 'invalid_request_err
|
|||
}
|
||||
|
||||
/**
|
||||
* OpenAI-compatible chat completions controller for agents.
|
||||
* Runs a validated chat-completions envelope in the current process.
|
||||
* Express remains runtime-only state while the envelope is the portable run input.
|
||||
*
|
||||
* POST /v1/chat/completions
|
||||
*
|
||||
* Request format:
|
||||
* {
|
||||
* "model": "agent_id_here",
|
||||
* "messages": [{"role": "user", "content": "Hello!"}],
|
||||
* "stream": true,
|
||||
* "conversation_id": "optional",
|
||||
* "parent_message_id": "optional"
|
||||
* }
|
||||
* @param {import('@librechat/api').ChatCompletionRunEnvelope} envelope
|
||||
* @param {{req: import('express').Request, res: import('express').Response}} runtime
|
||||
*/
|
||||
const OpenAIChatCompletionController = async (req, res) => {
|
||||
const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
|
||||
const appConfig = req.config;
|
||||
const requestStartTime = Date.now();
|
||||
|
||||
const validation = validateRequest(req.body);
|
||||
if (isChatCompletionValidationFailure(validation)) {
|
||||
return sendErrorResponse(res, 400, validation.error);
|
||||
}
|
||||
|
||||
const request = validation.request;
|
||||
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;
|
||||
|
||||
// Look up the agent
|
||||
|
|
@ -232,7 +225,7 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
'invalid_request_error',
|
||||
);
|
||||
}
|
||||
if (!(await db.getConvo(req.user?.id, request.conversation_id))) {
|
||||
if (!(await db.getConvo(principal.userId, request.conversation_id))) {
|
||||
return sendErrorResponse(res, 404, 'Conversation not found', 'invalid_request_error');
|
||||
}
|
||||
}
|
||||
|
|
@ -277,12 +270,12 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
|
||||
const enabledCapabilities = new Set(agentsEConfig?.capabilities);
|
||||
const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills);
|
||||
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;
|
||||
const ephemeralSkillsToggle = request.ephemeralAgent?.skills === true;
|
||||
const accessibleSkillIds = skillsCapabilityEnabled
|
||||
? withDeploymentSkillIds(
|
||||
await findAccessibleResources({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
userId: principal.userId,
|
||||
role: principal.role,
|
||||
resourceType: ResourceType.SKILL,
|
||||
requiredPermissions: PermissionBits.VIEW,
|
||||
}),
|
||||
|
|
@ -290,8 +283,8 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
: [];
|
||||
const editableSkillIds = skillsCapabilityEnabled
|
||||
? await findAccessibleResources({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
userId: principal.userId,
|
||||
role: principal.role,
|
||||
resourceType: ResourceType.SKILL,
|
||||
requiredPermissions: PermissionBits.EDIT,
|
||||
})
|
||||
|
|
@ -301,13 +294,13 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
: false;
|
||||
|
||||
const { skillStates, defaultActiveOnShare } = await loadSkillStates({
|
||||
userId: req.user.id,
|
||||
userId: principal.userId,
|
||||
appConfig,
|
||||
getUserById: db.getUserById,
|
||||
accessibleSkillIds,
|
||||
});
|
||||
|
||||
const manualSkills = extractManualSkills(req.body);
|
||||
const manualSkills = extractManualSkills(request);
|
||||
|
||||
const primaryScopedSkillIds = resolveAgentScopedSkillIds({
|
||||
agent,
|
||||
|
|
@ -741,7 +734,7 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
};
|
||||
|
||||
// Create and run the agent
|
||||
const userId = req.user?.id ?? 'api-user';
|
||||
const userId = principal.userId;
|
||||
|
||||
// Extract merged userMCPAuthMap (needed for MCP tool connections across
|
||||
// the primary and any discovered handoff sub-agents)
|
||||
|
|
@ -764,7 +757,7 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
conversationId,
|
||||
},
|
||||
user: { id: userId },
|
||||
tenantId: req.user?.tenantId,
|
||||
tenantId: principal.tenantId,
|
||||
/** Bills subagent child-run model calls (reported outside the
|
||||
* streamEvents loop) into the same collectedUsage array. */
|
||||
subagentUsageSink: createSubagentUsageSink(collectedUsage),
|
||||
|
|
@ -896,6 +889,38 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI-compatible chat completions ingress adapter for agents.
|
||||
* Authentication and remote-agent authorization have already run in route middleware.
|
||||
*
|
||||
* POST /v1/chat/completions
|
||||
*/
|
||||
const OpenAIChatCompletionController = async (req, res) => {
|
||||
const receivedAt = Date.now();
|
||||
const validation = validateRequest(req.body);
|
||||
if (isChatCompletionValidationFailure(validation)) {
|
||||
return sendErrorResponse(res, 400, validation.error);
|
||||
}
|
||||
|
||||
let envelope;
|
||||
try {
|
||||
envelope = createAgentRunEnvelope({
|
||||
protocol: 'chat.completions',
|
||||
requestId: req.requestId ?? req.id ?? `agent-run-${nanoid()}`,
|
||||
receivedAt,
|
||||
principal: req.user,
|
||||
payload: validation.request,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AgentRunEnvelopeError) {
|
||||
return sendErrorResponse(res, 400, error.message, 'invalid_request_error');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return executeOpenAIChatCompletion(envelope, { req, res });
|
||||
};
|
||||
|
||||
/**
|
||||
* List available agents as models (filtered by remote access permissions)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ const {
|
|||
createRun,
|
||||
applyContextToAgent,
|
||||
buildToolSet,
|
||||
AgentRunEnvelopeError,
|
||||
createAgentRunEnvelope,
|
||||
buildAgentScopedContext,
|
||||
buildAgentContextAttachmentsByAgentId,
|
||||
createSafeUser,
|
||||
|
|
@ -284,25 +286,20 @@ function convertMessagesToOutputItems(messages) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create Response - POST /v1/responses
|
||||
* Runs a validated Responses envelope in the current process.
|
||||
* Express remains runtime-only state while the envelope is the portable run input.
|
||||
*
|
||||
* Creates a model response following the Open Responses API specification.
|
||||
* Supports both streaming and non-streaming responses.
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
* @param {import('@librechat/api').ResponsesRunEnvelope} envelope
|
||||
* @param {{req: import('express').Request, res: import('express').Response}} runtime
|
||||
*/
|
||||
const createResponse = async (req, res) => {
|
||||
const executeResponse = async (envelope, { req, res }) => {
|
||||
const appConfig = req.config;
|
||||
const requestStartTime = Date.now();
|
||||
|
||||
// Validate request
|
||||
const validation = validateResponseRequest(req.body);
|
||||
if (isValidationFailure(validation)) {
|
||||
return sendResponsesErrorResponse(res, 400, validation.error);
|
||||
}
|
||||
|
||||
const request = validation.request;
|
||||
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 isStreaming = request.stream === true;
|
||||
const summarizationConfig = appConfig?.summarization;
|
||||
|
|
@ -348,7 +345,7 @@ const createResponse = async (req, res) => {
|
|||
'invalid_request',
|
||||
);
|
||||
}
|
||||
if (!(await db.getConvo(req.user?.id, request.previous_response_id))) {
|
||||
if (!(await db.getConvo(principal.userId, request.previous_response_id))) {
|
||||
return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found');
|
||||
}
|
||||
}
|
||||
|
|
@ -397,12 +394,12 @@ const createResponse = async (req, res) => {
|
|||
appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities,
|
||||
);
|
||||
const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills);
|
||||
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;
|
||||
const ephemeralSkillsToggle = request.ephemeralAgent?.skills === true;
|
||||
const accessibleSkillIds = skillsCapabilityEnabled
|
||||
? withDeploymentSkillIds(
|
||||
await findAccessibleResources({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
userId: principal.userId,
|
||||
role: principal.role,
|
||||
resourceType: ResourceType.SKILL,
|
||||
requiredPermissions: PermissionBits.VIEW,
|
||||
}),
|
||||
|
|
@ -410,8 +407,8 @@ const createResponse = async (req, res) => {
|
|||
: [];
|
||||
const editableSkillIds = skillsCapabilityEnabled
|
||||
? await findAccessibleResources({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
userId: principal.userId,
|
||||
role: principal.role,
|
||||
resourceType: ResourceType.SKILL,
|
||||
requiredPermissions: PermissionBits.EDIT,
|
||||
})
|
||||
|
|
@ -421,13 +418,13 @@ const createResponse = async (req, res) => {
|
|||
: false;
|
||||
|
||||
const { skillStates, defaultActiveOnShare } = await loadSkillStates({
|
||||
userId: req.user.id,
|
||||
userId: principal.userId,
|
||||
appConfig,
|
||||
getUserById: db.getUserById,
|
||||
accessibleSkillIds,
|
||||
});
|
||||
|
||||
const manualSkills = extractManualSkills(req.body);
|
||||
const manualSkills = extractManualSkills(request);
|
||||
|
||||
const primaryScopedSkillIds = resolveAgentScopedSkillIds({
|
||||
agent,
|
||||
|
|
@ -611,7 +608,7 @@ const createResponse = async (req, res) => {
|
|||
// Load previous messages if previous_response_id is provided
|
||||
let previousMessages = [];
|
||||
if (request.previous_response_id) {
|
||||
const userId = req.user?.id ?? 'api-user';
|
||||
const userId = principal.userId;
|
||||
previousMessages = await loadPreviousMessages(request.previous_response_id, userId);
|
||||
}
|
||||
|
||||
|
|
@ -777,7 +774,7 @@ const createResponse = async (req, res) => {
|
|||
};
|
||||
|
||||
// Create and run the agent
|
||||
const userId = req.user?.id ?? 'api-user';
|
||||
const userId = principal.userId;
|
||||
const userMCPAuthMap = mergedMCPAuthMap;
|
||||
|
||||
const run = await createRun({
|
||||
|
|
@ -795,7 +792,7 @@ const createResponse = async (req, res) => {
|
|||
conversationId,
|
||||
},
|
||||
user: { id: userId },
|
||||
tenantId: req.user?.tenantId,
|
||||
tenantId: principal.tenantId,
|
||||
/** Bills subagent child-run model calls (reported outside the
|
||||
* streamEvents loop) into the same collectedUsage array. */
|
||||
subagentUsageSink: createSubagentUsageSink(collectedUsage),
|
||||
|
|
@ -958,7 +955,7 @@ const createResponse = async (req, res) => {
|
|||
: {}),
|
||||
};
|
||||
|
||||
const userId = req.user?.id ?? 'api-user';
|
||||
const userId = principal.userId;
|
||||
const userMCPAuthMap = mergedMCPAuthMap;
|
||||
|
||||
const run = await createRun({
|
||||
|
|
@ -976,7 +973,7 @@ const createResponse = async (req, res) => {
|
|||
conversationId,
|
||||
},
|
||||
user: { id: userId },
|
||||
tenantId: req.user?.tenantId,
|
||||
tenantId: principal.tenantId,
|
||||
/** Bills subagent child-run model calls (reported outside the
|
||||
* streamEvents loop) into the same collectedUsage array. */
|
||||
subagentUsageSink: createSubagentUsageSink(collectedUsage),
|
||||
|
|
@ -1090,6 +1087,41 @@ const createResponse = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue