Skip to content

Commit f8a0f44

Browse files
authored
Merge branch 'main' into main
2 parents 03be393 + 4837c8f commit f8a0f44

File tree

21 files changed

+359
-110
lines changed

21 files changed

+359
-110
lines changed

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/index.ts

Lines changed: 2 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';
@@ -69,6 +70,7 @@ export const plugins = {
6970
alllowercase: defaultalllowercase,
7071
endsWith: defaultendsWith,
7172
modelWhitelist: defaultmodelWhitelist,
73+
modelRules: defaultmodelRules,
7274
jwt: defaultjwt,
7375
requiredMetadataKeys: defaultrequiredMetadataKeys,
7476
regexReplace: defaultregexReplace,

src/globals.ts

Lines changed: 4 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 = {

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: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,17 @@ 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:', proxyProvider, error);
325325
} finally {
326-
writer.close();
326+
try {
327+
await writer.close();
328+
} catch (closeError) {
329+
console.error(
330+
'Failed to close the writer:',
331+
proxyProvider,
332+
closeError
333+
);
334+
}
327335
}
328336
})();
329337
} else {
@@ -341,9 +349,17 @@ export function handleStreamingMode(
341349
await writer.write(encoder.encode(chunk));
342350
}
343351
} catch (error) {
344-
console.error(error);
352+
console.error('Error during stream processing:', proxyProvider, error);
345353
} finally {
346-
writer.close();
354+
try {
355+
await writer.close();
356+
} catch (closeError) {
357+
console.error(
358+
'Failed to close the writer:',
359+
proxyProvider,
360+
closeError
361+
);
362+
}
347363
}
348364
})();
349365
}

src/providers/google-vertex-ai/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export const GoogleApiConfig: ProviderAPIConfig = {
169169
return googleUrlMap.get(mappedFn) || `${projectRoute}`;
170170
}
171171

172+
case 'mistralai':
172173
case 'anthropic': {
173174
if (mappedFn === 'chatComplete' || mappedFn === 'messages') {
174175
return `${projectRoute}/publishers/${provider}/models/${model}:rawPredict`;

src/providers/google-vertex-ai/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ import {
5151
VertexAnthropicMessagesConfig,
5252
VertexAnthropicMessagesResponseTransform,
5353
} from './messages';
54+
import {
55+
GetMistralAIChatCompleteResponseTransform,
56+
GetMistralAIChatCompleteStreamChunkTransform,
57+
MistralAIChatCompleteConfig,
58+
} from '../mistral-ai/chatComplete';
5459

5560
const VertexConfig: ProviderConfigs = {
5661
api: VertexApiConfig,
@@ -162,6 +167,17 @@ const VertexConfig: ProviderConfigs = {
162167
...responseTransforms,
163168
},
164169
};
170+
case 'mistralai':
171+
return {
172+
chatComplete: MistralAIChatCompleteConfig,
173+
api: GoogleApiConfig,
174+
responseTransforms: {
175+
chatComplete:
176+
GetMistralAIChatCompleteResponseTransform(GOOGLE_VERTEX_AI),
177+
'stream-chatComplete':
178+
GetMistralAIChatCompleteStreamChunkTransform(GOOGLE_VERTEX_AI),
179+
},
180+
};
165181
default:
166182
return baseConfig;
167183
}

0 commit comments

Comments
 (0)