Skip to content

Commit fa30657

Browse files
authored
Merge branch 'main' into feat/mistral-on-vertex
2 parents 77a6a5c + 15b0733 commit fa30657

File tree

19 files changed

+468
-16
lines changed

19 files changed

+468
-16
lines changed

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,

src/globals.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export const FEATHERLESS_AI: string = 'featherless-ai';
102102
export const KRUTRIM: string = 'krutrim';
103103
export const QDRANT: string = 'qdrant';
104104
export const THREE_ZERO_TWO_AI: string = '302ai';
105+
export const MESHY: string = 'meshy';
106+
export const TRIPO3D: string = 'tripo3d';
105107

106108
export const VALID_PROVIDERS = [
107109
ANTHROPIC,
@@ -167,6 +169,8 @@ export const VALID_PROVIDERS = [
167169
KRUTRIM,
168170
QDRANT,
169171
THREE_ZERO_TWO_AI,
172+
MESHY,
173+
TRIPO3D,
170174
];
171175

172176
export const CONTENT_TYPES = {
@@ -215,6 +219,8 @@ export const fileExtensionMimeTypeMap = {
215219
mpegps: 'video/mpegps',
216220
flv: 'video/flv',
217221
webm: 'video/webm',
222+
mkv: 'video/mkv',
223+
threegpp: 'video/three_gpp',
218224
};
219225

220226
export const imagesMimeTypes = [
@@ -238,6 +244,19 @@ export const documentMimeTypes = [
238244
fileExtensionMimeTypeMap.txt,
239245
];
240246

247+
export const videoMimeTypes = [
248+
fileExtensionMimeTypeMap.mkv,
249+
fileExtensionMimeTypeMap.mov,
250+
fileExtensionMimeTypeMap.mp4,
251+
fileExtensionMimeTypeMap.webm,
252+
fileExtensionMimeTypeMap.flv,
253+
fileExtensionMimeTypeMap.mpeg,
254+
fileExtensionMimeTypeMap.mpg,
255+
fileExtensionMimeTypeMap.wmv,
256+
fileExtensionMimeTypeMap.threegpp,
257+
fileExtensionMimeTypeMap.avi,
258+
];
259+
241260
export enum BatchEndpoints {
242261
CHAT_COMPLETIONS = '/v1/chat/completions',
243262
COMPLETIONS = '/v1/completions',

src/handlers/handlerUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ export async function tryTargetsRecursively(
734734
conditionalRouter = new ConditionalRouter(currentTarget, {
735735
metadata,
736736
params,
737+
url: { pathname: c.req.path },
737738
});
738739
finalTarget = conditionalRouter.resolveTarget();
739740
} catch (conditionalRouter: any) {

src/handlers/streamHandler.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,13 @@ export function handleStreamingMode(
321321
await writer.write(encoder.encode(chunk));
322322
}
323323
} catch (error) {
324-
console.error(error);
324+
console.error('Error during stream processing:', error);
325325
} finally {
326-
writer.close();
326+
try {
327+
await writer.close();
328+
} catch (closeError) {
329+
console.error('Failed to close the writer:', closeError);
330+
}
327331
}
328332
})();
329333
} else {
@@ -341,9 +345,13 @@ export function handleStreamingMode(
341345
await writer.write(encoder.encode(chunk));
342346
}
343347
} catch (error) {
344-
console.error(error);
348+
console.error('Error during stream processing:', error);
345349
} finally {
346-
writer.close();
350+
try {
351+
await writer.close();
352+
} catch (closeError) {
353+
console.error('Failed to close the writer:', closeError);
354+
}
347355
}
348356
})();
349357
}

0 commit comments

Comments
 (0)