From 2f233823c5cc35d127b9aacec72a446693b5878e Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:53:14 +0530 Subject: [PATCH 1/7] chore: update model whitelist copy --- plugins/default/manifest.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/default/manifest.json b/plugins/default/manifest.json index 4e39390dc..e039c1979 100644 --- a/plugins/default/manifest.json +++ b/plugins/default/manifest.json @@ -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": { @@ -614,7 +614,7 @@ "description": [ { "type": "subHeading", - "text": "Enter the allowed models." + "text": "gpt-4o, llama-3-70b, mixtral-8x7b" } ], "items": { @@ -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 From 7cd3a85c49537c5d62ddcad8391b2501c783c28b Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:34:27 +0530 Subject: [PATCH 2/7] Add metadata guardrail --- plugins/README.md | 17 +++++++ plugins/default/default.test.ts | 77 ++++++++++++++++++++++++++++++++ plugins/default/manifest.json | 37 ++++++++++++++++ plugins/default/metadata.ts | 78 +++++++++++++++++++++++++++++++++ plugins/index.ts | 2 + 5 files changed, 211 insertions(+) create mode 100644 plugins/default/metadata.ts diff --git a/plugins/README.md b/plugins/README.md index 274f20300..08b324ec5 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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? @@ -173,6 +174,22 @@ To build plugins into your Gateway, follow these steps: } ``` +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: ``` diff --git a/plugins/default/default.test.ts b/plugins/default/default.test.ts index 9f8f0ef8a..4e3c98b16 100644 --- a/plugins/default/default.test.ts +++ b/plugins/default/default.test.ts @@ -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', () => { @@ -2467,3 +2468,79 @@ 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.', + }); + }); +}); diff --git a/plugins/default/manifest.json b/plugins/default/manifest.json index e039c1979..7db330698 100644 --- a/plugins/default/manifest.json +++ b/plugins/default/manifest.json @@ -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", + "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"] + } } ] } diff --git a/plugins/default/metadata.ts b/plugins/default/metadata.ts new file mode 100644 index 000000000..cb31afb73 --- /dev/null +++ b/plugins/default/metadata.ts @@ -0,0 +1,78 @@ +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 || typeof pairs !== 'object') { + throw new Error('Invalid or missing metadata pairs'); + } + + 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; + switch (operator) { + case 'any': + result = foundKeys.length > 0; + break; + case 'all': + result = missingKeys.length === 0; + break; + case 'none': + result = foundKeys.length === 0; + break; + default: + throw new Error('Invalid operator'); + } + + 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 }; +}; diff --git a/plugins/index.ts b/plugins/index.ts index 85b89c7c7..ba35a58a1 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -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'; @@ -67,6 +68,7 @@ export const plugins = { endsWith: defaultendsWith, modelWhitelist: defaultmodelWhitelist, jwt: defaultjwt, + metadata: defaultmetadata, }, portkey: { moderateContent: portkeymoderateContent, From 0fd7342dd42ae2f60487dd186caff2045ffc7e51 Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:35:38 +0530 Subject: [PATCH 3/7] docs: clarify how metadata header is used --- plugins/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/README.md b/plugins/README.md index 08b324ec5..0a5d922e7 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -174,6 +174,7 @@ 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 From 498b412db94218c848555244faaa82c28d020360 Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:13:50 +0530 Subject: [PATCH 4/7] Update plugins/default/metadata.ts Co-authored-by: matter-code-review[bot] <150888575+matter-code-review[bot]@users.noreply.github.com> --- plugins/default/metadata.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/default/metadata.ts b/plugins/default/metadata.ts index cb31afb73..b0d6c0c5b 100644 --- a/plugins/default/metadata.ts +++ b/plugins/default/metadata.ts @@ -19,8 +19,12 @@ export const handler: PluginHandler = async ( const operator = parameters.operator || 'all'; const not = parameters.not || false; - if (!pairs || typeof pairs !== 'object') { - throw new Error('Invalid or missing metadata pairs'); + 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'); } const metadata = context.metadata || {}; From e5bd432571404dd81130c9771f95a9d34a9656e5 Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:14:10 +0530 Subject: [PATCH 5/7] Update plugins/default/metadata.ts Co-authored-by: matter-code-review[bot] <150888575+matter-code-review[bot]@users.noreply.github.com> --- plugins/default/metadata.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/default/metadata.ts b/plugins/default/metadata.ts index b0d6c0c5b..252db6942 100644 --- a/plugins/default/metadata.ts +++ b/plugins/default/metadata.ts @@ -27,7 +27,19 @@ export const handler: PluginHandler = async ( throw new Error('Metadata pairs must be an object with key-value pairs'); } - const metadata = context.metadata || {}; + 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[] = []; From 5910ab80cc8818ec1ab5e634dcf4cf13bad83dd6 Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:14:33 +0530 Subject: [PATCH 6/7] Update plugins/default/metadata.ts Co-authored-by: matter-code-review[bot] <150888575+matter-code-review[bot]@users.noreply.github.com> --- plugins/default/metadata.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/plugins/default/metadata.ts b/plugins/default/metadata.ts index 252db6942..19095b732 100644 --- a/plugins/default/metadata.ts +++ b/plugins/default/metadata.ts @@ -52,18 +52,14 @@ export const handler: PluginHandler = async ( } let result = false; - switch (operator) { - case 'any': - result = foundKeys.length > 0; - break; - case 'all': - result = missingKeys.length === 0; - break; - case 'none': - result = foundKeys.length === 0; - break; - default: - throw new Error('Invalid operator'); + 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; From 2abcfd4283353ccf8bb17e65012613d097823d7b Mon Sep 17 00:00:00 2001 From: Vrushank Vyas <134934501+vrushankportkey@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:14:51 +0530 Subject: [PATCH 7/7] Update plugins/default/default.test.ts Co-authored-by: matter-code-review[bot] <150888575+matter-code-review[bot]@users.noreply.github.com> --- plugins/default/default.test.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/default/default.test.ts b/plugins/default/default.test.ts index 4e3c98b16..dbb1560b5 100644 --- a/plugins/default/default.test.ts +++ b/plugins/default/default.test.ts @@ -2543,4 +2543,26 @@ describe('metadata handler', () => { 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.', + }); + });