Julep-API / app.js
isididiidid's picture
Create app.js
ea14b15 verified
raw
history blame
16.3 kB
// 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}`);
});