Skip to content

Commit c149787

Browse files
authored
Merge branch 'main' into feat/traces-for-plugins
2 parents 09db224 + 9075db7 commit c149787

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2801
-226
lines changed

.github/pull_request_template.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
**Title:**
2-
- Brief Description of Changes
1+
## Description
2+
<!-- Provide a brief description of the changes in this PR -->
33

4-
**Description:** (optional)
5-
- Detailed change 1
6-
- Detailed change 2
4+
## Motivation
5+
<!-- Provide a brief motivation of why the changes in this PR are needed -->
76

8-
**Motivation:** (optional)
9-
- Why this change is necessary or beneficial
7+
## Type of Change
8+
<!-- Put an 'x' in the boxes that apply -->
9+
- [ ] Bug fix (non-breaking change which fixes an issue)
10+
- [ ] New feature (non-breaking change which adds functionality)
11+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12+
- [ ] Documentation update
13+
- [ ] Refactoring (no functional changes)
1014

11-
**Related Issues:** (optional)
12-
- #issue-number
15+
## How Has This Been Tested?
16+
<!-- Describe the tests you ran to verify your changes -->
17+
- [ ] Unit Tests
18+
- [ ] Integration Tests
19+
- [ ] Manual Testing
20+
21+
## Screenshots (if applicable)
22+
<!-- Add screenshots to help explain your changes -->
23+
24+
## Checklist
25+
<!-- Put an 'x' in the boxes that apply -->
26+
- [ ] My code follows the style guidelines of this project
27+
- [ ] I have performed a self-review of my own code
28+
- [ ] I have commented my code, particularly in hard-to-understand areas
29+
- [ ] I have made corresponding changes to the documentation
30+
- [ ] My changes generate no new warnings
31+
- [ ] I have added tests that prove my fix is effective or that my feature works
32+
- [ ] New and existing unit tests pass locally with my changes
33+
34+
## Related Issues
35+
<!-- Link related issues below. Insert the issue link or reference -->

