Skip to content

Commit 8516a27

Browse files
authored
Merge branch 'main' into feat/unified-count-tokens-endpoint
2 parents a4590ec + 4837c8f commit 8516a27

Some content is hidden

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

44 files changed

+1009
-161
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ COPY package*.json ./
99
COPY patches ./
1010

1111
# Upgrade system packages
12-
RUN apk update && apk upgrade --no-cache
12+
RUN apk upgrade --no-cache
1313

1414
# Upgrade npm to version 10.9.2
1515
RUN npm install -g [email protected]
@@ -29,7 +29,7 @@ RUN npm run build \
2929
FROM node:20-alpine
3030

3131
# Upgrade system packages
32-
RUN apk update && apk upgrade --no-cache
32+
RUN apk upgrade --no-cache
3333

3434
# Upgrade npm to version 10.9.2
3535
RUN npm install -g [email protected]

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.11.2",
3+
"version": "1.11.3",
44
"description": "A fast AI gateway by Portkey",
55
"repository": {
66
"type": "git",

plugins/default/manifest.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,45 @@
636636
"required": ["models"]
637637
}
638638
},
639+
{
640+
"name": "Model Rules",
641+
"id": "modelRules",
642+
"type": "guardrail",
643+
"supportedHooks": ["beforeRequestHook"],
644+
"description": [
645+
{
646+
"type": "subHeading",
647+
"text": "Allow requests based on metadata-driven rules mapping to allowed models."
648+
}
649+
],
650+
"parameters": {
651+
"type": "object",
652+
"properties": {
653+
"rules": {
654+
"type": "object",
655+
"label": "Rules object: {\"defaults\": [\"model\"], \"metadata\": {\"key\": {\"value\": [\"models\"]}}}",
656+
"description": [
657+
{
658+
"type": "text",
659+
"text": "Overrides model list using metadata-based routing."
660+
}
661+
]
662+
},
663+
"not": {
664+
"type": "boolean",
665+
"label": "Invert Model Check",
666+
"description": [
667+
{
668+
"type": "text",
669+
"text": "When on, any model resolved by rules is blocked instead of allowed."
670+
}
671+
],
672+
"default": false
673+
}
674+
},
675+
"required": ["rules"]
676+
}
677+
},
639678
{
640679
"name": "JWT",
641680
"id": "jwt",

plugins/default/modelRules.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
8+
interface RulesData {
9+
explanation: string;
10+
}
11+
12+
export const handler: PluginHandler = async (
13+
context: PluginContext,
14+
parameters: PluginParameters,
15+
eventType: HookEventType
16+
) => {
17+
let error = null;
18+
let verdict = false;
19+
let data: RulesData | null = null;
20+
21+
try {
22+
const rulesConfig = parameters.rules as Record<string, unknown> | undefined;
23+
const not = parameters.not || false;
24+
const requestModel = context.request?.json.model as string | undefined;
25+
const requestMetadata: Record<string, unknown> = context?.metadata || {};
26+
27+
if (!requestModel) {
28+
throw new Error('Missing model in request');
29+
}
30+
31+
if (!rulesConfig || typeof rulesConfig !== 'object') {
32+
throw new Error('Missing rules configuration');
33+
}
34+
35+
type RulesShape = {
36+
defaults?: unknown;
37+
metadata?: unknown;
38+
};
39+
const cfg = rulesConfig as RulesShape;
40+
41+
const defaultsArray = Array.isArray(cfg.defaults)
42+
? (cfg.defaults as unknown[])
43+
: [];
44+
const defaults = defaultsArray.map((m) => String(m));
45+
46+
const metadata =
47+
cfg.metadata && typeof cfg.metadata === 'object'
48+
? (cfg.metadata as Record<string, Record<string, unknown>>)
49+
: {};
50+
51+
const matched = new Set<string>();
52+
const matchedRules: string[] = [];
53+
54+
for (const [key, mapping] of Object.entries(metadata)) {
55+
const reqVal = requestMetadata[key];
56+
if (reqVal === undefined || reqVal === null) continue;
57+
58+
const reqVals = Array.isArray(reqVal)
59+
? reqVal.map((v) => String(v))
60+
: [String(reqVal)];
61+
62+
for (const val of reqVals) {
63+
const modelsUnknown = (mapping as Record<string, unknown>)[val];
64+
if (Array.isArray(modelsUnknown)) {
65+
const models = (modelsUnknown as unknown[]).filter(
66+
(m) => typeof m === 'string'
67+
) as string[];
68+
matchedRules.push(`${key}:${val}`);
69+
for (const m of models) {
70+
if (m && typeof m === 'string') {
71+
matched.add(String(m));
72+
}
73+
}
74+
}
75+
}
76+
}
77+
78+
let allowedSet = Array.from(matched);
79+
let usingDefaults = false;
80+
if (allowedSet.length === 0) {
81+
allowedSet = defaults;
82+
usingDefaults = true;
83+
}
84+
85+
if (!Array.isArray(allowedSet) || allowedSet.length === 0) {
86+
throw new Error('No allowed models resolved from rules');
87+
}
88+
89+
const inList = allowedSet.includes(requestModel);
90+
verdict = not ? !inList : inList;
91+
92+
let explanation = '';
93+
if (verdict) {
94+
explanation = not
95+
? `Model "${requestModel}" is not permitted by rules (blocked list).`
96+
: `Model "${requestModel}" is allowed by rules.`;
97+
if (matchedRules.length) {
98+
explanation += ` (matched rules: ${matchedRules.join(', ')})`;
99+
} else if (usingDefaults) {
100+
explanation += ' (using default models)';
101+
}
102+
} else {
103+
explanation = not
104+
? `Model "${requestModel}" is permitted by rules (in blocked list).`
105+
: `Model "${requestModel}" is not allowed by rules.`;
106+
}
107+
108+
data = { explanation };
109+
} catch (e) {
110+
const err = e as Error;
111+
error = err;
112+
data = {
113+
explanation: `An error occurred while checking model rules: ${err.message}`,
114+
};
115+
}
116+
117+
return { error, verdict, data };
118+
};

plugins/default/regexReplace.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
import { getCurrentContentPart, setCurrentContentPart } from '../utils';
8+
9+
export const handler: PluginHandler = async (
10+
context: PluginContext,
11+
parameters: PluginParameters,
12+
eventType: HookEventType
13+
) => {
14+
let error = null;
15+
let verdict = true;
16+
let data: any = null;
17+
const transformedData: Record<string, any> = {
18+
request: {
19+
json: null,
20+
},
21+
response: {
22+
json: null,
23+
},
24+
};
25+
let transformed = false;
26+
27+
try {
28+
const regexPattern = parameters.rule;
29+
const redactText = parameters.redactText || '[REDACTED]';
30+
const failOnDetection = parameters.failOnDetection || false;
31+
32+
const { content, textArray } = getCurrentContentPart(context, eventType);
33+
34+
if (!regexPattern) {
35+
throw new Error('Missing regex pattern');
36+
}
37+
if (!content) {
38+
throw new Error('Missing text to match');
39+
}
40+
41+
const regex = new RegExp(regexPattern, 'g');
42+
43+
// Process all text items in the array
44+
let hasMatches = false;
45+
const mappedTextArray: Array<string | null> = [];
46+
textArray.forEach((text) => {
47+
if (!text) {
48+
mappedTextArray.push(null);
49+
return;
50+
}
51+
52+
// Reset regex for each text when using global flag
53+
regex.lastIndex = 0;
54+
55+
const matches = text.match(regex);
56+
if (matches && matches.length > 0) {
57+
hasMatches = true;
58+
}
59+
const replacedText = text.replace(regex, redactText);
60+
mappedTextArray.push(replacedText);
61+
});
62+
63+
// Handle transformation
64+
if (hasMatches) {
65+
setCurrentContentPart(
66+
context,
67+
eventType,
68+
transformedData,
69+
mappedTextArray
70+
);
71+
transformed = true;
72+
}
73+
if (failOnDetection && hasMatches) {
74+
verdict = false;
75+
}
76+
data = {
77+
regexPattern,
78+
verdict,
79+
explanation: transformed
80+
? `Pattern '${regexPattern}' matched and was replaced with '${redactText}'`
81+
: `The regex pattern '${regexPattern}' did not match any text.`,
82+
};
83+
} catch (e: any) {
84+
error = e;
85+
data = {
86+
explanation: `An error occurred while processing the regex: ${e.message}`,
87+
regexPattern: parameters.rule,
88+
};
89+
}
90+
91+
return { error, verdict, data, transformedData, transformed };
92+
};

plugins/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { handler as defaultalluppercase } from './default/alluppercase';
1313
import { handler as defaultalllowercase } from './default/alllowercase';
1414
import { handler as defaultendsWith } from './default/endsWith';
1515
import { handler as defaultmodelWhitelist } from './default/modelWhitelist';
16+
import { handler as defaultmodelRules } from './default/modelRules';
1617
import { handler as portkeymoderateContent } from './portkey/moderateContent';
1718
import { handler as portkeylanguage } from './portkey/language';
1819
import { handler as portkeypii } from './portkey/pii';
@@ -50,6 +51,7 @@ import { handler as panwPrismaAirsintercept } from './panw-prisma-airs/intercept
5051
import { handler as defaultjwt } from './default/jwt';
5152
import { handler as defaultrequiredMetadataKeys } from './default/requiredMetadataKeys';
5253
import { handler as walledaiguardrails } from './walledai/guardrails';
54+
import { handler as defaultregexReplace } from './default/regexReplace';
5355

5456
export const plugins = {
5557
default: {
@@ -68,8 +70,10 @@ export const plugins = {
6870
alllowercase: defaultalllowercase,
6971
endsWith: defaultendsWith,
7072
modelWhitelist: defaultmodelWhitelist,
73+
modelRules: defaultmodelRules,
7174
jwt: defaultjwt,
7275
requiredMetadataKeys: defaultrequiredMetadataKeys,
76+
regexReplace: defaultregexReplace,
7377
},
7478
portkey: {
7579
moderateContent: portkeymoderateContent,

plugins/portkey/gibberish.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const handler: PluginHandler = async (
2929
text = textArray.filter((text) => text).join('\n');
3030
const not = parameters.not || false;
3131

32-
const response: any = await fetchPortkey(
32+
const { response }: any = await fetchPortkey(
3333
options?.env || {},
3434
PORTKEY_ENDPOINTS.GIBBERISH,
3535
parameters.credentials,

plugins/portkey/language.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const handler: PluginHandler = async (
3030
const languages = parameters.language;
3131
const not = parameters.not || false;
3232

33-
const result: any = await fetchPortkey(
33+
const { response: result }: any = await fetchPortkey(
3434
options?.env || {},
3535
PORTKEY_ENDPOINTS.LANGUAGE,
3636
parameters.credentials,

plugins/portkey/moderateContent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const handler: PluginHandler = async (
3030
const categories = parameters.categories;
3131
const not = parameters.not || false;
3232

33-
const result: any = await fetchPortkey(
33+
const { response: result }: any = await fetchPortkey(
3434
options?.env || {},
3535
PORTKEY_ENDPOINTS.MODERATIONS,
3636
parameters.credentials,

0 commit comments

Comments
 (0)