Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
## Introduction

This folder contains plugins for the Portkey Gateway. Plugins allow developers to extend the capabilities of the Gateway by adding custom functionality at various stages of the request lifecycle. Currently, the primary type of plugins supported in the AI gateway are guardrails.
The default plugin set includes guardrails for common checks such as regex matching, model whitelisting, and metadata verification.

## What are Plugins?

Expand Down Expand Up @@ -173,6 +174,23 @@ To build plugins into your Gateway, follow these steps:
}
```

Metadata is supplied to the Gateway via the `x-portkey-metadata` request header as a JSON object. The parsed metadata is available to plugins through `context.metadata`.
To check request metadata using the default plugin set, add a guardrail referencing `default.metadata`:

```json
{
"guardrails": {
"beforeRequest": [
{
"plugin": "default.metadata",
"pairs": { "foo": "bar" },
"operator": "all"
}
]
}
}
```

3. Run the build command to compile the plugins:

```
Expand Down
99 changes: 99 additions & 0 deletions plugins/default/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { handler as allLowerCaseHandler } from './alllowercase';
import { handler as modelWhitelistHandler } from './modelWhitelist';
import { handler as characterCountHandler } from './characterCount';
import { handler as jwtHandler } from './jwt';
import { handler as metadataHandler } from './metadata';
import { PluginContext, PluginParameters } from '../types';

describe('Regex Matcher Plugin', () => {
Expand Down Expand Up @@ -2467,3 +2468,101 @@ describe('jwt handler', () => {
});
});
});

describe('metadata handler', () => {
const mockEventType = 'beforeRequestHook';

it('should return true when any pair matches', async () => {
const context: PluginContext = {
metadata: { foo: 'bar', a: 'b' },
};
const parameters: PluginParameters = {
pairs: { foo: 'bar', x: 'y' },
operator: 'any',
not: false,
};

const result = await metadataHandler(context, parameters, mockEventType);

expect(result.error).toBe(null);
expect(result.verdict).toBe(true);
expect(result.data).toEqual({
verdict: true,
not: false,
operator: 'any',
foundKeys: ['foo'],
missingKeys: ['x'],
explanation: 'Metadata matches the specified pairs.',
});
});

it('should return false when not all pairs match', async () => {
const context: PluginContext = {
metadata: { foo: 'bar' },
};
const parameters: PluginParameters = {
pairs: { foo: 'bar', x: 'y' },
operator: 'all',
not: false,
};

const result = await metadataHandler(context, parameters, mockEventType);

expect(result.error).toBe(null);
expect(result.verdict).toBe(false);
expect(result.data).toEqual({
verdict: false,
not: false,
operator: 'all',
foundKeys: ['foo'],
missingKeys: ['x'],
explanation: 'Metadata does not match the specified pairs.',
});
});

it('should return true when none of the pairs match', async () => {
const context: PluginContext = {
metadata: { foo: 'bar' },
};
const parameters: PluginParameters = {
pairs: { x: 'y' },
operator: 'none',
not: false,
};

const result = await metadataHandler(context, parameters, mockEventType);

expect(result.error).toBe(null);
expect(result.verdict).toBe(true);
expect(result.data).toEqual({
verdict: true,
not: false,
operator: 'none',
foundKeys: [],
missingKeys: ['x'],
explanation: 'Metadata matches the specified pairs.',
});
});
it('should return false when any pair matches but not is true', async () => {
const context: PluginContext = {
metadata: { foo: 'bar', a: 'b' },
};
const parameters: PluginParameters = {
pairs: { foo: 'bar', x: 'y' },
operator: 'any',
not: true,
};

const result = await metadataHandler(context, parameters, mockEventType);

expect(result.error).toBe(null);
expect(result.verdict).toBe(false);
expect(result.data).toEqual({
verdict: false,
not: true,
operator: 'any',
foundKeys: ['foo'],
missingKeys: ['x'],
explanation: 'Metadata matches the specified pairs when it should not.',
});
});
45 changes: 41 additions & 4 deletions plugins/default/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,14 +595,14 @@
}
},
{
"name": "Model whitelisting",
"name": "Allowed Models",
"id": "modelwhitelist",
"type": "guardrail",
"supportedHooks": ["beforeRequestHook"],
"description": [
{
"type": "subHeading",
"text": "Check if the model in the request is part of the allowed model list."
"text": "Blocks any request whose model isn’t on this list."
}
],
"parameters": {
Expand All @@ -614,7 +614,7 @@
"description": [
{
"type": "subHeading",
"text": "Enter the allowed models."
"text": "gpt-4o, llama-3-70b, mixtral-8x7b"
Comment thread
vrushankportkey marked this conversation as resolved.
}
],
"items": {
Expand All @@ -627,7 +627,7 @@
"description": [
{
"type": "subHeading",
"text": "If true, the verdict will be true when model is not in the list"
"text": "When on, any model in the list is blocked instead of allowed."
}
],
"default": false
Expand Down Expand Up @@ -706,6 +706,43 @@
},
"required": ["jwksUri", "headerKey"]
}
},
{
"name": "Metadata Check",
"id": "metadata",
"type": "guardrail",
"supportedHooks": ["beforeRequestHook"],
"description": [
{
"type": "subHeading",
"text": "Verify metadata key/value pairs"
}
],
"parameters": {
"type": "object",
"properties": {
"pairs": {
"type": "json",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"type": "json" is not a valid json schema key afaik, please use "type": "object"
cc: @VisargD

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there are other "type":"json" in the code right?

"label": "Key/Value pairs",
"description": [
{
"type": "subHeading",
"text": "e.g. {\"foo\":\"bar\"}"
}
]
},
"operator": {
"type": "string",
"enum": ["any", "all", "none"],
"default": "all"
},
"not": {
"type": "boolean",
"default": false
}
},
"required": ["pairs"]
}
}
]
}
90 changes: 90 additions & 0 deletions plugins/default/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';

