isididiidid commited on
Commit
480a4ad
·
verified ·
1 Parent(s): 18e2344

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +470 -430
app.js CHANGED
@@ -1,459 +1,499 @@
1
  // app.js
2
- // 导入所需的Node.js模块
3
- const http = require('http');
4
- const { v4: uuidv4 } = require('uuid');
5
  const fetch = require('node-fetch');
 
 
6
 
7
- const JULEP_API_BASE_URL = "https://api.julep.ai/api";
8
- const PORT = process.env.PORT || 7860; // Hugging Face通常使用7860端口
9
-
10
- // 支持的模型列表
11
- const supportedModels = [
12
- 'mistral-large-2411', 'o1', 'text-embedding-3-large', 'vertex_ai/text-embedding-004',
13
- 'claude-3.5-haiku', 'cerebras/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b',
14
- 'magnum-v4-72b', 'voyage-multilingual-2', 'claude-3-haiku', 'gpt-4o', 'BAAI/bge-m3',
15
- 'openrouter/meta-llama/llama-4-maverick', 'openrouter/meta-llama/llama-4-scout',
16
- 'claude-3.5-sonnet', 'hermes-3-llama-3.1-70b', 'claude-3.5-sonnet-20240620',
17
- 'qwen-2.5-72b-instruct', 'l3.3-euryale-70b', 'gpt-4o-mini', 'cerebras/llama-3.3-70b',
18
- 'o1-preview', 'gemini-1.5-pro-latest', 'l3.1-euryale-70b', 'claude-3-sonnet',
19
- 'Alibaba-NLP/gte-large-en-v1.5', 'openrouter/meta-llama/llama-4-scout:free',
20
- 'llama-3.1-70b', 'eva-qwen-2.5-72b', 'claude-3.5-sonnet-20241022', 'gemini-2.0-flash',
21
- 'deepseek-chat', 'o1-mini', 'eva-llama-3.33-70b', 'gemini-2.5-pro-preview-03-25',
22
- 'gemini-1.5-pro', 'gpt-4-turbo', 'openrouter/meta-llama/llama-4-maverick:free',
23
- 'o3-mini', 'claude-3.7-sonnet', 'voyage-3', 'cerebras/llama-3.1-8b', 'claude-3-opus'
24
  ];
25
 
26
- // 处理/v1/models端点
27
- function listModelsHandler(res) {
28
- const models = supportedModels.map(modelId => ({
29
- id: modelId,
30
- object: "model",
31
- created: Math.floor(Date.now() / 1000),
32
- owned_by: "julep-proxy",
33
- }));
34
-
35
- const responseBody = {
36
- object: "list",
37
- data: models,
38
- };
39
-
40
- res.writeHead(200, { 'Content-Type': 'application/json' });
41
- res.end(JSON.stringify(responseBody));
42
- }
43
 
44
- // 将OpenAI格式转换为Julep格式
45
- function convertOpenaiToJulep(openaiPayload) {
46
- // 处理system消息
47
- let systemMessage = null;
48
- let userMessages = [];
49
-
50
- // 分离system消息和其他消息
51
- if (openaiPayload.messages && Array.isArray(openaiPayload.messages)) {
52
- openaiPayload.messages.forEach(msg => {
53
- if (msg.role === 'system') {
54
- // 保存system消息,Julep可能有特殊处理方式
55
- systemMessage = msg;
56
- } else {
57
- userMessages.push(msg);
58
- }
59
- });
60
- }
61
-
62
- // 创建Julep格式的payload
63
- const julepPayload = {
64
- messages: userMessages.map(msg => ({
65
- role: msg.role,
66
- content: msg.content,
67
- name: msg.name,
68
- tool_call_id: msg.tool_call_id,
69
- tool_calls: msg.tool_calls ? msg.tool_calls.map(tc => ({
70
- type: tc.type,
71
- function: tc.function ? {
72
- name: tc.function.name,
73
- arguments: tc.function.arguments
74
- } : undefined,
75
- integration: tc.integration,
76
- system: tc.system,
77
- api_call: tc.api_call,
78
- computer_20241022: tc.computer_20241022,
79
- text_editor_20241022: tc.text_editor_20241022,
80
- bash_20241022: tc.bash_20241022,
81
- id: tc.id,
82
- })) : undefined,
83
- })),
84
- tools: openaiPayload.tools,
85
- tool_choice: openaiPayload.tool_choice,
86
- recall: openaiPayload.recall,
87
- save: openaiPayload.save,
88
- model: openaiPayload.model,
89
- stream: openaiPayload.stream,
90
- stop: openaiPayload.stop,
91
- seed: openaiPayload.seed,
92
- max_tokens: openaiPayload.max_tokens,
93
- logit_bias: openaiPayload.logit_bias,
94
- response_format: openaiPayload.response_format,
95
- agent: openaiPayload.agent,
96
- repetition_penalty: openaiPayload.repetition_penalty,
97
- length_penalty: openaiPayload.length_penalty,
98
- min_p: openaiPayload.min_p,
99
- frequency_penalty: openaiPayload.frequency_penalty,
100
- presence_penalty: openaiPayload.presence_penalty,
101
- temperature: openaiPayload.temperature,
102
- top_p: openaiPayload.top_p,
103
- };
104
-
105
- // 如果有system消息,添加到Julep payload中
106
- // Julep API可能使用system_prompt字段或其他方式处理system消息
107
- if (systemMessage) {
108
- julepPayload.system_prompt = systemMessage.content;
109
- }
110
-
111
- // 删除可能错误包含的session_id
112
- delete julepPayload.session_id;
113
-
114
- return julepPayload;
115
  }
116
 
