mirror of
https://gh-proxy.org/https://github.com/klinsmaya/gateway.git
synced 2026-11-08 00:57:59 +08:00
feat: forward request metadata to CrowdStrike AIDR
Forward additional context to CrowdStrike AIDR's guard_chat_completions endpoint: - `user_id` from `metadata._user` or `metadata.user_id` - `llm_provider` from the request provider - `model` from the request body - `extra_info.user_name` from `metadata._user_name` or `metadata.user_name`
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import { handler } from './guardChatCompletion';
|
||||
import testCredsFile from './.creds.json';
|
||||
import { HookEventType, PluginContext } from '../types';
|
||||
import * as utils from '../utils';
|
||||
|
||||
const options = {
|
||||
env: {},
|
||||
};
|
||||
|
||||
const testCreds = {
|
||||
apiKey: testCredsFile.apiKey,
|
||||
baseUrl: testCredsFile.baseUrl,
|
||||
blockApiKey: testCredsFile.blockApiKey,
|
||||
redactApiKey: testCredsFile.redactApiKey,
|
||||
};
|
||||
|
||||
describe('AIDR Handlers', () => {
|
||||
@@ -75,10 +74,7 @@ describe('AIDR Handlers', () => {
|
||||
};
|
||||
const eventType = 'beforeRequestHook';
|
||||
const parameters = {
|
||||
credentials: {
|
||||
baseUrl: testCreds.baseUrl,
|
||||
apiKey: testCreds.blockApiKey,
|
||||
},
|
||||
credentials: testCreds,
|
||||
};
|
||||
const result = await handler(context, parameters, eventType, options);
|
||||
expect(result.error).toBeNull();
|
||||
@@ -102,10 +98,7 @@ describe('AIDR Handlers', () => {
|
||||
};
|
||||
const eventType = 'beforeRequestHook';
|
||||
const parameters = {
|
||||
credentials: {
|
||||
baseUrl: testCreds.baseUrl,
|
||||
apiKey: testCreds.redactApiKey,
|
||||
},
|
||||
credentials: testCreds,
|
||||
};
|
||||
const result = await handler(context, parameters, eventType, options);
|
||||
expect(result.error).toBeNull();
|
||||
@@ -118,3 +111,127 @@ describe('AIDR Handlers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AIDR request body metadata', () => {
|
||||
const parameters = {
|
||||
credentials: {
|
||||
baseUrl: 'https://aidr.example.test',
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
};
|
||||
|
||||
let postSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
postSpy = jest.spyOn(utils, 'post').mockResolvedValue({
|
||||
status: 'Success',
|
||||
result: { blocked: false, transformed: false, policy: 'test-policy' },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
postSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('forwards user_id, user_name, llm_provider, and model when available', async () => {
|
||||
const context = {
|
||||
provider: 'openai',
|
||||
metadata: {
|
||||
_user: 'alice',
|
||||
_user_name: 'Alice Example',
|
||||
},
|
||||
request: {
|
||||
json: {
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await handler(context, parameters, 'beforeRequestHook');
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
const body = postSpy.mock.calls[0][1] as Record<string, any>;
|
||||
expect(body).toMatchObject({
|
||||
event_type: 'input',
|
||||
app_id: 'Portkey AI Gateway',
|
||||
user_id: 'alice',
|
||||
llm_provider: 'openai',
|
||||
model: 'gpt-4o-mini',
|
||||
extra_info: { user_name: 'Alice Example' },
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers _user/_user_name over user_id/user_name fallbacks', async () => {
|
||||
const context = {
|
||||
provider: 'anthropic',
|
||||
metadata: {
|
||||
_user: 'preferred-id',
|
||||
user_id: 'fallback-id',
|
||||
_user_name: 'Preferred Name',
|
||||
user_name: 'Fallback Name',
|
||||
},
|
||||
request: { json: { model: 'claude-3-5-sonnet', messages: [] } },
|
||||
};
|
||||
|
||||
await handler(context, parameters, 'beforeRequestHook');
|
||||
|
||||
const body = postSpy.mock.calls[0][1] as Record<string, any>;
|
||||
expect(body.user_id).toBe('preferred-id');
|
||||
expect(body.extra_info.user_name).toBe('Preferred Name');
|
||||
});
|
||||
|
||||
it('falls back to user_id/user_name when underscore keys are missing', async () => {
|
||||
const context = {
|
||||
provider: 'openai',
|
||||
metadata: {
|
||||
user_id: 'bob',
|
||||
user_name: 'Bob Example',
|
||||
},
|
||||
request: { json: { model: 'gpt-4o', messages: [] } },
|
||||
};
|
||||
|
||||
await handler(context, parameters, 'beforeRequestHook');
|
||||
|
||||
const body = postSpy.mock.calls[0][1] as Record<string, any>;
|
||||
expect(body.user_id).toBe('bob');
|
||||
expect(body.extra_info.user_name).toBe('Bob Example');
|
||||
});
|
||||
|
||||
it('omits metadata fields when no values are available', async () => {
|
||||
const context = {
|
||||
request: { json: { messages: [] } },
|
||||
};
|
||||
|
||||
await handler(context, parameters, 'beforeRequestHook');
|
||||
|
||||
const body = postSpy.mock.calls[0][1] as Record<string, any>;
|
||||
expect(body).toEqual({
|
||||
guard_input: { messages: [] },
|
||||
event_type: 'input',
|
||||
app_id: 'Portkey AI Gateway',
|
||||
extra_info: {},
|
||||
});
|
||||
expect(body).not.toHaveProperty('user_id');
|
||||
expect(body).not.toHaveProperty('llm_provider');
|
||||
expect(body).not.toHaveProperty('model');
|
||||
});
|
||||
|
||||
it('uses output event_type for afterRequestHook and still includes metadata', async () => {
|
||||
const context = {
|
||||
provider: 'bedrock',
|
||||
metadata: { _user: 'charlie' },
|
||||
request: { json: { model: 'claude-3-haiku', messages: [] } },
|
||||
response: { json: { choices: [{ message: { content: 'hi' } }] } },
|
||||
};
|
||||
|
||||
await handler(context, parameters, 'afterRequestHook');
|
||||
|
||||
const body = postSpy.mock.calls[0][1] as Record<string, any>;
|
||||
expect(body.event_type).toBe('output');
|
||||
expect(body.user_id).toBe('charlie');
|
||||
expect(body.llm_provider).toBe('bedrock');
|
||||
expect(body.model).toBe('claude-3-haiku');
|
||||
expect(body.guard_input).toEqual(context.response.json);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,13 +37,25 @@ export const handler: PluginHandler = async (
|
||||
const json = context[target].json;
|
||||
const aidrEventType = target === 'request' ? 'input' : 'output';
|
||||
|
||||
const requestBody: object = {
|
||||
const metadata = context.metadata ?? {};
|
||||
const userId = metadata._user ?? metadata.user_id;
|
||||
const userName = metadata._user_name ?? metadata.user_name;
|
||||
const model = context.request?.json?.model;
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
guard_input: json,
|
||||
event_type: aidrEventType,
|
||||
app_id: 'Portkey AI Gateway',
|
||||
// TODO: Add as much other metadata as we have
|
||||
extra_info: {},
|
||||
};
|
||||
|
||||
if (userId) requestBody.user_id = userId;
|
||||
if (context.provider) requestBody.llm_provider = context.provider;
|
||||
if (model) requestBody.model = model;
|
||||
if (userName) {
|
||||
requestBody.extra_info = { ...requestBody.extra_info, user_name: userName };
|
||||
}
|
||||
|
||||
const requestOptions: object = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -66,6 +66,7 @@ import { handler as javelinguardrails } from './javelin/guardrails';
|
||||
import { handler as f5GuardrailsScan } from './f5-guardrails/scan';
|
||||
import { handler as azureShieldPrompt } from './azure/shieldPrompt';
|
||||
import { handler as azureProtectedMaterial } from './azure/protectedMaterial';
|
||||
import { handler as crowdstrikeAidrGuardChatCompletions } from './crowdstrike-aidr/guardChatCompletion';
|
||||
|
||||
export const plugins = {
|
||||
default: {
|
||||
@@ -176,4 +177,7 @@ export const plugins = {
|
||||
'f5-guardrails': {
|
||||
scan: f5GuardrailsScan,
|
||||
},
|
||||
'crowdstrike-aidr': {
|
||||
guardChatCompletions: crowdstrikeAidrGuardChatCompletions,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user