export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
_eventType: HookEventType
) => {
let error = null;
let verdict = false;
let data: any = null;

try {
const pairs = parameters.pairs;
const operator = parameters.operator || 'all';
const not = parameters.not || false;

if (!pairs) {
throw new Error('Missing metadata pairs parameter');
}

if (typeof pairs !== 'object' || Array.isArray(pairs)) {
throw new Error('Metadata pairs must be an object with key-value pairs');
}

if (!context.metadata) {
data = {
verdict: false,
explanation: 'No metadata provided in the request context',
operator,
not,
foundKeys: [],
missingKeys: Object.keys(pairs)
};
return { error: null, verdict: not, data };
}

const metadata = context.metadata;
const foundKeys: string[] = [];
const missingKeys: string[] = [];

for (const [key, value] of Object.entries(pairs)) {
if (metadata[key] === value) {
foundKeys.push(key);
} else {
missingKeys.push(key);
}
}

let result = false;
if (operator === 'any') {
result = foundKeys.length > 0;
} else if (operator === 'all') {
result = missingKeys.length === 0;
} else if (operator === 'none') {
result = foundKeys.length === 0;
} else {
throw new Error(`Invalid operator: ${operator}. Must be one of: any, all, none`);
}

verdict = not ? !result : result;
data = {
verdict,
not,
operator,
foundKeys,
missingKeys,
explanation: verdict
? not
? 'Metadata does not match the specified pairs as expected.'
: 'Metadata matches the specified pairs.'
: not
? 'Metadata matches the specified pairs when it should not.'
: 'Metadata does not match the specified pairs.',
};
} catch (e: any) {
error = e;
data = {
explanation: `An error occurred while verifying metadata: ${e.message}`,
operator: parameters.operator || 'all',
not: parameters.not || false,
};
}

return { error, verdict, data };
};
2 changes: 2 additions & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { handler as defaultalluppercase } from './default/alluppercase';
import { handler as defaultalllowercase } from './default/alllowercase';
import { handler as defaultendsWith } from './default/endsWith';
import { handler as defaultmodelWhitelist } from './default/modelWhitelist';
import { handler as defaultmetadata } from './default/metadata';
import { handler as portkeymoderateContent } from './portkey/moderateContent';
import { handler as portkeylanguage } from './portkey/language';
import { handler as portkeypii } from './portkey/pii';
Expand Down Expand Up @@ -67,6 +68,7 @@ export const plugins = {
endsWith: defaultendsWith,
modelWhitelist: defaultmodelWhitelist,
jwt: defaultjwt,
metadata: defaultmetadata,
},
portkey: {
moderateContent: portkeymoderateContent,
Expand Down
Loading