conf.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"sydelabs",
77
"pillar",
88
"patronus",
9-
"pangea"
9+
"pangea",
10+
"promptsecurity"
1011
],
1112
"credentials": {
1213
"portkey": {

cookbook/guardrails/Langchain Chatbot with PII Guardrails.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"- **Custom guardrail**s: Integrate your existing guardrail systems custom guardrail integration\n",
3030
"-**LLM-based guardrails** (e.g., gibberish detection, prompt injection scanning)\n",
3131
"\n",
32-
"With **20+ deterministic guardrail**s and integrations with platforms like **Aporia, Pillar and Patronus AI,** Portkey provides comprehensive AI safety solutions. Guardrails can be configured for inputs, outputs, or both, with actions ranging from request denial to alternative LLM fallbacks.\n",
32+
"With **20+ deterministic guardrail**s and integrations with platforms like **Aporia, Pillar, Patronus AI and Prompt Security,** Portkey provides comprehensive AI safety solutions. Guardrails can be configured for inputs, outputs, or both, with actions ranging from request denial to alternative LLM fallbacks.\n",
3333
"\n",
3434
"For more details, visit the [Portkey Guardrails documentation](https://docs.portkey.ai/docs/product/guardrails/list-of-guardrail-checks/patronus-ai).\n",
3535
"\n",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@portkey-ai/gateway",
3-
"version": "1.9.15",
3+
"version": "1.9.17",
44
"description": "A fast AI gateway by Portkey",
55
"repository": {
66
"type": "git",

plugins/azure/azure.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2+
import { handler as piiHandler } from './pii';
3+
import { handler as contentSafetyHandler } from './contentSafety';
4+
import { HookEventType, PluginContext, PluginParameters } from '../types';
5+
import { AzureCredentials } from './types';
6+
import { pii, contentSafety } from './.creds.json';
7+
8+
describe('Azure Plugins', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
});
12+
13+
describe('PII Plugin', () => {
14+
const mockContext: PluginContext = {
15+
request: {
16+
text: 'My email is abc@xyz.com and SSN is 123-45-6789',
17+
json: {
18+
messages: [
19+
{
20+
role: 'user',
21+
content: 'My email is abc@xyz.com and SSN is 123-45-6789',
22+
},
23+
],
24+
},
25+
},
26+
requestType: 'chatComplete',
27+
};
28+
29+
describe('API Key Authentication', () => {
30+
const params: PluginParameters<{ pii: AzureCredentials }> = {
31+
credentials: {
32+
pii: pii.apiKey as AzureCredentials,
33+
},
34+
redact: true,
35+
apiVersion: '2024-11-01',
36+
};
37+
38+
it('should successfully analyze and redact PII with API key', async () => {
39+
const result = await piiHandler(
40+
mockContext,
41+
params,
42+
'beforeRequestHook'
43+
);
44+
45+
expect(result.error).toBeNull();
46+
expect(result.verdict).toBe(true);
47+
expect(result.transformed).toBe(true);
48+
});
49+
50+
it('should handle API errors gracefully', async () => {
51+
const result = await piiHandler(
52+
mockContext,
53+
{
54+
...params,
55+
credentials: {
56+
pii: {
57+
azureAuthMode: 'apiKey',
58+
resourceName: 'wrong-resurce-name',
59+
apiKey: 'wrong-api-key',
60+
},
61+
},
62+
},
63+
'beforeRequestHook'
64+
);
65+
66+
expect(result.error).toBeDefined();
67+
expect(result.verdict).toBe(true);
68+
expect(result.data).toBeNull();
69+
});
70+
});
71+
72+
describe('Entra ID Authentication', () => {
73+
const params: PluginParameters<{ pii: AzureCredentials }> = {
74+
credentials: {
75+
pii: pii.entra as AzureCredentials,
76+
},
77+
redact: true,
78+
};
79+
80+
it('should successfully analyze and redact PII with Entra ID', async () => {
81+
const result = await piiHandler(
82+
mockContext,
83+
params,
84+
'beforeRequestHook'
85+
);
86+
expect(result.error).toBeNull();
87+
expect(result.verdict).toBe(true);
88+
expect(result.transformed).toBe(true);
89+
});
90+
});
91+
});
92+
93+
describe('Content Safety Plugin', () => {
94+
const mockContext = {
95+
request: {
96+
text: "Fuck you, if you don't answer I'll kill you.",
97+
json: {
98+
messages: [
99+
{
100+
role: 'user',
101+
content: `Fuck you, if you don't answer I'll kill you.`,
102+
},
103+
],
104+
},
105+
},
106+
};
107+
108+
describe('API Key Authentication', () => {
109+
const params: PluginParameters<{ contentSafety: AzureCredentials }> = {
110+
credentials: {
111+
contentSafety: contentSafety.apiKey as AzureCredentials,
112+
},
113+
categories: ['Hate', 'Violence'],
114+
apiVersion: '2024-09-01',
115+
};
116+
117+
it('should successfully analyze content with API key', async () => {
118+
const result = await contentSafetyHandler(
119+
mockContext,
120+
params,
121+
'beforeRequestHook'
122+
);
123+
124+
expect(result.error).toBeNull();
125+
expect(result.verdict).toBe(false);
126+
expect(result.data).toBeDefined();
127+
});
128+
});
129+
130+
describe('Entra ID Authentication', () => {
131+
const params: PluginParameters<{ contentSafety: AzureCredentials }> = {
132+
credentials: {
133+
contentSafety: contentSafety.entra as AzureCredentials,
134+
},
135+
categories: ['Hate', 'Violence'],
136+
apiVersion: '2024-09-01',
137+
};
138+
139+
it('should successfully analyze content with Entra ID', async () => {
140+
const result = await contentSafetyHandler(
141+
mockContext,
142+
params,
143+
'beforeRequestHook'
144+
);
145+
146+
expect(result.error).toBeNull();
147+
expect(result.verdict).toBe(false);
148+
expect(result.data).toBeDefined();
149+
});
150+
151+
it('should detect harmful content correctly', async () => {
152+
const harmfulResponse = {
153+
categoriesAnalysis: [
154+
{
155+
category: 'Hate',
156+
severity: 2, // High severity
157+
},
158+
],
159+
blocklistsMatch: [],
160+
};
161+
162+
const result = await contentSafetyHandler(
163+
mockContext,
164+
params,
165+
'beforeRequestHook'
166+
);
167+
expect(result.error).toBeNull();
168+
expect(result.verdict).toBe(false); // Should be false due to high severity
169+
expect(result.data).toBeDefined();
170+
});
171+
});
172+
});
173+
});

