diff --git a/src/handlers/services/hooksService.ts b/src/handlers/services/hooksService.ts index f8ff3f9f8..d49d22379 100644 --- a/src/handlers/services/hooksService.ts +++ b/src/handlers/services/hooksService.ts @@ -22,6 +22,7 @@ export class HooksService { afterRequestHooks, endpoint, requestHeaders, + providerOption, } = this.requestContext; const hookSpan = this.hooksManager.createSpan( params, @@ -32,7 +33,8 @@ export class HooksService { afterRequestHooks, null, endpoint, - requestHeaders + requestHeaders, + providerOption.includeHookTransformedData ?? false ); return hookSpan; } diff --git a/src/middlewares/hooks/index.ts b/src/middlewares/hooks/index.ts index edbcfadd3..8b14bf874 100644 --- a/src/middlewares/hooks/index.ts +++ b/src/middlewares/hooks/index.ts @@ -24,6 +24,7 @@ export class HookSpan { afterRequestHooksResult: HookResult[]; }; private parentHookSpanId: string | null; + public readonly includeHookTransformedData: boolean; public readonly id: string; constructor( @@ -35,7 +36,8 @@ export class HookSpan { afterRequestHooks: HookObject[], parentHookSpanId: string | null, requestType: string, - requestHeaders: Record + requestHeaders: Record, + includeHookTransformedData: boolean = false ) { this.context = this.createContext( requestParams, @@ -54,6 +56,7 @@ export class HookSpan { 'afterRequestHook' ); this.parentHookSpanId = parentHookSpanId; + this.includeHookTransformedData = includeHookTransformedData; this.hooksResult = { beforeRequestHooksResult: [], afterRequestHooksResult: [], @@ -216,7 +219,8 @@ export class HooksManager { afterRequestHooks: HookObject[], parentHookSpanId: string | null, requestType: string, - requestHeaders: Record + requestHeaders: Record, + includeHookTransformedData: boolean = false ): HookSpan { const span = new HookSpan( requestParams, @@ -227,7 +231,8 @@ export class HooksManager { afterRequestHooks, parentHookSpanId, requestType, - requestHeaders + requestHeaders, + includeHookTransformedData ); this.spans[span.id] = span; @@ -357,7 +362,9 @@ export class HooksManager { result.transformedData.request.json ); } - delete result.transformedData; + if (!span.includeHookTransformedData) { + delete result.transformedData; + } checkResults.push(result); } } @@ -384,7 +391,9 @@ export class HooksManager { result.transformedData.request.json ); } - delete result.transformedData; + if (!span.includeHookTransformedData) { + delete result.transformedData; + } checkResults.push(result); } } else { @@ -412,7 +421,9 @@ export class HooksManager { checkResult.transformedData.request.json ); } - delete checkResult.transformedData; + if (!span.includeHookTransformedData) { + delete checkResult.transformedData; + } }); } } diff --git a/src/middlewares/requestValidator/schema/config.ts b/src/middlewares/requestValidator/schema/config.ts index 2854ae578..55498e1c8 100644 --- a/src/middlewares/requestValidator/schema/config.ts +++ b/src/middlewares/requestValidator/schema/config.ts @@ -100,6 +100,7 @@ export const configSchema: any = z before_request_hooks: z .array(z.object({}).catchall(z.any())) // Allows any object structure .optional(), + include_hook_transformed_data: z.boolean().optional(), input_guardrails: z .union([ z.array(z.string()), diff --git a/src/types/requestBody.ts b/src/types/requestBody.ts index cbc8395de..0cbc8480a 100644 --- a/src/types/requestBody.ts +++ b/src/types/requestBody.ts @@ -133,6 +133,8 @@ export interface Options { beforeRequestHooks?: HookObject[]; defaultInputGuardrails?: HookObject[]; defaultOutputGuardrails?: HookObject[]; + /** Include hook transformedData in hook_results for debugging/testing */ + includeHookTransformedData?: boolean; /** OpenAI specific */ openaiProject?: string; openaiOrganization?: string; diff --git a/tests/unit/src/handlers/services/hooksService.test.ts b/tests/unit/src/handlers/services/hooksService.test.ts index 46ace8f4e..fb268e918 100644 --- a/tests/unit/src/handlers/services/hooksService.test.ts +++ b/tests/unit/src/handlers/services/hooksService.test.ts @@ -9,7 +9,7 @@ import { } from '../../../../../src/middlewares/hooks/types'; // Mock the HooksManager and HookSpan -jest.mock('../../../middlewares/hooks'); +jest.mock('../../../../../src/middlewares/hooks'); describe('HooksService', () => { let mockRequestContext: RequestContext; @@ -37,6 +37,7 @@ describe('HooksService', () => { afterRequestHooks: [], endpoint: 'chatComplete', requestHeaders: {}, + providerOption: {}, hooksManager: mockHooksManager, } as unknown as RequestContext; @@ -54,7 +55,8 @@ describe('HooksService', () => { mockRequestContext.afterRequestHooks, null, mockRequestContext.endpoint, - mockRequestContext.requestHeaders + mockRequestContext.requestHeaders, + false ); }); }); diff --git a/tests/unit/src/middlewares/hooks/index.test.ts b/tests/unit/src/middlewares/hooks/index.test.ts new file mode 100644 index 000000000..13eb9cf6c --- /dev/null +++ b/tests/unit/src/middlewares/hooks/index.test.ts @@ -0,0 +1,92 @@ +import { HOOKS_EVENT_TYPE_PRESETS } from '../../../../../src/middlewares/hooks/globals'; +import { HooksManager } from '../../../../../src/middlewares/hooks'; +import { + HookObject, + HookType, +} from '../../../../../src/middlewares/hooks/types'; + +jest.mock('../../../../../plugins', () => ({ + plugins: { + test: { + transform: jest.fn(async () => ({ + verdict: true, + data: null, + transformed: true, + transformedData: { + request: { + json: { + messages: [{ role: 'user', content: 'transformed message' }], + }, + }, + response: { json: null }, + }, + })), + }, + }, +})); + +describe('HooksManager transformedData results', () => { + const createManagerWithTransformHook = ( + includeHookTransformedData = false + ) => { + const hooksManager = new HooksManager(); + const hook: HookObject = { + id: 'test-hook', + type: HookType.MUTATOR, + checks: [{ id: 'test.transform', parameters: {} }], + } as HookObject; + + const span = hooksManager.createSpan( + { messages: [{ role: 'user', content: 'original message' }] }, + {}, + 'openai', + false, + [hook], + [], + null, + 'chatComplete', + {}, + includeHookTransformedData + ); + + return { hooksManager, span }; + }; + + it('omits transformedData from hook results by default', async () => { + const { hooksManager, span } = createManagerWithTransformHook(); + + await hooksManager.executeHooks( + span.id, + [HOOKS_EVENT_TYPE_PRESETS.SYNC_BEFORE_REQUEST_HOOK], + { env: {} } + ); + + const checkResult = + span.getHooksResult().beforeRequestHooksResult[0].checks[0]; + + expect(checkResult.transformed).toBe(true); + expect(checkResult.transformedData).toBeUndefined(); + }); + + it('includes transformedData in hook results when enabled', async () => { + const { hooksManager, span } = createManagerWithTransformHook(true); + + await hooksManager.executeHooks( + span.id, + [HOOKS_EVENT_TYPE_PRESETS.SYNC_BEFORE_REQUEST_HOOK], + { env: {} } + ); + + const checkResult = + span.getHooksResult().beforeRequestHooksResult[0].checks[0]; + + expect(checkResult.transformedData).toEqual({ + request: { + json: { + messages: [{ role: 'user', content: 'transformed message' }], + }, + }, + response: { json: null }, + }); + }); +});