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 });