[New Plugin] Lakera Guard#1637
Conversation
Adds Lakera Guard as a guardrail provider for the Portkey AI Gateway. Supports beforeRequestHook and afterRequestHook with prompt attack blocking and PII redaction via the Lakera /v2/guard API.
narengogi
left a comment
There was a problem hiding this comment.
you can delete the helper methods you created, common utilities are available in utils file
| } | ||
| const projectID = parameters.projectID; | ||
|
|
||
| const messages = extractMessages(context, eventType); |
There was a problem hiding this comment.
you can use the getText() method from import { getText } from '../utils';
There was a problem hiding this comment.
Pull request overview
Adds a new native guardrail plugin that integrates with Lakera Guard’s /v2/guard endpoint and applies PII redaction (via Lakera payload spans) when the only detected violations are pii/*.
Changes:
- Introduces Lakera plugin handler that calls
/v2/guardfor bothbeforeRequestHookandafterRequestHook. - Adds redaction utilities for masking multiple spans across messages, plus helper unit tests.
- Registers the new plugin in the central
plugins/index.tsregistry.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/lakera/main-function.ts | New Lakera Guard plugin handler (request/response screening + optional redaction). |
| plugins/lakera/redaction.ts | Span normalization/merging + message masking helpers used by the handler. |
| plugins/lakera/manifest.json | Plugin manifest describing credentials, parameters, and hook support. |
| plugins/lakera/test-file.test.ts | Unit tests for redaction helper functions. |
| plugins/index.ts | Registers the lakera.guard handler in the plugins map. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const indices = new Set( | ||
| payload | ||
| .map((p) => p.message_id) | ||
| .filter((id) => id !== undefined && id !== null) | ||
| ); | ||
| for (const idx of indices) { | ||
| const i = Number(idx); | ||
| if (i < 0 || i >= out.length) { | ||
| warnings.push( | ||
| `payload references message_id=${i} but only ${out.length} messages` | ||
| ); | ||
| continue; | ||
| } | ||
| const msg = out[i]; | ||
| const content = msg.content; |
| if (eventType === 'beforeRequestHook') { | ||
| const reqJson = context.request?.json | ||
| ? JSON.parse(JSON.stringify(context.request.json)) | ||
| : {}; | ||
| reqJson.messages = maskedMsgs; | ||
| transformedData.request.json = reqJson; | ||
| transformed = true; | ||
| } else { | ||
| const respJson = context.response?.json | ||
| ? JSON.parse(JSON.stringify(context.response.json)) | ||
| : {}; | ||
| const choices = respJson.choices || []; | ||
| if (choices[0]?.message && maskedMsgs.length > 0) { | ||
| const last = maskedMsgs[maskedMsgs.length - 1]; | ||
| if (last && last.role === 'assistant') { | ||
| choices[0].message = choices[0].message || {}; | ||
| choices[0].message.content = last.content; | ||
| respJson.choices = choices; | ||
| } | ||
| } | ||
| transformedData.response.json = respJson; | ||
| transformed = true; | ||
| } |
| export const handler: PluginHandler = async ( | ||
| context: PluginContext, | ||
| parameters: PluginParameters, | ||
| eventType: HookEventType | ||
| ) => { | ||
| let error: any = null; | ||
| let verdict = false; | ||
| let data: any = null; | ||
| let transformed = false; | ||
| const transformedData: Record<string, any> = { | ||
| request: { json: null, text: null }, | ||
| response: { json: null, text: null }, | ||
| }; | ||
|
|
||
| try { | ||
| const apiKey = parameters.credentials?.apiKey as string | undefined; | ||
| if (!apiKey) { | ||
| throw new Error( | ||
| 'Missing Lakera apiKey: set credentials.apiKey in the guardrail config' | ||
| ); | ||
| } | ||
| const projectID = parameters.projectID; | ||
|
|
||
| const messages = extractMessages(context, eventType); | ||
| if (!messages.length) { | ||
| return { | ||
| error: null, | ||
| verdict: true, | ||
| data: { explanation: 'no messages to screen' }, | ||
| }; | ||
| } | ||
|
|
||
| const apiBase = String( | ||
| (parameters.credentials?.apiBase as string | undefined) ?? | ||
| 'https://api.lakera.ai' | ||
| ).replace(/\/$/, ''); | ||
| const url = `${apiBase}/v2/guard`; | ||
| const body: Record<string, unknown> = { | ||
| messages, | ||
| payload: true, | ||
| breakdown: true, | ||
| }; | ||
| if (projectID) { | ||
| body.project_id = projectID; | ||
| } |
| import { | ||
| applyMasksToMessages, | ||
| applyPayloadMasksToString, | ||
| dedupePayloadItems, | ||
| isOnlyPiiViolation, | ||
| mergeOverlappingIntervals, | ||
| normalizeSpan, | ||
| } from './redaction'; |
| const i = text.indexOf('👋'); | ||
| const payload = [ | ||
| { message_id: 0, start: i, end: i + 1, detector_type: 'pii/x' }, | ||
| ]; | ||
| const { text: out } = applyPayloadMasksToString(text, payload, 0, false); | ||
| expect(out).not.toContain('👋'); |
| "functions": [ | ||
| { | ||
| "name": "Guard — screen content", | ||
| "id": "guard", | ||
| "supportedHooks": ["beforeRequestHook", "afterRequestHook"], | ||
| "type": "guardrail", |
|
Taking over this work while @teddyamkie-lakera is OOO. I've addressed the review comments and opened a new PR: #1647 |
Closes #1636
Summary
/v2/guardAPIbeforeRequestHook(prompt screening) andafterRequestHook(response screening)pii/*detectors fire, passes through clean content, and blocks on any other policy hitproject_idparameter to target a specific Lakera policyapiBasecredential for regional endpointsTest plan
npx jest plugins/lakera— 9 unit tests covering redaction helpers (overlapping spans, unicode, invalid spans, message isolation)make format; make lint(ornpm run format && npm run format:check)🤖 Generated with Claude Code