Spaces:
Sleeping
Sleeping
// app.js | |
// 导入所需的Node.js模块 | |
const http = require('http'); | |
const { v4: uuidv4 } = require('uuid'); | |
const fetch = require('node-fetch'); | |
const JULEP_API_BASE_URL = "https://api.julep.ai/api"; | |
const PORT = process.env.PORT || 7860; // Hugging Face通常使用7860端口 | |
// 支持的模型列表 | |
const supportedModels = [ | |
'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' | |
]; | |
// 处理/v1/models端点 | |
function listModelsHandler(res) { | |
const models = supportedModels.map(modelId => ({ | |
id: modelId, | |
object: "model", | |
created: Math.floor(Date.now() / 1000), | |
owned_by: "julep-proxy", | |
})); | |
const responseBody = { | |
object: "list", | |
data: models, | |
}; | |
res.writeHead(200, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify(responseBody)); | |
} | |
// 将OpenAI格式转换为Julep格式 | |
function convertOpenaiToJulep(openaiPayload) { | |
// 处理system消息 | |
let systemMessage = null; | |
let userMessages = []; | |
// 分离system消息和其他消息 | |
if (openaiPayload.messages && Array.isArray(openaiPayload.messages)) { | |
openaiPayload.messages.forEach(msg => { | |
if (msg.role === 'system') { | |
// 保存system消息,Julep可能有特殊处理方式 | |
systemMessage = msg; | |
} else { | |
userMessages.push(msg); | |
} | |
}); | |
} | |
// 创建Julep格式的payload | |
const julepPayload = { | |
messages: userMessages.map(msg => ({ | |
role: msg.role, | |
content: msg.content, | |
name: msg.name, | |
tool_call_id: msg.tool_call_id, | |
tool_calls: msg.tool_calls ? msg.tool_calls.map(tc => ({ | |
type: tc.type, | |
function: tc.function ? { | |
name: tc.function.name, | |
arguments: tc.function.arguments | |
} : undefined, | |
integration: tc.integration, | |
system: tc.system, | |
api_call: tc.api_call, | |
computer_20241022: tc.computer_20241022, | |
text_editor_20241022: tc.text_editor_20241022, | |
bash_20241022: tc.bash_20241022, | |
id: tc.id, | |
})) : undefined, | |
})), | |
tools: openaiPayload.tools, | |
tool_choice: openaiPayload.tool_choice, | |
recall: openaiPayload.recall, | |
save: openaiPayload.save, | |
model: openaiPayload.model, | |
stream: openaiPayload.stream, | |
stop: openaiPayload.stop, | |
seed: openaiPayload.seed, | |
max_tokens: openaiPayload.max_tokens, | |
logit_bias: openaiPayload.logit_bias, | |
response_format: openaiPayload.response_format, | |
agent: openaiPayload.agent, | |
repetition_penalty: openaiPayload.repetition_penalty, | |
length_penalty: openaiPayload.length_penalty, | |
min_p: openaiPayload.min_p, | |
frequency_penalty: openaiPayload.frequency_penalty, | |
presence_penalty: openaiPayload.presence_penalty, | |
temperature: openaiPayload.temperature, | |
top_p: openaiPayload.top_p, | |
}; | |
// 如果有system消息,添加到Julep payload中 | |
// Julep API可能使用system_prompt字段或其他方式处理system消息 | |
if (systemMessage) { | |
julepPayload.system_prompt = systemMessage.content; | |
} | |
// 删除可能错误包含的session_id | |
delete julepPayload.session_id; | |
return julepPayload; | |
} | |
// 将非流式Julep响应转换为OpenAI格式 | |
function convertJulepToOpenai(julepData, model, sessionId) { | |
const openaiResponse = { | |
id: sessionId, | |
object: "chat.completion", | |
created: Math.floor(new Date(julepData.created_at).getTime() / 1000), | |
model: model, | |
choices: julepData.choices.map(choice => ({ | |
index: choice.index, | |
message: { | |
role: choice.message?.role || "assistant", | |
content: choice.message?.content || "", | |
tool_calls: choice.message?.tool_calls ? choice.message.tool_calls.map(tc => ({ | |
id: tc.id, | |
type: tc.type, | |
function: tc.function ? { | |
name: tc.function.name, | |
arguments: tc.function.arguments | |
} : undefined, | |
})) : undefined, | |
}, | |
finish_reason: choice.finish_reason, | |
})), | |
usage: julepData.usage ? { | |
prompt_tokens: julepData.usage.prompt_tokens, | |
completion_tokens: julepData.usage.completion_tokens, | |
total_tokens: julepData.usage.total_tokens, | |
} : undefined, | |
}; | |
return openaiResponse; | |
} | |
// 将单个Julep流式块转换为OpenAI流式格式 | |
function convertJulepChunkToOpenai(julepChunk, model, sessionId) { | |
const openaiChunk = { | |
id: sessionId, | |
object: "chat.completion.chunk", | |
created: Math.floor(Date.now() / 1000), | |
model: model, | |
choices: julepChunk.choices.map(choice => { | |
const openaiChoice = { | |
index: choice.index, | |
delta: { | |
role: choice.delta?.role, | |
content: choice.delta?.content, | |
tool_calls: choice.delta?.tool_calls ? choice.delta.tool_calls.map(tc => ({ | |
id: tc.id, | |
type: tc.type, | |
function: tc.function ? { | |
name: tc.function.name, | |
arguments: tc.function.arguments | |
} : undefined, | |
})) : undefined, | |
}, | |
finish_reason: choice.finish_reason, | |
}; | |
// 清理空的delta字段 | |
if (openaiChoice.delta.role === undefined) delete openaiChoice.delta.role; | |
if (openaiChoice.delta.content === undefined) delete openaiChoice.delta.content; | |
if (openaiChoice.delta.tool_calls === undefined) delete openaiChoice.delta.tool_calls; | |
if (Object.keys(openaiChoice.delta).length === 0 && openaiChoice.finish_reason === undefined) { | |
delete openaiChoice.delta; | |
} | |
return openaiChoice; | |
}), | |
}; | |
return openaiChunk; | |
} | |
// 处理/v1/chat/completions端点 | |
async function chatCompletionsHandler(req, res) { | |
let body = ''; | |
req.on('data', chunk => { | |
body += chunk.toString(); | |
}); | |
req.on('end', async () => { | |
try { | |
// 检查授权头 | |
if (!req.headers.authorization) { | |
res.writeHead(401, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify({ error: "Authorization header is required." })); | |
return; | |
} | |
const openaiPayload = JSON.parse(body); | |
// 1. 为此会话创建一个新的Agent | |
const agentId = uuidv4(); | |
// 检查是否有system消息,如果有,将其添加到agent的描述中 | |
let systemPrompt = ""; | |
if (openaiPayload.messages && Array.isArray(openaiPayload.messages)) { | |
const systemMessage = openaiPayload.messages.find(msg => msg.role === 'system'); | |
if (systemMessage) { | |
systemPrompt = systemMessage.content; | |
} | |
} | |
const createAgentPayload = { | |
name: `temp-agent-${agentId}`, | |
about: "Temporary agent created for a chat session.", | |
system_prompt: systemPrompt || undefined // 如果有system消息,添加到agent配置中 | |
}; | |
const createAgentResponse = await fetch(`${JULEP_API_BASE_URL}/agents/${agentId}`, { | |
method: "POST", | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': req.headers.authorization | |
}, | |
body: JSON.stringify(createAgentPayload), | |
}); | |
if (!createAgentResponse.ok) { | |
console.error("Failed to create agent:", await createAgentResponse.text()); | |
res.writeHead(createAgentResponse.status, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify({ error: "Failed to initialize chat session (agent creation failed)." })); | |
return; | |
} | |
// 2. 使用创建的Agent创建一个新的Session | |
const sessionId = uuidv4(); | |
const createSessionPayload = { | |
agent: agentId, | |
}; | |
const createSessionResponse = await fetch(`${JULEP_API_BASE_URL}/sessions/${sessionId}`, { | |
method: "POST", | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': req.headers.authorization | |
}, | |
body: JSON.stringify(createSessionPayload), | |
}); | |
if (!createSessionResponse.ok) { | |
console.error("Failed to create session:", await createSessionResponse.text()); | |
res.writeHead(createSessionResponse.status, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify({ error: "Failed to initialize chat session (session creation failed)." })); | |
return; | |
} | |
// 3. 使用新的session ID发起聊天 | |
const julepPayload = convertOpenaiToJulep(openaiPayload); | |
const julepUrl = `${JULEP_API_BASE_URL}/sessions/${sessionId}/chat`; | |
const julepResponse = await fetch(julepUrl, { | |
method: "POST", | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': req.headers.authorization | |
}, | |
body: JSON.stringify(julepPayload), | |
}); | |
// 处理流式响应 | |
if (openaiPayload.stream && julepResponse.headers.get('content-type')?.includes('text/event-stream')) { | |
// 设置正确的响应头 | |
res.writeHead(200, { | |
'Content-Type': 'text/event-stream', | |
'Cache-Control': 'no-cache', | |
'Connection': 'keep-alive', | |
'Transfer-Encoding': 'chunked' | |
}); | |
// 使用更可靠的方式处理流 | |
const stream = julepResponse.body; | |
// 创建一个TextDecoder来处理二进制数据 | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
// 处理数据块 | |
stream.on('data', (chunk) => { | |
// 将二进制数据转换为文本 | |
const textChunk = decoder.decode(chunk, { stream: true }); | |
buffer += textChunk; | |
// 处理完整的SSE事件 | |
const lines = buffer.split('\n'); | |
// 保留最后一个可能不完整的行 | |
buffer = lines.pop() || ''; | |
for (const line of lines) { | |
if (line.startsWith('data:')) { | |
const data = line.substring(5).trim(); | |
// 处理结束标记 | |
if (data === '[DONE]') { | |
res.write('data: [DONE]\n\n'); | |
continue; | |
} | |
try { | |
// 解析Julep的JSON响应 | |
const julepChunk = JSON.parse(data); | |
// 转换为OpenAI格式 | |
const openaiChunk = convertJulepChunkToOpenai(julepChunk, openaiPayload.model || 'julep-model', sessionId); | |
// 发送给客户端 | |
res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`); | |
} catch (parseError) { | |
console.error("Error parsing Julep stream chunk:", parseError, "Raw data:", data); | |
// 可选:发送错误信息给客户端 | |
// res.write(`data: ${JSON.stringify({ error: "Failed to parse response" })}\n\n`); | |
} | |
} else if (line.trim() && !line.startsWith(':')) { | |
// 记录非注释的未知行类型 | |
console.warn("Received unexpected stream line:", line); | |
} | |
} | |
// 确保数据立即发送 | |
res.flushHeaders(); | |
}); | |
// 处理流结束 | |
stream.on('end', () => { | |
// 确保发送[DONE]标记(如果Julep没有发送) | |
if (buffer.trim()) { | |
// 处理缓冲区中剩余的数据 | |
if (buffer.startsWith('data:')) { | |
const data = buffer.substring(5).trim(); | |
if (data && data !== '[DONE]') { | |
try { | |
const julepChunk = JSON.parse(data); | |
const openaiChunk = convertJulepChunkToOpenai(julepChunk, openaiPayload.model || 'julep-model', sessionId); | |
res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`); | |
} catch (e) { | |
console.error("Error parsing final chunk:", e); | |
} | |
} | |
} | |
} | |
// 发送最终的[DONE]标记 | |
res.write('data: [DONE]\n\n'); | |
res.end(); | |
console.log(`Stream completed for session ${sessionId}`); | |
}); | |
// 处理错误 | |
stream.on('error', (error) => { | |
console.error("Stream error:", error); | |
// 尝试发送错误信息给客户端 | |
try { | |
res.write(`data: ${JSON.stringify({ error: "Stream error occurred" })}\n\n`); | |
res.write('data: [DONE]\n\n'); | |
} catch (e) { | |
// 可能客户端已断开连接 | |
} | |
res.end(); | |
}); | |
// 处理客户端断开连接 | |
req.on('close', () => { | |
console.log(`Client disconnected for session ${sessionId}`); | |
// 可以选择在这里清理资源 | |
}); | |
} else { | |
// 处理非流式响应 | |
const julepData = await julepResponse.json(); | |
const openaiResponse = convertJulepToOpenai(julepData, openaiPayload.model || 'julep-model', sessionId); | |
res.writeHead(julepResponse.status, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify(openaiResponse)); | |
} | |
} catch (error) { | |
console.error("Error processing request:", error); | |
res.writeHead(500, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify({ error: "Internal Server Error", details: error.message })); | |
} | |
}); | |
} | |
// 创建HTTP服务器 | |
const server = http.createServer((req, res) => { | |
const url = new URL(req.url, `http://${req.headers.host}`); | |
// 处理CORS预检请求 | |
if (req.method === 'OPTIONS') { | |
res.writeHead(204, { | |
'Access-Control-Allow-Origin': '*', | |
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', | |
'Access-Control-Allow-Headers': 'Content-Type, Authorization', | |
'Access-Control-Max-Age': '86400' | |
}); | |
res.end(); | |
return; | |
} | |
// 为所有响应添加CORS头 | |
res.setHeader('Access-Control-Allow-Origin', '*'); | |
// 处理/v1/models端点 | |
if (url.pathname === '/v1/models' && req.method === 'GET') { | |
listModelsHandler(res); | |
} | |
// 处理/v1/chat/completions端点 | |
else if (url.pathname === '/v1/chat/completions' && req.method === 'POST') { | |
chatCompletionsHandler(req, res); | |
} | |
// 处理根路径,返回简单的HTML页面 | |
else if (url.pathname === '/' && req.method === 'GET') { | |
res.writeHead(200, { 'Content-Type': 'text/html' }); | |
res.end(` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Julep API Proxy</title> | |
<style> | |
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } | |
h1 { color: #333; } | |
pre { background: #f4f4f4; padding: 10px; border-radius: 5px; } | |
</style> | |
</head> | |
<body> | |
<h1>Julep API Proxy</h1> | |
<p>This server proxies requests to the Julep API using OpenAI-compatible endpoints:</p> | |
<ul> | |
<li><code>GET /v1/models</code> - List available models</li> | |
<li><code>POST /v1/chat/completions</code> - Create a chat completion</li> | |
</ul> | |
<p>Server is running on port ${PORT}</p> | |
</body> | |
</html> | |
`); | |
} | |
// 返回404 | |
else { | |
res.writeHead(404, { 'Content-Type': 'application/json' }); | |
res.end(JSON.stringify({ error: "Not Found" })); | |
} | |
}); | |
// 启动服务器 | |
server.listen(PORT, () => { | |
console.log(`Server running on port ${PORT}`); | |
}); | |