Skip to content

Commit 005a323

Browse files
authored
Merge branch 'main' into feature/add-qualifire-integration
2 parents d3708ad + d56830b commit 005a323

Some content is hidden

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

56 files changed

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

plugins/default/addPrefix.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
import { getCurrentContentPart, setCurrentContentPart } from '../utils';
8+
9+
const addPrefixToCompletion = (
10+
context: PluginContext,
11+
prefix: string,
12+
eventType: HookEventType
13+
): Record<string, any> => {
14+
const transformedData: Record<string, any> = {
15+
request: { json: null },
16+
response: { json: null },
17+
};
18+
19+
const { content, textArray } = getCurrentContentPart(context, eventType);
20+
if (!content) {
21+
return transformedData;
22+
}
23+
24+
const updatedTexts = (
25+
Array.isArray(textArray) ? textArray : [String(textArray)]
26+
).map((text, index) => (index === 0 ? `${prefix}${text ?? ''}` : text));
27+
28+
setCurrentContentPart(context, eventType, transformedData, updatedTexts);
29+
return transformedData;
30+
};
31+
32+
const addPrefixToChatCompletion = (
33+
context: PluginContext,
34+
prefix: string,
35+
applyToRole: string = 'user',
36+
addToExisting: boolean = true,
37+
onlyIfEmpty: boolean = false,
38+
eventType: HookEventType
39+
): Record<string, any> => {
40+
const json = context.request.json;
41+
const updatedJson = { ...json };
42+
const messages = Array.isArray(json.messages) ? [...json.messages] : [];
43+
44+
// Find the target role message
45+
const targetIndex = messages.findIndex((msg) => msg.role === applyToRole);
46+
47+
// Helper to build a message content with the prefix in both chatComplete and messages formats
48+
const buildPrefixedContent = (existing: any): any => {
49+
if (existing == null || typeof existing === 'string') {
50+
return `${prefix}${existing ?? ''}`;
51+
}
52+
if (Array.isArray(existing)) {
53+
if (existing.length > 0 && existing[0]?.type === 'text') {
54+
const cloned = existing.map((item) => ({ ...item }));
55+
cloned[0].text = `${prefix}${cloned[0]?.text ?? ''}`;
56+
return cloned;
57+
}
58+
return [{ type: 'text', text: prefix }, ...existing];
59+
}
60+
return `${prefix}${String(existing)}`;
61+
};
62+
63+
// If the target role exists
64+
if (targetIndex !== -1) {
65+
const targetMsg = messages[targetIndex];
66+
const content = targetMsg?.content;
67+
68+
const isEmptyContent =
69+
(typeof content === 'string' && content.trim().length === 0) ||
70+
(Array.isArray(content) && content.length === 0);
71+
72+
if (onlyIfEmpty && !isEmptyContent) {
73+
// Respect onlyIfEmpty by skipping modification when non-empty
74+
return {
75+
request: { json: updatedJson },
76+
response: { json: null },
77+
};
78+
}
79+
80+
if (addToExisting) {
81+
// If this is the last message, leverage utils to ensure messages route compatibility
82+
if (targetIndex === messages.length - 1) {
83+
const transformedData: Record<string, any> = {
84+
request: { json: null },
85+
response: { json: null },
86+
};
87+
const { content: currentContent, textArray } = getCurrentContentPart(
88+
context,
89+
eventType
90+
);
91+
if (currentContent !== null) {
92+
const updatedTexts = (
93+
Array.isArray(textArray) ? textArray : [String(textArray)]
94+
).map((text, idx) => (idx === 0 ? `${prefix}${text ?? ''}` : text));
95+
setCurrentContentPart(
96+
context,
97+
eventType,
98+
transformedData,
99+
updatedTexts
100+
);
101+
}
102+
return transformedData;
103+
}
104+
105+
// Otherwise, modify the specific message inline
106+
messages[targetIndex] = {
107+
...targetMsg,
108+
content: buildPrefixedContent(targetMsg.content),
109+
};
110+
} else {
111+
// Create new message with prefix before the existing one
112+
const newMessage = {
113+
role: applyToRole,
114+
content:
115+
context.requestType === 'messages'
116+
? [{ type: 'text', text: prefix }]
117+
: prefix,
118+
};
119+
messages.splice(targetIndex, 0, newMessage);
120+
}
121+
} else {
122+
// No message with target role exists, create one
123+
const newMessage = {
124+
role: applyToRole,
125+
content:
126+
context.requestType === 'messages'
127+
? [{ type: 'text', text: prefix }]
128+
: prefix,
129+
};
130+
131+
if (applyToRole === 'system') {
132+
messages.unshift(newMessage);
133+
} else {
134+
messages.push(newMessage);
135+
}
136+
}
137+
138+
updatedJson.messages = messages;
139+
140+
return {
141+
request: {
142+
json: updatedJson,
143+
},
144+
response: {
145+
json: null,
146+
},
147+
};
148+
};
149+
150+
export const handler: PluginHandler = async (
151+
context: PluginContext,
152+
parameters: PluginParameters,
153+
eventType: HookEventType
154+
) => {
155+
let error = null;
156+
let verdict = true; // Always allow the request to continue
157+
let data = null;
158+
const transformedData: Record<string, any> = {
159+
request: {
160+
json: null,
161+
},
162+
response: {
163+
json: null,
164+
},
165+
};
166+
let transformed = false;
167+
168+
try {
169+
// Only process before request and only for completion/chat completion/messages
170+
if (
171+
eventType !== 'beforeRequestHook' ||
172+
!['complete', 'chatComplete', 'messages'].includes(
173+
context.requestType || ''
174+
)
175+
) {
176+
return {
177+
error: null,
178+
verdict: true,
179+
data: null,
180+
transformedData,
181+
transformed,
182+
};
183+
}
184+
185+
// Get prefix from parameters
186+
const prefix = parameters.prefix;
187+
if (!prefix || typeof prefix !== 'string') {
188+
return {
189+
error: { message: 'Prefix parameter is required and must be a string' },
190+
verdict: true,
191+
data: null,
192+
transformedData,
193+
transformed,
194+
};
195+
}
196+
197+
// Check if request JSON exists
198+
if (!context.request?.json) {
199+
return {
200+
error: { message: 'Request JSON is empty or missing' },
201+
verdict: true,
202+
data: null,
203+
transformedData,
204+
transformed,
205+
};
206+
}
207+
208+
let newTransformedData;
209+
210+
if (
211+
context.requestType &&
212+
['chatComplete', 'messages'].includes(context.requestType)
213+
) {
214+
// Handle chat completion
215+
newTransformedData = addPrefixToChatCompletion(
216+
context,
217+
prefix,
218+
parameters.applyToRole || 'user',
219+
parameters.addToExisting !== false, // default to true
220+
parameters.onlyIfEmpty === true, // default to false
221+
eventType
222+
);
223+
} else {
224+
// Handle regular completion
225+
newTransformedData = addPrefixToCompletion(context, prefix, eventType);
226+
}
227+
228+
Object.assign(transformedData, newTransformedData);
229+
transformed = true;
230+
231+
data = {
232+
prefix: prefix,
233+
requestType: context.requestType,
234+
applyToRole: parameters.applyToRole || 'user',
235+
addToExisting: parameters.addToExisting !== false,
236+
onlyIfEmpty: parameters.onlyIfEmpty === true,
237+
};
238+
} catch (e: any) {
239+
delete e.stack;
240+
error = {
241+
message: `Error in addPrefix plugin: ${e.message || 'Unknown error'}`,
242+
originalError: e,
243+
};
244+
}
245+
246+
return { error, verdict, data, transformedData, transformed };
247+
};

0 commit comments

Comments
 (0)