isididiidid commited on
Commit
ea14b15
·
verified ·
1 Parent(s): 8021973

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +459 -0
app.js ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ });