File size: 14,138 Bytes
430c991 c10af0f 430c991 c10af0f 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 73cb3e1 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 c10af0f 105e682 430c991 c10af0f 430c991 105e682 c10af0f 430c991 105e682 430c991 105e682 430c991 c10af0f 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 105e682 430c991 73cb3e1 430c991 105e682 430c991 73cb3e1 430c991 73cb3e1 430c991 105e682 430c991 73cb3e1 105e682 430c991 105e682 430c991 73cb3e1 430c991 105e682 430c991 73cb3e1 430c991 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
import { serve } from "https://deno.land/[email protected]/http/server.ts";
// Julep API Base URL (fixed)
const JULEP_API_BASE = "https://api.julep.ai/api";
// Hardcoded list of models (Agent IDs in this context)
const HARDCODED_MODELS = [
'mistral-large-2411', 'o1', 'text-embedding-3-large', 'vertex_ai/text-embedding-004',
'claude-3.5-haiku', 'cerebras/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b',
'magnum-v4-72b', 'voyage-multilingual-2', 'claude-3-haiku', 'gpt-4o',
'BAAI/bge-m3', 'openrouter/meta-llama/llama-4-maverick', 'openrouter/meta-llama/llama-4-scout',
'claude-3.5-sonnet', 'hermes-3-llama-3.1-70b', 'claude-3.5-sonnet-20240620',
'qwen-2.5-72b-instruct', 'l3.3-euryale-70b', 'gpt-4o-mini', 'cerebras/llama-3.3-70b',
'o1-preview', 'gemini-1.5-pro-latest', 'l3.1-euryale-70b', 'claude-3-sonnet',
'Alibaba-NLP/gte-large-en-v1.5', 'openrouter/meta-llama/llama-4-scout:free',
'llama-3.1-70b', 'eva-qwen-2.5-72b', 'claude-3.5-sonnet-20241022', 'gemini-2.0-flash',
'deepseek-chat', 'o1-mini', 'eva-llama-3.33-70b', 'gemini-2.5-pro-preview-03-25',
'gemini-1.5-pro', 'gpt-4-turbo', 'openrouter/meta-llama/llama-4-maverick:free',
'o3-mini', 'claude-3.7-sonnet', 'voyage-3', 'cerebras/llama-3.1-8b', 'claude-3-opus'
];
// Helper function to get Julep API Key from Authorization header
function getJulepApiKey(req: Request): string | null {
const authHeader = req.headers.get("Authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7); // Extract the token after "Bearer "
}
return null;
}
// OpenAI Models endpoint handler (hardcoded)
async function handleModels(req: Request): Promise<Response> {
const julepApiKey = getJulepApiKey(req);
if (!julepApiKey) {
return new Response("Unauthorized: Missing or invalid Authorization header", { status: 401 });
}
// Format hardcoded models into OpenAI models format
const openaiModels = HARDCODED_MODELS.map((modelId) => ({
id: modelId,
object: "model",
created: Math.floor(Date.now() / 1000), // Use current time for creation
owned_by: "julep", // Or "openai" if you prefer
permission: [
{
id: `modelperm-${modelId}`,
object: "model_permission",
created: Math.floor(Date.now() / 1000),
allow_create_engine: false,
allow_sampling: true,
allow_logprobs: true,
allow_search_indices: false,
allow_view: true,
allow_fine_tuning: false,
organization: "*",
group: null,
is_blocking: false,
},
],
root: modelId,
parent: null,
}));
return new Response(JSON.stringify({ data: openaiModels, object: "list" }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
}
// OpenAI Chat Completions endpoint handler
async function handleChatCompletions(req: Request): Promise<Response> {
const julepApiKey = getJulepApiKey(req);
if (!julepApiKey) {
return new Response("Unauthorized: Missing or invalid Authorization header", { status: 401 });
}
const headers = {
"Authorization": `Bearer ${julepApiKey}`,
"Content-Type": "application/json",
};
let agentId: string | null = null; // Variable to store the created agent ID
let sessionId: string | null = null; // Variable to store the created session ID
try {
const requestBody = await req.json();
const { model, messages, stream, ...rest } = requestBody;
if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
return new Response("Invalid request body. 'model' and 'messages' are required.", { status: 400 });
}
// Check if the requested model is in our hardcoded list
if (!HARDCODED_MODELS.includes(model)) {
return new Response(`Invalid model: ${model}. Please use one of the available models.`, { status: 400 });
}
// 1. Create a new Agent for this request
const createAgentUrl = `${JULEP_API_BASE}/agents`;
const createAgentBody = {
name: model, // Set agent name to the model value
model: model, // Use the requested OpenAI model as the Julep Agent's model
about: model, // Set agent about to the model value
instructions: ["Follow user instructions carefully."], // Keep some default instructions
};
const createAgentResponse = await fetch(createAgentUrl, {
method: "POST",
headers,
body: JSON.stringify(createAgentBody),
});
if (!createAgentResponse.ok) {
const errorText = await createAgentResponse.text();
console.error(`Error creating Julep Agent: ${createAgentResponse.status} - ${errorText}`);
return new Response(`Error creating Julep Agent: ${createAgentResponse.statusText}`, { status: createAgentResponse.status });
}
const agentData = await createAgentResponse.json();
agentId = agentData.id; // Store the agent ID
// 2. Create a Session using the new Agent ID
const createSessionUrl = `${JULEP_API_BASE}/sessions`;
const createSessionBody = {
agent: agentId, // Use the newly created Agent ID
// You can add other Session creation parameters here if needed
};
const createSessionResponse = await fetch(createSessionUrl, {
method: "POST",
headers,
body: JSON.stringify(createSessionBody),
});
if (!createSessionResponse.ok) {
const errorText = await createSessionResponse.text();
console.error(`Error creating Julep Session: ${createSessionResponse.status} - ${errorText}`);
// Attempt to clean up the temporary agent
if (agentId) {
fetch(`${JULEP_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
return new Response(`Error creating Julep Session: ${createSessionResponse.statusText}`, { status: createSessionResponse.status });
}
const sessionData = await createSessionResponse.json();
sessionId = sessionData.id; // Store the session ID
// 3. Perform Chat Completion
const chatUrl = `${JULEP_API_BASE}/sessions/${sessionId}/chat`;
const chatBody = {
messages: messages.map((msg: any) => ({
role: msg.role,
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), // Handle potential object content
// Map other relevant fields if necessary
})),
stream: stream === true,
...rest, // Forward any other parameters from the OpenAI request
};
const chatResponse = await fetch(chatUrl, {
method: "POST",
headers,
body: JSON.stringify(chatBody),
});
// 4. Handle Response and Clean Up
if (!chatResponse.ok) {
// If the chat request itself fails, read the error body and then clean up
const errorText = await chatResponse.text();
console.error(`Error during Julep Chat Completion: ${chatResponse.status} - ${errorText}`);
// Attempt to clean up the temporary agent and session
if (sessionId) {
fetch(`${JULEP_API_BASE}/sessions/${sessionId}`, { method: "DELETE", headers }).catch(console.error);
}
if (agentId) {
fetch(`${JULEP_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
return new Response(`Error during Julep Chat Completion: ${chatResponse.statusText} - ${errorText}`, { status: chatResponse.status });
}
if (stream) {
// Handle streaming response (Server-Sent Events)
// Pipe the Julep response body directly to the client response body
// and add cleanup to the end of the stream.
const readableStream = chatResponse.body!.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream({
transform(chunk, controller) {
// Parse Julep streaming chunks and format as OpenAI SSE
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data:')) {
const data = JSON.parse(line.substring(5).trim());
// Format the Julep chunk data into OpenAI SSE format
const openaiChunk = {
id: data.id,
object: "chat.completion.chunk",
created: Math.floor(new Date(data.created_at).getTime() / 1000),
model: model, // Use the requested model ID
choices: data.choices.map((choice: any) => ({
index: choice.index,
delta: {
role: choice.delta.role,
content: choice.delta.content,
tool_calls: choice.delta.tool_calls ? toolCallDeltaToOpenAI(choice.delta.tool_calls) : undefined,
},
finish_reason: choice.finish_reason,
})),
};
controller.enqueue(`data: ${JSON.stringify(openaiChunk)}\n\n`);
} else {
// Pass through non-data lines like comments or empty lines if needed
controller.enqueue(`${line}\n`);
}
}
},
}));
// Attach cleanup to the end of the stream
// We need to duplicate the stream to be able to pipe it to the client response
// AND to a WritableStream for cleanup.
const [stream1, stream2] = readableStream.tee();
const cleanupPromise = new Promise<void>((resolve, reject) => {
stream2.pipeTo(new WritableStream({
close: () => {
if (sessionId) {
fetch(`${JULEP_API_BASE}/sessions/${sessionId}`, { method: "DELETE", headers }).catch(console.error);
}
if (agentId) {
fetch(`${JULEP_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
resolve();
},
abort: (reason) => {
console.error("Stream aborted:", reason);
if (sessionId) {
fetch(`${JULEP_API_BASE}/sessions/${sessionId}`, { method: "DELETE", headers }).catch(console.error);
}
if (agentId) {
fetch(`${JULEP_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
reject(reason);
}
})).catch(reject);
});
// Return the response with the first stream.
return new Response(stream1, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
status: 200,
});
} else {
// Handle non-streaming response
const julepChatData = await chatResponse.json();
const openaiCompletion = {
id: julepChatData.id,
object: "chat.completion",
created: Math.floor(new Date(julepChatData.created_at).getTime() / 1000),
model: model, // Use the requested model ID
choices: julepChatData.choices.map((choice: any) => ({
index: choice.index,
message: {
role: choice.message.role,
content: choice.message.content,
tool_calls: choice.message.tool_calls ? toolCallMessageToOpenAI(choice.message.tool_calls) : undefined,
},
finish_reason: choice.finish_reason,
})),
usage: julepChatData.usage ? {
prompt_tokens: julepChatData.usage.prompt_tokens,
completion_tokens: julepChatData.usage.completion_tokens,
total_tokens: julepChatData.usage.total_tokens,
} : undefined,
};
// Attempt to clean up the temporary agent and session (fire and forget)
if (sessionId) {
fetch(`${JULEP_API_BASE}/sessions/${sessionId}`, { method: "DELETE", headers }).catch(console.error);
}
if (agentId) {
fetch(`${JULEP_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
return new Response(JSON.stringify(openaiCompletion), {
headers: { "Content-Type": "application/json" },
status: 200,
});
}
} catch (error) {
console.error("Error handling chat completions request:", error);
// Attempt to clean up in case of errors before session/agent creation
if (sessionId) {
fetch(`${JULEP_API_BASE}/sessions/${sessionId}`, { method: "DELETE", headers }).catch(console.error);
}
if (agentId) {
fetch(`${Julep_API_BASE}/agents/${agentId}`, { method: "DELETE", headers }).catch(console.error);
}
return new Response("Internal Server Error", { status: 500 });
}
}
// Helper to format Julep ToolCall delta to OpenAI format
function toolCallDeltaToOpenAI(julepToolCalls: any[]): any[] {
return julepToolCalls.map(toolCall => {
// Assuming Julep's delta format for tool_calls is similar to the message format
// and contains function objects directly. Adjust if necessary.
return {
id: toolCall.id,
type: "function",
function: {
name: toolCall.function?.name,
arguments: toolCall.function?.arguments, // Arguments might be streamed as chunks
},
};
});
}
// Helper to format Julep ToolCall message to OpenAI format
function toolCallMessageToOpenAI(julepToolCalls: any[]): any[] {
return julepToolCalls.map(toolCall => {
return {
id: toolCall.id,
type: "function",
function: {
name: toolCall.function?.name,
arguments: toolCall.function?.arguments, // Arguments should be complete in non-streaming
},
};
});
}
// Main request handler
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/v1/models" && req.method === "GET") {
return handleModels(req);
} else if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
return handleChatCompletions(req);
} else {
return new Response("Not Found", { status: 404 });
}
}
console.log(`HTTP server running on http://localhost:8000`);
serve(handler, { port: 7860 }); |