117
- // 将非流式Julep响应转换为OpenAI格式
118
- function convertJulepToOpenai(julepData, model, sessionId) {
119
- const openaiResponse = {
120
- id: sessionId,
121
- object: "chat.completion",
122
- created: Math.floor(new Date(julepData.created_at).getTime() / 1000),
123
- model: model,
124
- choices: julepData.choices.map(choice => ({
125
- index: choice.index,
126
- message: {
127
- role: choice.message?.role || "assistant",
128
- content: choice.message?.content || "",
129
- tool_calls: choice.message?.tool_calls ? choice.message.tool_calls.map(tc => ({
130
- id: tc.id,
131
- type: tc.type,
132
- function: tc.function ? {
133
- name: tc.function.name,
134
- arguments: tc.function.arguments
135
- } : undefined,
136
- })) : undefined,
137
- },
138
- finish_reason: choice.finish_reason,
139
- })),
140
- usage: julepData.usage ? {
141
- prompt_tokens: julepData.usage.prompt_tokens,
142
- completion_tokens: julepData.usage.completion_tokens,
143
- total_tokens: julepData.usage.total_tokens,
144
- } : undefined,
145
- };
146
-
147
- return openaiResponse;
148
  }
149
 
150
- // 将单个Julep流式块转换为OpenAI流式格式
151
- function convertJulepChunkToOpenai(julepChunk, model, sessionId) {
152
- const openaiChunk = {
153
- id: sessionId,
154
- object: "chat.completion.chunk",
155
- created: Math.floor(Date.now() / 1000),
156
- model: model,
157
- choices: julepChunk.choices.map(choice => {
158
- const openaiChoice = {
159
- index: choice.index,
160
- delta: {
161
- role: choice.delta?.role,
162
- content: choice.delta?.content,
163
- tool_calls: choice.delta?.tool_calls ? choice.delta.tool_calls.map(tc => ({
164
- id: tc.id,
165
- type: tc.type,
166
- function: tc.function ? {
167
- name: tc.function.name,
168
- arguments: tc.function.arguments
169
- } : undefined,
170
- })) : undefined,
171
- },
172
- finish_reason: choice.finish_reason,
173
- };
174
-
175
- // 清理空的delta字段
176
- if (openaiChoice.delta.role === undefined) delete openaiChoice.delta.role;
177
- if (openaiChoice.delta.content === undefined) delete openaiChoice.delta.content;
178
- if (openaiChoice.delta.tool_calls === undefined) delete openaiChoice.delta.tool_calls;
179
- if (Object.keys(openaiChoice.delta).length === 0 && openaiChoice.finish_reason === undefined) {
180
- delete openaiChoice.delta;
181
- }
182
-
183
- return openaiChoice;
184
- }),
185
- };
186
-
187
- return openaiChunk;
188
  }
189
 
190
- // 处理/v1/chat/completions端点
191
- async function chatCompletionsHandler(req, res) {
192
- let body = '';
193
- req.on('data', chunk => {
194
- body += chunk.toString();
195
- });
196
 
197
- req.on('end', async () => {
198
- try {
199
- // 检查授权头
200
- if (!req.headers.authorization) {
201
- res.writeHead(401, { 'Content-Type': 'application/json' });
202
- res.end(JSON.stringify({ error: "Authorization header is required." }));
203
- return;
204
- }
205
-
206
- const openaiPayload = JSON.parse(body);
207
-
208
- // 1. 为此会话创建一个新的Agent
209
- const agentId = uuidv4();
210
-
211
- // 检查是否有system消息,如果有,将其添加到agent的描述中
212
- let systemPrompt = "";
213
- if (openaiPayload.messages && Array.isArray(openaiPayload.messages)) {
214
- const systemMessage = openaiPayload.messages.find(msg => msg.role === 'system');
215
- if (systemMessage) {
216
- systemPrompt = systemMessage.content;
217
  }
218
- }
219
-
220
- const createAgentPayload = {
221
- name: `temp-agent-${agentId}`,
222
- about: "Temporary agent created for a chat session.",
223
- system_prompt: systemPrompt || undefined // 如果有system消息,添加到agent配置中
224
- };
225
-
226
- const createAgentResponse = await fetch(`${JULEP_API_BASE_URL}/agents/${agentId}`, {
227
- method: "POST",
228
- headers: {
229
- 'Content-Type': 'application/json',
230
- 'Authorization': req.headers.authorization
231
- },
232
- body: JSON.stringify(createAgentPayload),
233
- });
234
-
235
- if (!createAgentResponse.ok) {
236
- console.error("Failed to create agent:", await createAgentResponse.text());
237
- res.writeHead(createAgentResponse.status, { 'Content-Type': 'application/json' });
238
- res.end(JSON.stringify({ error: "Failed to initialize chat session (agent creation failed)." }));
239
- return;
240
- }
241
-
242
- // 2. 使用创建的Agent创建一个新的Session
243
- const sessionId = uuidv4();
244
- const createSessionPayload = {
245
- agent: agentId,
246
- };
247
-
248
- const createSessionResponse = await fetch(`${JULEP_API_BASE_URL}/sessions/${sessionId}`, {
249
- method: "POST",
250
- headers: {
251
- 'Content-Type': 'application/json',
252
- 'Authorization': req.headers.authorization
 
 
 
253
  },
254
- body: JSON.stringify(createSessionPayload),
255
- });
256
-
257
- if (!createSessionResponse.ok) {
258
- console.error("Failed to create session:", await createSessionResponse.text());
259
- res.writeHead(createSessionResponse.status, { 'Content-Type': 'application/json' });
260
- res.end(JSON.stringify({ error: "Failed to initialize chat session (session creation failed)." }));
261
- return;
262
- }
263
-
264
- // 3. 使用新的session ID发起聊天
265
- const julepPayload = convertOpenaiToJulep(openaiPayload);
266
- const julepUrl = `${JULEP_API_BASE_URL}/sessions/${sessionId}/chat`;
267
-
268
- const julepResponse = await fetch(julepUrl, {
269
- method: "POST",
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- 'Authorization': req.headers.authorization
273
  },
274
- body: JSON.stringify(julepPayload),
275
- });
276
-
277
- // 处理流式响应
278
- if (openaiPayload.stream && julepResponse.headers.get('content-type')?.includes('text/event-stream')) {
279
- // 设置正确的响应头
280
- res.writeHead(200, {
281
- 'Content-Type': 'text/event-stream',
282
- 'Cache-Control': 'no-cache',
283
- 'Connection': 'keep-alive',
284
- 'Transfer-Encoding': 'chunked'
285
- });
286
 
287
- // 使用更可靠的方式处理流
288
- const stream = julepResponse.body;
289
-
290
- // 创建一个TextDecoder来处理二进制数据
291
- const decoder = new TextDecoder();
292
- let buffer = '';
293
-
294
- // 处理数据块
295
- stream.on('data', (chunk) => {
296
- // 将二进制数据转换为文本
297
- const textChunk = decoder.decode(chunk, { stream: true });
298
- buffer += textChunk;
299
-
300
- // 处理完整的SSE事件
301
- const lines = buffer.split('\n');
302
- // 保留最后一个可能不完整的行
303
- buffer = lines.pop() || '';
304
-
305
- for (const line of lines) {
306
- if (line.startsWith('data:')) {
307
- const data = line.substring(5).trim();
308
-
309
- // 处理结束标记
310
- if (data === '[DONE]') {
311
- res.write('data: [DONE]\n\n');
312
- continue;
313
- }
314
-
315
- try {
316
- // 解析Julep的JSON响应
317
- const julepChunk = JSON.parse(data);
318
- // 转换为OpenAI格式
319
- const openaiChunk = convertJulepChunkToOpenai(julepChunk, openaiPayload.model || 'julep-model', sessionId);
320
- // 发送给客户端
321
- res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
322
- } catch (parseError) {
323
- console.error("Error parsing Julep stream chunk:", parseError, "Raw data:", data);
324
- // 可选:发送错误信息给客户端
325
- // res.write(`data: ${JSON.stringify({ error: "Failed to parse response" })}\n\n`);
326
- }
327
- } else if (line.trim() && !line.startsWith(':')) {
328
- // 记录非注释的未知行类型
329
- console.warn("Received unexpected stream line:", line);
330
  }
331
- }
332
-
333
- // 确保数据立即发送
334
- res.flushHeaders();
335
- });
336
-
337
- // 处理流结束
338
- stream.on('end', () => {
339
- // 确保发送[DONE]标记(如果Julep没有发送)
340
- if (buffer.trim()) {
341
- // 处理缓冲区中剩余的数据
342
- if (buffer.startsWith('data:')) {
343
- const data = buffer.substring(5).trim();
344
- if (data && data !== '[DONE]') {
345
- try {
346
- const julepChunk = JSON.parse(data);
347
- const openaiChunk = convertJulepChunkToOpenai(julepChunk, openaiPayload.model || 'julep-model', sessionId);
348
- res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
349
- } catch (e) {
350
- console.error("Error parsing final chunk:", e);
351
  }
352
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  }
354
- }
355
-
356
- // 发送最终的[DONE]标记
357
- res.write('data: [DONE]\n\n');
358
- res.end();
359
- console.log(`Stream completed for session ${sessionId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  });
361
-
362
- // 处理错误
363
- stream.on('error', (error) => {
364
- console.error("Stream error:", error);
365
- // 尝试发送错误信息给客户端
366
- try {
367
- res.write(`data: ${JSON.stringify({ error: "Stream error occurred" })}\n\n`);
368
- res.write('data: [DONE]\n\n');
369
- } catch (e) {
370
- // 可能客户端已断开连接
371
- }
372
- res.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  });
374
-
375
- // 处理客户端断开连接
376
- req.on('close', () => {
377
- console.log(`Client disconnected for session ${sessionId}`);
378
- // 可以选择在这里清理资源
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  });
380
- } else {
381
- // 处理非流式响应
382
- const julepData = await julepResponse.json();
383
- const openaiResponse = convertJulepToOpenai(julepData, openaiPayload.model || 'julep-model', sessionId);
384
-
385
- res.writeHead(julepResponse.status, { 'Content-Type': 'application/json' });
386
- res.end(JSON.stringify(openaiResponse));
387
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  } catch (error) {
389
- console.error("Error processing request:", error);
390
- res.writeHead(500, { 'Content-Type': 'application/json' });
391
- res.end(JSON.stringify({ error: "Internal Server Error", details: error.message }));
 
 
 
392
  }
393
- });
394
- }
395
 
396
- // 创建HTTP服务器
397
- const server = http.createServer((req, res) => {
398
- const url = new URL(req.url, `http://${req.headers.host}`);
399
-
400
- // 处理CORS预检请求
401
- if (req.method === 'OPTIONS') {
402
- res.writeHead(204, {
403
- 'Access-Control-Allow-Origin': '*',
404
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
405
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
406
- 'Access-Control-Max-Age': '86400'
407
- });
408
- res.end();
409
- return;
410
- }
411
-
412
- // 为所有响应添加CORS头
413
- res.setHeader('Access-Control-Allow-Origin', '*');
414
-
415
- // 处理/v1/models端点
416
- if (url.pathname === '/v1/models' && req.method === 'GET') {
417
- listModelsHandler(res);
418
- }
419
- // 处理/v1/chat/completions端点
420
- else if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
421
- chatCompletionsHandler(req, res);
422
- }
423
- // 处理根路径,返回简单的HTML页面
424
- else if (url.pathname === '/' && req.method === 'GET') {
425
- res.writeHead(200, { 'Content-Type': 'text/html' });
426
- res.end(`
427
- <!DOCTYPE html>
428
- <html>
429
- <head>
430
- <title>Julep API Proxy</title>
431
- <style>
432
- body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
433
- h1 { color: #333; }
434
- pre { background: #f4f4f4; padding: 10px; border-radius: 5px; }
435
- </style>
436
- </head>
437
- <body>
438
- <h1>Julep API Proxy</h1>
439
- <p>This server proxies requests to the Julep API using OpenAI-compatible endpoints:</p>
440
- <ul>
441
- <li><code>GET /v1/models</code> - List available models</li>
442
- <li><code>POST /v1/chat/completions</code> - Create a chat completion</li>
443
- </ul>
444
- <p>Server is running on port ${PORT}</p>
445
- </body>
446
- </html>
447
- `);
448
- }
449
- // 返回404
450
- else {
451
- res.writeHead(404, { 'Content-Type': 'application/json' });
452
- res.end(JSON.stringify({ error: "Not Found" }));
453
- }
454
  });
455
 
456
- // 启动服务器
457
- server.listen(PORT, () => {
458
- console.log(`Server running on port ${PORT}`);
459
  });
 
 
1
  // app.js
2
+ const express = require('express');
 
 
3
  const fetch = require('node-fetch');
4
+ const app = express();
5
+ const PORT = process.env.PORT || 7860;
6
 
7
+ // Julep API Base URL (fixed)
8
+ const JULEP_API_BASE = "https://api.julep.ai/api";
9
+
10
+ // Hardcoded list of models (Agent IDs in this context)
11
+ const HARDCODED_MODELS = [
12
+ 'mistral-large-2411', 'o1', 'text-embedding-3-large', 'vertex_ai/text-embedding-004',
13
+ 'claude-3.5-haiku', 'cerebras/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b',
14
+ 'magnum-v4-72b', 'voyage-multilingual-2', 'claude-3-haiku', 'gpt-4o',
15
+ 'BAAI/bge-m3', 'openrouter/meta-llama/llama-4-maverick', 'openrouter/meta-llama/llama-4-scout',
16
+ 'claude-3.5-sonnet', 'hermes-3-llama-3.1-70b', 'claude-3.5-sonnet-20240620',
17
+ 'qwen-2.5-72b-instruct', 'l3.3-euryale-70b', 'gpt-4o-mini', 'cerebras/llama-3.3-70b',
18
+ 'o1-preview', 'gemini-1.5-pro-latest', 'l3.1-euryale-70b', 'claude-3-sonnet',
19
+ 'Alibaba-NLP/gte-large-en-v1.5', 'openrouter/meta-llama/llama-4-scout:free',
20
+ 'llama-3.1-70b', 'eva-qwen-2.5-72b', 'claude-3.5-sonnet-20241022', 'gemini-2.0-flash',
21
+ 'deepseek-chat', 'o1-mini', 'eva-llama-3.33-70b', 'gemini-2.5-pro-preview-03-25',
22
+ 'gemini-1.5-pro', 'gpt-4-turbo', 'openrouter/meta-llama/llama-4-maverick:free',
23
+ 'o3-mini', 'claude-3.7-sonnet', 'voyage-3', 'cerebras/llama-3.1-8b', 'claude-3-opus'
24
  ];
25
 
26
+ // --- Helper Functions ---
27
+
28
+ // Define acceptable log levels
29
+ const LogLevels = {
30
+ DEBUG: 'debug',
31
+ INFO: 'info',
32
+ WARN: 'warn',
33
+ ERROR: 'error',
34
+ TRACE: 'trace'
35
+ };
 
 
 
 
 
 
 
36
 
37
+ function log(level, message, data = null) {
38
+ // Basic check if the console object has the method
39
+ if (typeof console[level] === 'function') {
40
+ console[level](`[${level.toUpperCase()}] ${new Date().toISOString()} - ${message}${data ? ':' : ''}`, data !== null ? JSON.stringify(data, null, 2) : '');
41
+ } else {
42
+ // Fallback for potentially missing methods like 'trace' in some environments
43
+ console.log(`[${level.toUpperCase()}] ${new Date().toISOString()} - ${message}${data ? ':' : ''}`, data !== null ? JSON.stringify(data, null, 2) : '');
44
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
+ function getJulepApiKey(req) {
48
+ const authHeader = req.headers.authorization;
49
+ if (authHeader && authHeader.startsWith("Bearer ")) {
50
+ log(LogLevels.DEBUG, 'Extracted Julep API Key successfully.');
51
+ return authHeader.substring(7);
52
+ }
53
+ log(LogLevels.WARN, 'Could not extract Julep API Key from Authorization header.');
54
+ return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
+ // Helper for small delays
58
+ function sleep(ms) {
59
+ return new Promise(resolve => setTimeout(resolve, ms));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
+ // Note: Using fire-and-forget for background tasks
63
+ async function cleanupJulepResources(agentId, sessionId, headers) {
64
+ log(LogLevels.INFO, 'Attempting Julep resource cleanup.', { agentId, sessionId });
65
+ const cleanupPromises = [];
 
 
66
 
67
+ // Define cleanup logic as separate async functions for clarity
68
+ const deleteResource = async (url, type, id) => {
69
+ try {
70
+ log(LogLevels.DEBUG, `Sending DELETE request for ${type} ${id} to: ${url}`);
71
+ const response = await fetch(url, { method: "DELETE", headers });
72
+ const responseText = await response.text(); // Get text regardless of status
73
+ if (!response.ok) {
74
+ log(LogLevels.WARN, `Cleanup failed for ${type} ${id}: ${response.status} ${response.statusText}`, { body: responseText });
75
+ } else {
76
+ log(LogLevels.INFO, `Cleanup successful for ${type} ${id}.`, { status: response.status, body: responseText });
77
+ }
78
+ } catch (err) {
79
+ log(LogLevels.ERROR, `Cleanup error during fetch for ${type} ${id}: ${err instanceof Error ? err.message : String(err)}`, { error: err });
 
 
 
 
 
 
 
80
  }
81
+ };
82
+
83
+ if (sessionId) {
84
+ const sessionDeleteUrl = `${JULEP_API_BASE}/sessions/${sessionId}`;
85
+ cleanupPromises.push(deleteResource(sessionDeleteUrl, 'session', sessionId));
86
+ }
87
+ if (agentId) {
88
+ const agentDeleteUrl = `${JULEP_API_BASE}/agents/${agentId}`;
89
+ // Add a small delay before deleting the agent, sometimes helps if session deletion is slow
90
+ await sleep(100);
91
+ cleanupPromises.push(deleteResource(agentDeleteUrl, 'agent', agentId));
92
+ }
93
+
94
+ if (cleanupPromises.length > 0) {
95
+ log(LogLevels.DEBUG, `Waiting for ${cleanupPromises.length} cleanup promises.`);
96
+ // Run cleanup in background
97
+ Promise.allSettled(cleanupPromises)
98
+ .then(results => {
99
+ log(LogLevels.INFO, 'Cleanup promises settled.', { results });
100
+ })
101
+ .catch(error => {
102
+ log(LogLevels.ERROR, 'Unexpected error during Promise.allSettled for cleanup.', { error });
103
+ });
104
+ } else {
105
+ log(LogLevels.INFO, 'No Julep resources to clean up.');
106
+ }
107
+ }
108
+
109
+ // Helper to format Julep ToolCall delta to OpenAI format
110
+ function toolCallDeltaToOpenAI(julepToolCalls) {
111
+ if (!julepToolCalls) return undefined;
112
+ return julepToolCalls.map((toolCall, index) => ({
113
+ index: toolCall.index ?? index,
114
+ id: toolCall.id,
115
+ type: "function",
116
+ function: {
117
+ name: toolCall.function?.name,
118
+ arguments: toolCall.function?.arguments,
119
  },
120
+ }));
121
+ }
122
+
123
+ // Helper to format Julep ToolCall message to OpenAI format
124
+ function toolCallMessageToOpenAI(julepToolCalls) {
125
+ if (!julepToolCalls) return undefined;
126
+ return julepToolCalls.map(toolCall => ({
127
+ id: toolCall.id,
128
+ type: "function",
129
+ function: {
130
+ name: toolCall.function?.name,
131
+ arguments: toolCall.function?.arguments,
 
 
 
 
 
 
 
132
  },
133
+ }));
134
+ }
 
 
 
 
 
 
 
 
 
 
135
 
136
+ // Helper function to simulate streaming from a complete response
137
+ async function simulateStream(julepChatData, requestedModel, res) {
138
+ log(LogLevels.INFO, 'Starting stream simulation.');
139
+ try {
140
+ const baseChunk = {
141
+ id: julepChatData.id || `chatcmpl-sim-${Date.now()}`,
142
+ object: "chat.completion.chunk",
143
+ created: Math.floor(new Date(julepChatData.created_at || Date.now()).getTime() / 1000),
144
+ model: requestedModel,
145
+ system_fingerprint: julepChatData.system_fingerprint || null,
146
+ };
147
+
148
+ for (const [index, choice] of julepChatData.choices.entries()) {
149
+ log(LogLevels.DEBUG, `Simulating stream for choice index ${index}.`);
150
+ const role = choice.message?.role;
151
+ const content = choice.message?.content;
152
+ const toolCallsInput = choice.message?.tool_calls;
153
+ const toolCallsDelta = toolCallsInput ? toolCallDeltaToOpenAI(toolCallsInput) : undefined; // Format as delta
154
+ const finishReason = choice.finish_reason;
155
+
156
+ // 1. Send role chunk
157
+ if (role) {
158
+ const roleChunk = { ...baseChunk, choices: [{ index: index, delta: { role: role }, finish_reason: null }] };
159
+ log(LogLevels.DEBUG, 'Sending role chunk:', roleChunk);
160
+ res.write(`data: ${JSON.stringify(roleChunk)}\n\n`);
161
+ await sleep(5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
+
164
+ // 2. Send tool calls chunk(s) if they exist
165
+ if (toolCallsDelta && toolCallsDelta.length > 0) {
166
+ const toolCallDeltaChunk = { ...baseChunk, choices: [{ index: index, delta: { tool_calls: toolCallsDelta }, finish_reason: null }] };
167
+ log(LogLevels.DEBUG, 'Sending tool_calls chunk:', toolCallDeltaChunk);
168
+ res.write(`data: ${JSON.stringify(toolCallDeltaChunk)}\n\n`);
169
+ await sleep(5);
170
+ }
171
+
172
+ // 3. Stream content
173
+ if (content && typeof content === 'string') {
174
+ log(LogLevels.DEBUG, `Streaming content for choice ${index} (length: ${content.length})`);
175
+ for (const char of content) {
176
+ const contentChunk = { ...baseChunk, choices: [{ index: index, delta: { content: char }, finish_reason: null }] };
177
+ res.write(`data: ${JSON.stringify(contentChunk)}\n\n`);
178
+ await sleep(2); // Simulate typing delay
 
 
 
 
179
  }
180
+ log(LogLevels.DEBUG, `Finished streaming content for choice ${index}`);
181
+ } else if (content) {
182
+ // Send non-string content as a single chunk (might be structured JSON etc.)
183
+ const contentChunk = { ...baseChunk, choices: [{ index: index, delta: { content: JSON.stringify(content) }, finish_reason: null }] };
184
+ log(LogLevels.DEBUG, 'Sending non-string content chunk:', contentChunk);
185
+ res.write(`data: ${JSON.stringify(contentChunk)}\n\n`);
186
+ await sleep(5);
187
+ }
188
+
189
+ // 4. Send finish reason chunk
190
+ if (finishReason) {
191
+ const finishChunk = { ...baseChunk, choices: [{ index: index, delta: {}, finish_reason: finishReason }] };
192
+ log(LogLevels.DEBUG, 'Sending finish reason chunk:', finishChunk);
193
+ res.write(`data: ${JSON.stringify(finishChunk)}\n\n`);
194
+ await sleep(5);
195
+ }
196
+ }
197
+
198
+ // 5. Send DONE marker
199
+ log(LogLevels.INFO, 'Sending [DONE] marker.');
200
+ res.write('data: [DONE]\n\n');
201
+ res.end();
202
+ log(LogLevels.INFO, 'Stream simulation completed successfully.');
203
+
204
+ } catch (error) {
205
+ log(LogLevels.ERROR, `Error during stream simulation: ${error instanceof Error ? error.message : String(error)}`, { error: error });
206
+ // Try to end the response if possible
207
+ try {
208
+ if (!res.headersSent) {
209
+ res.status(500).json({ error: 'Stream simulation error' });
210
+ } else {
211
+ res.end();
212
  }
213
+ } catch (endError) {
214
+ log(LogLevels.ERROR, 'Error ending response after stream error', { error: endError });
215
+ }
216
+ }
217
+ }
218
+
219
+ // --- Middleware ---
220
+ app.use(express.json());
221
+
222
+ // CORS middleware
223
+ app.use((req, res, next) => {
224
+ res.header('Access-Control-Allow-Origin', '*');
225
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
226
+ res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
227
+
228
+ if (req.method === 'OPTIONS') {
229
+ return res.status(204).end();
230
+ }
231
+ next();
232
+ });
233
+
234
+ // --- Routes ---
235
+ app.get('/v1/models', async (req, res) => {
236
+ log(LogLevels.INFO, 'Handling /v1/models request.');
237
+ const julepApiKey = getJulepApiKey(req);
238
+ if (!julepApiKey) {
239
+ log(LogLevels.WARN, 'Unauthorized /v1/models request (missing API key).');
240
+ // Optionally allow models request without key, or enforce it
241
+ }
242
+
243
+ const now = Math.floor(Date.now() / 1000);
244
+ const openaiModels = HARDCODED_MODELS.map((modelId) => ({
245
+ id: modelId, object: "model", created: now, owned_by: "julep",
246
+ permission: [{ id: `modelperm-${modelId}-${now}`, object: "model_permission", created: now, 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, }],
247
+ root: modelId, parent: null,
248
+ }));
249
+ log(LogLevels.DEBUG, 'Returning hardcoded models list.');
250
+ res.json({ data: openaiModels, object: "list" });
251
+ });
252
+
253
+ app.post('/v1/chat/completions', async (req, res) => {
254
+ log(LogLevels.INFO, 'Handling /v1/chat/completions request.');
255
+ const julepApiKey = getJulepApiKey(req);
256
+ if (!julepApiKey) {
257
+ log(LogLevels.ERROR, 'Unauthorized chat completions request: Missing Julep API Key.');
258
+ return res.status(401).send("Unauthorized: Missing or invalid Authorization header");
259
+ }
260
+
261
+ // Define headers early, use this single object throughout
262
+ const headers = {
263
+ "Authorization": `Bearer ${julepApiKey}`,
264
+ "Content-Type": "application/json",
265
+ };
266
+ log(LogLevels.DEBUG, 'Julep API request headers prepared (key omitted).', { "Content-Type": headers["Content-Type"] });
267
+
268
+ let agentId = null;
269
+ let sessionId = null;
270
+ let requestBody = req.body;
271
+
272
+ try {
273
+ const { model, messages, stream, ...rest } = requestBody;
274
+ const clientRequestedStream = stream === true;
275
+ log(LogLevels.INFO, `Request details: model=${model}, clientRequestedStream=${clientRequestedStream}`);
276
+
277
+ // Validate essential parameters
278
+ if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
279
+ log(LogLevels.ERROR, 'Invalid request body: "model" and "messages" are required.', { model, messages });
280
+ return res.status(400).send("Invalid request body. 'model' and 'messages' are required.");
281
+ }
282
+ if (!HARDCODED_MODELS.includes(model)) {
283
+ log(LogLevels.ERROR, `Invalid model requested: ${model}`);
284
+ return res.status(400).send(`Invalid model: ${model}. Please use one of the available models.`);
285
+ }
286
+ log(LogLevels.DEBUG, 'Request parameters validated.');
287
+
288
+ // --- Agent and Session Creation ---
289
+ // 2. Create Agent
290
+ const createAgentUrl = `${JULEP_API_BASE}/agents`;
291
+ const createAgentBody = {
292
+ name: `temp-openai-${model}-${Date.now()}`,
293
+ model: model,
294
+ about: `Temporary agent for OpenAI model ${model}`,
295
+ };
296
+ log(LogLevels.INFO, 'Attempting to create Julep Agent.', { url: createAgentUrl, body: createAgentBody });
297
+ const createAgentResponse = await fetch(createAgentUrl, {
298
+ method: "POST",
299
+ headers,
300
+ body: JSON.stringify(createAgentBody)
301
  });
302
+ log(LogLevels.DEBUG, `Create Agent response status: ${createAgentResponse.status}`);
303
+
304
+ if (!createAgentResponse.ok) {
305
+ const errorStatus = createAgentResponse.status;
306
+ const errorStatusText = createAgentResponse.statusText;
307
+ let errorText = "[Could not read error body]";
308
+ try {
309
+ errorText = await createAgentResponse.text();
310
+ } catch (e) {
311
+ log(LogLevels.WARN, `Could not read error text from createAgentResponse: ${e instanceof Error ? e.message : String(e)}`);
312
+ }
313
+ log(LogLevels.ERROR, `Error creating Julep Agent: ${errorStatus} - ${errorText}`);
314
+ return res.status(errorStatus).send(`Error creating Julep Agent: ${errorStatusText} - ${errorText}`);
315
+ }
316
+
317
+ let agentData;
318
+ try {
319
+ const agentResponseText = await createAgentResponse.text();
320
+ log(LogLevels.DEBUG, 'Create Agent raw response text:', agentResponseText);
321
+ agentData = JSON.parse(agentResponseText);
322
+ log(LogLevels.INFO, 'Julep Agent created successfully.', { agentData });
323
+ agentId = agentData.id;
324
+ } catch (e) {
325
+ log(LogLevels.ERROR, `Failed to parse Julep Agent creation response JSON: ${e instanceof Error ? e.message : String(e)}`, { error: e });
326
+ // Attempt cleanup (fire-and-forget)
327
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after agent parse error', err));
328
+ return res.status(500).send(`Internal Server Error: Failed to parse Julep Agent response. ${e instanceof Error ? e.message : String(e)}`);
329
+ }
330
+
331
+ // 3. Create Session
332
+ const createSessionUrl = `${JULEP_API_BASE}/sessions`;
333
+ const createSessionBody = { agent: agentId }; // Julep API uses agent
334
+ log(LogLevels.INFO, 'Attempting to create Julep Session.', { url: createSessionUrl, body: createSessionBody });
335
+ const createSessionResponse = await fetch(createSessionUrl, {
336
+ method: "POST",
337
+ headers,
338
+ body: JSON.stringify(createSessionBody)
339
  });
340
+ log(LogLevels.DEBUG, `Create Session response status: ${createSessionResponse.status}`);
341
+
342
+ if (!createSessionResponse.ok) {
343
+ const errorStatus = createSessionResponse.status;
344
+ const errorStatusText = createSessionResponse.statusText;
345
+ let errorText = "[Could not read error body]";
346
+ try {
347
+ errorText = await createSessionResponse.text();
348
+ } catch (e) {
349
+ log(LogLevels.WARN, `Could not read error text from createSessionResponse: ${e instanceof Error ? e.message : String(e)}`);
350
+ }
351
+ log(LogLevels.ERROR, `Error creating Julep Session: ${errorStatus} - ${errorText}`);
352
+ // Cleanup the agent we just created (fire-and-forget)
353
+ cleanupJulepResources(agentId, null, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after session creation error', err));
354
+ return res.status(errorStatus).send(`Error creating Julep Session: ${errorStatusText} - ${errorText}`);
355
+ }
356
+
357
+ let sessionData;
358
+ try {
359
+ const sessionResponseText = await createSessionResponse.text();
360
+ log(LogLevels.DEBUG, 'Create Session raw response text:', sessionResponseText);
361
+ sessionData = JSON.parse(sessionResponseText);
362
+ log(LogLevels.INFO, 'Julep Session created successfully.', { sessionData });
363
+ sessionId = sessionData.id;
364
+ } catch (e) {
365
+ log(LogLevels.ERROR, `Failed to parse Julep Session creation response JSON: ${e instanceof Error ? e.message : String(e)}`, { error: e });
366
+ // Cleanup agent and session (fire-and-forget)
367
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after session parse error', err));
368
+ return res.status(500).send(`Internal Server Error: Failed to parse Julep Session response. ${e instanceof Error ? e.message : String(e)}`);
369
+ }
370
+
371
+ // --- Perform Chat Completion (ALWAYS non-streaming to Julep) ---
372
+ // 4. Send Chat Request to Julep
373
+ const chatUrl = `${JULEP_API_BASE}/sessions/${sessionId}/chat`;
374
+ const chatBodyToJulep = {
375
+ messages: messages.map((msg) => ({
376
+ role: msg.role,
377
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
378
+ // Include tool_calls if present in the input message (OpenAI format)
379
+ tool_calls: msg.tool_calls, // Assuming Julep accepts OpenAI tool call format here
380
+ tool_call_id: msg.tool_call_id // If it's a tool response message
381
+ })),
382
+ stream: false, // Force non-streaming
383
+ ...rest, // Pass through other OpenAI parameters like temperature, top_p, etc.
384
+ };
385
+ log(LogLevels.INFO, 'Sending Chat request to Julep (forced non-stream).', { url: chatUrl });
386
+ log(LogLevels.DEBUG, 'Julep Chat Request Body:', chatBodyToJulep);
387
+ const chatResponse = await fetch(chatUrl, {
388
+ method: "POST",
389
+ headers,
390
+ body: JSON.stringify(chatBodyToJulep)
391
  });
392
+ log(LogLevels.DEBUG, `Julep Chat response status: ${chatResponse.status}`);
393
+
394
+ // --- Handle Julep Response ---
395
+ if (!chatResponse.ok) {
396
+ const errorStatus = chatResponse.status;
397
+ const errorStatusText = chatResponse.statusText;
398
+ let errorText = "[Could not read error body]";
399
+ try {
400
+ errorText = await chatResponse.text();
401
+ } catch (e) {
402
+ log(LogLevels.WARN, `Could not read error text from chatResponse: ${e instanceof Error ? e.message : String(e)}`);
403
+ }
404
+ log(LogLevels.ERROR, `Error during Julep Chat Completion: ${errorStatus} - ${errorText}`);
405
+ // Cleanup agent and session (fire-and-forget)
406
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after chat error', err));
407
+ return res.status(errorStatus).send(`Error during Julep Chat Completion: ${errorStatusText} - ${errorText}`);
408
+ }
409
+
410
+ // Julep request was successful, get the full JSON body
411
+ let julepChatData;
412
+ try {
413
+ const chatResponseText = await chatResponse.text();
414
+ log(LogLevels.DEBUG, 'Julep Chat raw response text:', chatResponseText);
415
+ julepChatData = JSON.parse(chatResponseText);
416
+ log(LogLevels.INFO, 'Julep chat completion successful.', { responseId: julepChatData.id })
417
+ log(LogLevels.DEBUG, 'Julep Chat response data:', julepChatData);
418
+ } catch (e) {
419
+ log(LogLevels.ERROR, `Failed to parse Julep Chat response JSON (status was OK): ${e instanceof Error ? e.message : String(e)}`, { error: e });
420
+ // Cleanup agent and session (fire-and-forget)
421
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after chat parse error', err));
422
+ return res.status(500).send(`Internal Server Error: Failed to parse Julep Chat response. ${e instanceof Error ? e.message : String(e)}`);
423
+ }
424
+
425
+ // *** Trigger cleanup NOW (fire-and-forget), before returning the response/stream ***
426
+ log(LogLevels.INFO, 'Julep chat successful, queueing cleanup.');
427
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed after successful chat', err));
428
+
429
+ // --- Format and Return Response to Client ---
430
+ // Access the actual chat response data
431
+ const julepResponseData = julepChatData;
432
+ if (!julepResponseData || !julepResponseData.choices) {
433
+ log(LogLevels.ERROR, 'Julep response format unexpected. Missing "response" or "response.choices".', { julepChatData });
434
+ return res.status(500).send('Internal Server Error: Unexpected format from Julep API.');
435
+ }
436
+
437
+ if (clientRequestedStream) {
438
+ log(LogLevels.INFO, 'Client requested stream, starting simulation.');
439
+ // Set headers for SSE
440
+ res.setHeader('Content-Type', 'text/event-stream');
441
+ res.setHeader('Cache-Control', 'no-cache');
442
+ res.setHeader('Connection', 'keep-alive');
443
+ res.flushHeaders(); // Flush the headers to establish SSE with client
444
+
445
+ // Start simulation
446
+ simulateStream(julepResponseData, model, res)
447
+ .catch(streamErr => {
448
+ log(LogLevels.ERROR, 'Stream simulation task failed.', { error: streamErr });
449
+ });
450
+ } else {
451
+ log(LogLevels.INFO, 'Client requested non-streaming response.');
452
+ // Format julepResponseData to OpenAI format
453
+ const openaiCompletion = {
454
+ id: julepResponseData.id || `chatcmpl-${Date.now()}`,
455
+ object: "chat.completion",
456
+ created: Math.floor(new Date(julepResponseData.created_at || Date.now()).getTime() / 1000),
457
+ model: model, // Use the originally requested model
458
+ choices: julepResponseData.choices.map((choice) => ({
459
+ index: choice.index,
460
+ message: {
461
+ role: choice.message.role,
462
+ content: choice.message.content,
463
+ // Use toolCallMessageToOpenAI here for the completed message format
464
+ tool_calls: choice.message.tool_calls ? toolCallMessageToOpenAI(choice.message.tool_calls) : undefined
465
+ },
466
+ finish_reason: choice.finish_reason
467
+ })),
468
+ usage: julepResponseData.usage ? {
469
+ prompt_tokens: julepResponseData.usage.prompt_tokens,
470
+ completion_tokens: julepResponseData.usage.completion_tokens,
471
+ total_tokens: julepResponseData.usage.total_tokens
472
+ } : undefined,
473
+ system_fingerprint: julepResponseData.system_fingerprint || null,
474
+ };
475
+ log(LogLevels.DEBUG, 'Formatted non-streaming OpenAI response:', openaiCompletion);
476
+ log(LogLevels.INFO, 'Returning non-streaming JSON response to client.');
477
+ res.json(openaiCompletion);
478
+ }
479
+
480
  } catch (error) {
481
+ // Catch errors from initial parsing, validation, or unexpected issues within the try block
482
+ log(LogLevels.ERROR, `Error in handleChatCompletions (outer catch): ${error instanceof Error ? error.message : String(error)}`, { error: error, agentId, sessionId });
483
+ // Use the headers defined at the start if available
484
+ // Attempt cleanup (fire-and-forget)
485
+ cleanupJulepResources(agentId, sessionId, headers).catch(err => log(LogLevels.ERROR, 'Background cleanup failed in outer catch block', err));
486
+ res.status(500).send(`Internal Server Error: ${error instanceof Error ? error.message : String(error)}`);
487
  }
488
+ });
 
489
 
490
+ // Health check endpoint for Hugging Face
491
+ app.get('/', (req, res) => {
492
+ res.send('Julep API Proxy is running');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  });
494
 
495
+ // Start the server
496
+ app.listen(PORT, () => {
497
+ log(LogLevels.INFO, `Server running on port ${PORT}`);
498
  });
499
+