Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const fetch = require('node-fetch'); | |
| const app = express(); | |
| const PORT = process.env.PORT || 4777; | |
| const A2BASE_API_URL = 'https://a2abase.ai/api'; | |
| app.use(express.json()); | |
| // 模型列表 | |
| const MODELS_DATA = { | |
| "models": [ | |
| { | |
| "id": "gemini/gemini-ultra", | |
| "display_name": "Gemini Ultra", | |
| "short_name": "Gemini Ultra", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 5.0, | |
| "output_cost_per_million_tokens": 20.0, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output", "thinking"], | |
| "recommended": false, | |
| "priority": 106 | |
| }, | |
| { | |
| "id": "gemini/gemini-3-deep-think-exp", | |
| "display_name": "Gemini 3 Deep Think", | |
| "short_name": "Gemini 3 Deep Think", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 2.5, | |
| "output_cost_per_million_tokens": 10.0, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "thinking"], | |
| "recommended": false, | |
| "priority": 105 | |
| }, | |
| { | |
| "id": "gemini/gemini-3-flash-preview", | |
| "display_name": "Gemini 3 Flash", | |
| "short_name": "Gemini 3 Flash", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.3, | |
| "output_cost_per_million_tokens": 1.5, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 104 | |
| }, | |
| { | |
| "id": "gemini/gemini-3-pro-preview", | |
| "display_name": "Gemini 3", | |
| "short_name": "Gemini 3", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.36, | |
| "output_cost_per_million_tokens": 3.0, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 103 | |
| }, | |
| { | |
| "id": "anthropic/claude-sonnet-4-5-20250929", | |
| "display_name": "Claude Sonnet 4.5", | |
| "short_name": "Claude Sonnet 4.5", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 3.6, | |
| "output_cost_per_million_tokens": 18.0, | |
| "context_window": 200000, | |
| "capabilities": ["chat", "function_calling", "vision", "thinking", "structured_output"], | |
| "recommended": false, | |
| "priority": 102 | |
| }, | |
| { | |
| "id": "gemini/gemini-2.5-flash", | |
| "display_name": "Gemini 2.5 Flash", | |
| "short_name": "Gemini 2.5 Flash", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.36, | |
| "output_cost_per_million_tokens": 3.0, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 102 | |
| }, | |
| { | |
| "id": "anthropic/claude-haiku-4-5-20251001", | |
| "display_name": "Claude Haiku 4.5", | |
| "short_name": "Claude Haiku 4.5", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 1.0, | |
| "output_cost_per_million_tokens": 5.0, | |
| "context_window": 200000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 101 | |
| }, | |
| { | |
| "id": "gemini/gemini-2.5-pro", | |
| "display_name": "Gemini 2.5 Pro", | |
| "short_name": "Gemini 2.5 Pro", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 1.25, | |
| "output_cost_per_million_tokens": 5.0, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output", "thinking"], | |
| "recommended": false, | |
| "priority": 101 | |
| }, | |
| { | |
| "id": "openrouter/z-ai/glm-4.7", | |
| "display_name": "GLM 4.7", | |
| "short_name": "GLM 4.7", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.5, | |
| "output_cost_per_million_tokens": 2.2, | |
| "context_window": 205000, | |
| "capabilities": ["chat", "function_calling", "code_interpreter", "thinking"], | |
| "recommended": false, | |
| "priority": 100 | |
| }, | |
| { | |
| "id": "openrouter/x-ai/grok-code-fast-1", | |
| "display_name": "Grok Code Fast 1", | |
| "short_name": "Grok Code Fast 1", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.24, | |
| "output_cost_per_million_tokens": 1.8, | |
| "context_window": 256000, | |
| "capabilities": ["chat", "function_calling", "code_interpreter"], | |
| "recommended": false, | |
| "priority": 100 | |
| }, | |
| { | |
| "id": "openrouter/z-ai/glm-4.6", | |
| "display_name": "GLM 4.6", | |
| "short_name": "GLM 4.6", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.48, | |
| "output_cost_per_million_tokens": 2.1, | |
| "context_window": 202752, | |
| "capabilities": ["chat", "function_calling"], | |
| "recommended": false, | |
| "priority": 99 | |
| }, | |
| { | |
| "id": "openrouter/minimax/minimax-m2", | |
| "display_name": "Minimax M2", | |
| "short_name": "Minimax M2", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.306, | |
| "output_cost_per_million_tokens": 1.224, | |
| "context_window": 204800, | |
| "capabilities": ["chat", "function_calling", "vision"], | |
| "recommended": false, | |
| "priority": 98 | |
| }, | |
| { | |
| "id": "openai/gpt-5.2", | |
| "display_name": "GPT-5.2", | |
| "short_name": "GPT-5.2", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 5.0, | |
| "output_cost_per_million_tokens": 15.0, | |
| "context_window": 400000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output", "thinking"], | |
| "recommended": true, | |
| "priority": 97 | |
| }, | |
| { | |
| "id": "openai/gpt-5-mini", | |
| "display_name": "GPT-5 Mini", | |
| "short_name": "GPT-5 Mini", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.3, | |
| "output_cost_per_million_tokens": 2.4, | |
| "context_window": 400000, | |
| "capabilities": ["chat", "function_calling", "structured_output"], | |
| "recommended": false, | |
| "priority": 96 | |
| }, | |
| { | |
| "id": "openrouter/deepseek/deepseek-chat", | |
| "display_name": "DeepSeek Chat", | |
| "short_name": "DeepSeek Chat", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.46, | |
| "output_cost_per_million_tokens": 1.07, | |
| "context_window": 128000, | |
| "capabilities": ["chat", "function_calling"], | |
| "recommended": false, | |
| "priority": 95 | |
| }, | |
| { | |
| "id": "openai/o3-mini", | |
| "display_name": "o3-mini", | |
| "short_name": "o3-mini", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 3.5, | |
| "output_cost_per_million_tokens": 14.0, | |
| "context_window": 128000, | |
| "capabilities": ["chat", "thinking"], | |
| "recommended": false, | |
| "priority": 95 | |
| }, | |
| { | |
| "id": "openai/gpt-4.1-mini", | |
| "display_name": "GPT-4.1 Mini", | |
| "short_name": "GPT-4.1 Mini", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.48, | |
| "output_cost_per_million_tokens": 1.92, | |
| "context_window": 1047576, | |
| "capabilities": ["chat", "function_calling", "structured_output"], | |
| "recommended": false, | |
| "priority": 94 | |
| }, | |
| { | |
| "id": "openrouter/x-ai/grok-4-fast", | |
| "display_name": "Grok 4 Fast", | |
| "short_name": "Grok 4 Fast", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.24, | |
| "output_cost_per_million_tokens": 0.6, | |
| "context_window": 2000000, | |
| "capabilities": ["chat", "function_calling"], | |
| "recommended": false, | |
| "priority": 93 | |
| }, | |
| { | |
| "id": "openai/gpt-4o-2024-11-20", | |
| "display_name": "GPT-4o (Nov 2024)", | |
| "short_name": "GPT-4o (Nov 2024)", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 2.5, | |
| "output_cost_per_million_tokens": 10.0, | |
| "context_window": 128000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 91 | |
| }, | |
| { | |
| "id": "openai/gpt-4o", | |
| "display_name": "GPT-4o", | |
| "short_name": "GPT-4o", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 2.5, | |
| "output_cost_per_million_tokens": 10.0, | |
| "context_window": 128000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 90 | |
| }, | |
| { | |
| "id": "gemini/gemini-2.0-flash-exp", | |
| "display_name": "Gemini 2.0 Flash", | |
| "short_name": "Gemini 2.0 Flash", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.075, | |
| "output_cost_per_million_tokens": 0.3, | |
| "context_window": 1000000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 89 | |
| }, | |
| { | |
| "id": "openai/gpt-4o-mini", | |
| "display_name": "GPT-4o Mini", | |
| "short_name": "GPT-4o Mini", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.18, | |
| "output_cost_per_million_tokens": 0.72, | |
| "context_window": 128000, | |
| "capabilities": ["chat", "function_calling", "vision", "structured_output"], | |
| "recommended": false, | |
| "priority": 88 | |
| }, | |
| { | |
| "id": "gemini/gemini-2.0-flash-lite", | |
| "display_name": "Gemini 2.0 Flash-Lite", | |
| "short_name": "Gemini 2.0 Flash-Lite", | |
| "requires_subscription": false, | |
| "input_cost_per_million_tokens": 0.0375, | |
| "output_cost_per_million_tokens": 0.15, | |
| "context_window": 1000000, | |
| "capabilities": ["chat", "function_calling", "vision"], | |
| "recommended": false, | |
| "priority": 86 | |
| } | |
| ], | |
| "subscription_tier": "free", | |
| "total_models": 23, | |
| "allowed_models_count": 23 | |
| }; | |
| // GET /v1/models - 返回模型列表 | |
| app.get('/v1/models', (req, res) => { | |
| res.json({ | |
| object: 'list', | |
| data: MODELS_DATA.models.map(model => ({ | |
| id: model.id, | |
| object: 'model', | |
| created: Date.now(), | |
| owned_by: 'a2base', | |
| permission: [], | |
| root: model.id, | |
| parent: null | |
| })) | |
| }); | |
| }); | |
| // POST /v1/chat/completions - 处理聊天请求 | |
| app.post('/v1/chat/completions', async (req, res) => { | |
| try { | |
| // 从Authorization header中提取apiKey和agentId | |
| const authHeader = req.headers.authorization; | |
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | |
| return res.status(401).json({ error: { message: 'Missing or invalid Authorization header', type: 'invalid_request_error' } }); | |
| } | |
| const token = authHeader.substring(7); // 去除 'Bearer ' | |
| const [apiKey, agentId] = token.split('|'); | |
| if (!apiKey || !agentId) { | |
| return res.status(401).json({ error: { message: 'Invalid token format. Expected: apiKey|agentId', type: 'invalid_request_error' } }); | |
| } | |
| const { messages, model, stream = false, temperature, max_tokens } = req.body; | |
| if (!messages || !Array.isArray(messages) || messages.length === 0) { | |
| return res.status(400).json({ error: { message: 'Messages array is required', type: 'invalid_request_error' } }); | |
| } | |
| // 步骤1: 创建Thread | |
| const threadResponse = await fetch(`${A2BASE_API_URL}/threads`, { | |
| method: 'POST', | |
| headers: { | |
| 'X-API-Key': apiKey | |
| } | |
| }); | |
| if (!threadResponse.ok) { | |
| const errorText = await threadResponse.text(); | |
| return res.status(threadResponse.status).json({ error: { message: `Failed to create thread: ${errorText}`, type: 'api_error' } }); | |
| } | |
| const threadData = await threadResponse.json(); | |
| const threadId = threadData.thread_id; | |
| // 步骤2: 添加消息到Thread | |
| await fetch(`${A2BASE_API_URL}/threads/${threadId}/messages`, { | |
| method: 'POST', | |
| headers: { | |
| 'X-API-Key': apiKey, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| content: messages.map(msg => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`).join('\n'), | |
| type: 'user', | |
| is_llm_message: true | |
| }) | |
| }); | |
| // 步骤3: 启动Agent执行 | |
| const startResponse = await fetch(`${A2BASE_API_URL}/thread/${threadId}/agent/start`, { | |
| method: 'POST', | |
| headers: { | |
| 'X-API-Key': apiKey, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| agent_id: agentId, | |
| model_name: model || 'gemini/gemini-3-pro-preview', | |
| stream: true | |
| }) | |
| }); | |
| if (!startResponse.ok) { | |
| const errorText = await startResponse.text(); | |
| return res.status(startResponse.status).json({ error: { message: `Failed to start agent: ${errorText}`, type: 'api_error' } }); | |
| } | |
| const startData = await startResponse.json(); | |
| const agentRunId = startData.agent_run_id; | |
| // 步骤4: 获取流式响应 | |
| const streamResponse = await fetch(`${A2BASE_API_URL}/agent-run/${agentRunId}/stream`, { | |
| method: 'GET', | |
| headers: { | |
| 'X-API-Key': apiKey, | |
| 'Accept': 'text/event-stream' | |
| } | |
| }); | |
| if (!streamResponse.ok) { | |
| const errorText = await streamResponse.text(); | |
| return res.status(streamResponse.status).json({ error: { message: `Failed to get stream: ${errorText}`, type: 'api_error' } }); | |
| } | |
| const chatId = `chatcmpl-${Date.now()}`; | |
| const created = Math.floor(Date.now() / 1000); | |
| if (stream) { | |
| // 流式返回 | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| let buffer = ''; | |
| streamResponse.body.on('data', (chunk) => { | |
| // console.log('Received chunk: ', chunk.toString()); | |
| buffer += chunk.toString(); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const jsonStr = line.substring(6); | |
| try { | |
| const data = JSON.parse(jsonStr); | |
| // 处理assistant类型的消息 | |
| if (data.type === 'assistant' && data.content) { | |
| const contentObj = JSON.parse(data.content); | |
| const metaObj = JSON.parse(data.metadata || '{}'); | |
| if (contentObj.content && metaObj.stream_status === 'chunk') { | |
| const openaiChunk = { | |
| id: chatId, | |
| object: 'chat.completion.chunk', | |
| created: created, | |
| model: model || 'gemini/gemini-2.5-pro', | |
| choices: [{ | |
| index: 0, | |
| delta: { | |
| content: contentObj.content | |
| }, | |
| finish_reason: null | |
| }] | |
| }; | |
| res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`); | |
| } | |
| } | |
| // 处理完成状态 | |
| if (data.status === 'completed' || data.type === 'assistant_response_end') { | |
| // const finalChunk = { | |
| // id: chatId, | |
| // object: 'chat.completion.chunk', | |
| // created: created, | |
| // model: model || 'gemini/gemini-2.5-pro', | |
| // choices: [{ | |
| // index: 0, | |
| // delta: {}, | |
| // finish_reason: 'stop' | |
| // }] | |
| // }; | |
| // res.write(`data: ${JSON.stringify(finalChunk)}\n\n`); | |
| res.write('data: [DONE]\n\n'); | |
| res.end(); | |
| } | |
| } catch (e) { | |
| // 忽略解析错误 | |
| } | |
| } | |
| } | |
| }); | |
| streamResponse.body.on('end', () => { | |
| if (!res.writableEnded) { | |
| res.end(); | |
| } | |
| }); | |
| streamResponse.body.on('error', (err) => { | |
| console.error('Stream error:', err); | |
| if (!res.writableEnded) { | |
| res.end(); | |
| } | |
| }); | |
| } else { | |
| // 非流式返回 - 收集所有内容后一次性返回 | |
| let fullContent = ''; | |
| let buffer = ''; | |
| await new Promise((resolve, reject) => { | |
| streamResponse.body.on('data', (chunk) => { | |
| buffer += chunk.toString(); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const jsonStr = line.substring(6); | |
| try { | |
| const data = JSON.parse(jsonStr); | |
| if (data.type === 'assistant' && data.content) { | |
| const contentObj = JSON.parse(data.content); | |
| const metaObj = JSON.parse(data.metadata || '{}'); | |
| if (contentObj.content && metaObj.stream_status === 'chunk') { | |
| fullContent += contentObj.content; | |
| } | |
| } | |
| if (data.status === 'completed' || data.type === 'assistant_response_end') { | |
| resolve(); | |
| } | |
| } catch (e) { | |
| // 忽略解析错误 | |
| } | |
| } | |
| } | |
| }); | |
| streamResponse.body.on('end', resolve); | |
| streamResponse.body.on('error', reject); | |
| }); | |
| // 返回OpenAI格式的响应 | |
| const response = { | |
| id: chatId, | |
| object: 'chat.completion', | |
| created: created, | |
| model: model || 'gemini/gemini-2.5-pro', | |
| choices: [{ | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: fullContent | |
| }, | |
| finish_reason: 'stop' | |
| }], | |
| usage: { | |
| prompt_tokens: 0, | |
| completion_tokens: 0, | |
| total_tokens: 0 | |
| } | |
| }; | |
| res.json(response); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| res.status(500).json({ error: { message: error.message, type: 'internal_error' } }); | |
| } | |
| }); | |
| // 健康检查端点 | |
| app.get('/health', (req, res) => { | |
| res.json({ status: 'ok' }); | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`A2Base to OpenAI API server running on port ${PORT}`); | |
| }); | |