plugins/azure/contentSafety.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
import { post, getText } from '../utils';
8+
import { AzureCredentials } from './types';
9+
import { getAccessToken } from './utils';
10+
11+
const defaultCategories = ['Hate', 'SelfHarm', 'Sexual', 'Violence'];
12+
13+
export const handler: PluginHandler<{
14+
contentSafety: AzureCredentials;
15+
}> = async (
16+
context: PluginContext,
17+
parameters: PluginParameters<{ contentSafety: AzureCredentials }>,
18+
eventType: HookEventType,
19+
options
20+
) => {
21+
let error = null;
22+
let verdict = true;
23+
let data = null;
24+
25+
const credentials = parameters.credentials?.contentSafety;
26+
27+
if (!credentials) {
28+
return {
29+
error: new Error('parameters.credentials.contentSafety must be set'),
30+
verdict: true,
31+
data,
32+
};
33+
}
34+
35+
// Validate required credentials
36+
if (!credentials?.resourceName) {
37+
return {
38+
error: new Error('Content Safety credentials must include resourceName'),
39+
verdict: true,
40+
data,
41+
};
42+
}
43+
44+
// prefer api key over auth mode
45+
if (!credentials?.azureAuthMode && !credentials?.apiKey) {
46+
return {
47+
error: new Error(
48+
'Content Safety credentials must include either apiKey or azureAuthMode'
49+
),
50+
verdict: true,
51+
data,
52+
};
53+
}
54+
55+
const text = getText(context, eventType);
56+
if (!text) {
57+
return {
58+
error: new Error('request or response text is empty'),
59+
verdict: true,
60+
data,
61+
};
62+
}
63+
64+
const apiVersion = parameters.apiVersion || '2024-11-01';
65+
66+
const url = `https://${credentials.resourceName}.cognitiveservices.azure.com/contentsafety/text:analyze?api-version=${apiVersion}`;
67+
68+
const { token, error: tokenError } = await getAccessToken(
69+
credentials as any,
70+
'contentSafety',
71+
options,
72+
options?.env
73+
);
74+
75+
if (tokenError) {
76+
return {
77+
error: tokenError,
78+
verdict: true,
79+
data,
80+
};
81+
}
82+
83+
const headers: Record<string, string> = {
84+
'Content-Type': 'application/json',
85+
'User-Agent': 'portkey-ai-plugin/',
86+
'Ocp-Apim-Subscription-Key': token,
87+
};
88+
89+
if (credentials?.azureAuthMode && credentials?.azureAuthMode !== 'apiKey') {
90+
headers['Authorization'] = `Bearer ${token}`;
91+
delete headers['Ocp-Apim-Subscription-Key'];
92+
}
93+
94+
const request = {
95+
text: text,
96+
categories: parameters.categories || defaultCategories,
97+
blocklistNames: parameters.blocklistNames || [],
98+
};
99+
100+
const timeout = parameters.timeout || 5000;
101+
let response;
102+
try {
103+
response = await post(url, request, { headers }, timeout);
104+
} catch (e) {
105+
return { error: e, verdict: true, data };
106+
}
107+
108+
if (response) {
109+
data = response;
110+
111+
// Check if any category exceeds the threshold (default: 2 - Medium)
112+
const hasHarmfulContent = response.categoriesAnalysis?.some(
113+
(category: any) => {
114+
return category.severity >= (parameters.severity || 2);
115+
}
116+
);
117+
118+
// Check if any blocklist items were hit
119+
const hasBlocklistHit = response.blocklistsMatch?.length > 0;
120+
121+
verdict = !(hasHarmfulContent || hasBlocklistHit);
122+
}
123+
124+
return {
125+
error,
126+
verdict,
127+
data,
128+
};
129+
};

0 commit comments

Comments
 (0)