Spaces:
Paused
Paused
| const express = require('express'); | |
| const router = express.Router(); | |
| const { fetch, ProxyAgent, Agent } = require('undici'); | |
| const { v4: uuidv4, v5: uuidv5 } = require('uuid'); | |
| const config = require('../config/config'); | |
| const $root = require('../proto/message.js'); | |
| const { generateCursorBody, chunkToUtf8String, generateHashed64Hex, generateCursorChecksum } = require('../utils/utils.js'); | |
| router.get("/models", async (req, res) => { | |
| try{ | |
| let bearerToken = req.headers.authorization?.replace('Bearer ', ''); | |
| let authToken = bearerToken.split(',').map((key) => key.trim())[0]; | |
| if (authToken && authToken.includes('%3A%3A')) { | |
| authToken = authToken.split('%3A%3A')[1]; | |
| } | |
| else if (authToken && authToken.includes('::')) { | |
| authToken = authToken.split('::')[1]; | |
| } | |
| const cursorChecksum = req.headers['x-cursor-checksum'] | |
| ?? generateCursorChecksum(authToken.trim()); | |
| const cursorClientVersion = "0.48.7" | |
| const availableModelsResponse = await fetch("https://api2.cursor.sh/aiserver.v1.AiService/AvailableModels", { | |
| method: 'POST', | |
| headers: { | |
| 'accept-encoding': 'gzip', | |
| 'authorization': `Bearer ${authToken}`, | |
| 'connect-protocol-version': '1', | |
| 'content-type': 'application/proto', | |
| 'user-agent': 'connect-es/1.6.1', | |
| 'x-cursor-checksum': cursorChecksum, | |
| 'x-cursor-client-version': cursorClientVersion, | |
| 'x-cursor-config-version': uuidv4(), | |
| 'x-cursor-timezone': 'Asia/Shanghai', | |
| 'x-ghost-mode': 'true', | |
| 'Host': 'api2.cursor.sh', | |
| }, | |
| }) | |
| const data = await availableModelsResponse.arrayBuffer(); | |
| const buffer = Buffer.from(data); | |
| try{ | |
| const models = $root.AvailableModelsResponse.decode(buffer).models; | |
| return res.json({ | |
| object: "list", | |
| data: models.map(model => ({ | |
| id: model.name, | |
| created: Date.now(), | |
| object: 'model', | |
| owned_by: 'cursor' | |
| })) | |
| }) | |
| } catch (error) { | |
| const text = buffer.toString('utf-8'); | |
| throw new Error(text); | |
| } | |
| } | |
| catch (error) { | |
| console.error(error); | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| }); | |
| } | |
| }) | |
| router.post('/chat/completions', async (req, res) => { | |
| try { | |
| const { model, messages, stream = false } = req.body; | |
| let bearerToken = req.headers.authorization?.replace('Bearer ', ''); | |
| const keys = bearerToken.split(',').map((key) => key.trim()); | |
| // Randomly select one key to use | |
| let authToken = keys[Math.floor(Math.random() * keys.length)] | |
| if (!messages || !Array.isArray(messages) || messages.length === 0 || !authToken) { | |
| return res.status(400).json({ | |
| error: 'Invalid request. Messages should be a non-empty array and authorization is required', | |
| }); | |
| } | |
| if (authToken && authToken.includes('%3A%3A')) { | |
| authToken = authToken.split('%3A%3A')[1]; | |
| } | |
| else if (authToken && authToken.includes('::')) { | |
| authToken = authToken.split('::')[1]; | |
| } | |
| const cursorChecksum = req.headers['x-cursor-checksum'] | |
| ?? generateCursorChecksum(authToken.trim()); | |
| const sessionid = uuidv5(authToken, uuidv5.DNS); | |
| const clientKey = generateHashed64Hex(authToken) | |
| const cursorClientVersion = "0.48.7" | |
| const cursorConfigVersion = uuidv4(); | |
| // Request the AvailableModels before StreamChat. | |
| const availableModelsResponse = fetch("https://api2.cursor.sh/aiserver.v1.AiService/AvailableModels", { | |
| method: 'POST', | |
| headers: { | |
| 'accept-encoding': 'gzip', | |
| 'authorization': `Bearer ${authToken}`, | |
| 'connect-protocol-version': '1', | |
| 'content-type': 'application/proto', | |
| 'user-agent': 'connect-es/1.6.1', | |
| 'x-amzn-trace-id': `Root=${uuidv4()}`, | |
| 'x-client-key': clientKey, | |
| 'x-cursor-checksum': cursorChecksum, | |
| 'x-cursor-client-version': cursorClientVersion, | |
| 'x-cursor-config-version': cursorConfigVersion, | |
| 'x-cursor-timezone': 'Asia/Shanghai', | |
| 'x-ghost-mode': 'true', | |
| "x-request-id": uuidv4(), | |
| "x-session-id": sessionid, | |
| 'Host': 'api2.cursor.sh', | |
| }, | |
| }) | |
| const cursorBody = generateCursorBody(messages, model); | |
| const dispatcher = config.proxy.enabled | |
| ? new ProxyAgent(config.proxy.url, { allowH2: true }) | |
| : new Agent({ allowH2: true }); | |
| const response = await fetch('https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools', { | |
| method: 'POST', | |
| headers: { | |
| 'authorization': `Bearer ${authToken}`, | |
| 'connect-accept-encoding': 'gzip', | |
| 'connect-content-encoding': 'gzip', | |
| 'connect-protocol-version': '1', | |
| 'content-type': 'application/connect+proto', | |
| 'user-agent': 'connect-es/1.6.1', | |
| 'x-amzn-trace-id': `Root=${uuidv4()}`, | |
| 'x-client-key': clientKey, | |
| 'x-cursor-checksum': cursorChecksum, | |
| 'x-cursor-client-version': cursorClientVersion, | |
| 'x-cursor-config-version': cursorConfigVersion, | |
| 'x-cursor-timezone': 'Asia/Shanghai', | |
| 'x-ghost-mode': 'true', | |
| 'x-request-id': uuidv4(), | |
| 'x-session-id': sessionid, | |
| 'Host': 'api2.cursor.sh' | |
| }, | |
| body: cursorBody, | |
| dispatcher: dispatcher, | |
| timeout: { | |
| connect: 5000, | |
| read: 30000 | |
| } | |
| }); | |
| if (response.status !== 200) { | |
| return res.status(response.status).json({ | |
| error: response.statusText | |
| }); | |
| } | |
| if (stream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| const responseId = `chatcmpl-${uuidv4()}`; | |
| try { | |
| let thinkingStart = "<thinking>"; | |
| let thinkingEnd = "</thinking>"; | |
| for await (const chunk of response.body) { | |
| const { thinking, text } = chunkToUtf8String(chunk); | |
| let content = "" | |
| if (thinkingStart !== "" && thinking.length > 0 ){ | |
| content += thinkingStart + "\n" | |
| thinkingStart = "" | |
| } | |
| content += thinking | |
| if (thinkingEnd !== "" && thinking.length === 0 && text.length !== 0 && thinkingStart === "") { | |
| content += "\n" + thinkingEnd + "\n" | |
| thinkingEnd = "" | |
| } | |
| content += text | |
| if (content.length > 0) { | |
| res.write( | |
| `data: ${JSON.stringify({ | |
| id: responseId, | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: model, | |
| choices: [{ | |
| index: 0, | |
| delta: { | |
| content: content, | |
| }, | |
| }], | |
| })}\n\n` | |
| ); | |
| } | |
| } | |
| } catch (streamError) { | |
| console.error('Stream error:', streamError); | |
| if (streamError.name === 'TimeoutError') { | |
| res.write(`data: ${JSON.stringify({ error: 'Server response timeout' })}\n\n`); | |
| } else { | |
| res.write(`data: ${JSON.stringify({ error: 'Stream processing error' })}\n\n`); | |
| } | |
| } finally { | |
| res.write('data: [DONE]\n\n'); | |
| res.end(); | |
| } | |
| } else { | |
| // Non-streaming response | |
| try { | |
| let thinkingStart = "<thinking>"; | |
| let thinkingEnd = "</thinking>"; | |
| let content = ''; | |
| for await (const chunk of response.body) { | |
| const { thinking, text } = chunkToUtf8String(chunk); | |
| if (thinkingStart !== "" && thinking.length > 0 ){ | |
| content += thinkingStart + "\n" | |
| thinkingStart = "" | |
| } | |
| content += thinking | |
| if (thinkingEnd !== "" && thinking.length === 0 && text.length !== 0 && thinkingStart === "") { | |
| content += "\n" + thinkingEnd + "\n" | |
| thinkingEnd = "" | |
| } | |
| content += text | |
| } | |
| return res.json({ | |
| id: `chatcmpl-${uuidv4()}`, | |
| object: 'chat.completion', | |
| created: Math.floor(Date.now() / 1000), | |
| model: model, | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: 'assistant', | |
| content: content, | |
| }, | |
| finish_reason: 'stop', | |
| }, | |
| ], | |
| usage: { | |
| prompt_tokens: 0, | |
| completion_tokens: 0, | |
| total_tokens: 0, | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error('Non-stream error:', error); | |
| if (error.name === 'TimeoutError') { | |
| return res.status(408).json({ error: 'Server response timeout' }); | |
| } | |
| throw error; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| if (!res.headersSent) { | |
| const errorMessage = { | |
| error: error.name === 'TimeoutError' ? 'Request timeout' : 'Internal server error' | |
| }; | |
| if (req.body.stream) { | |
| res.write(`data: ${JSON.stringify(errorMessage)}\n\n`); | |
| return res.end(); | |
| } else { | |
| return res.status(error.name === 'TimeoutError' ? 408 : 500).json(errorMessage); | |
| } | |
| } | |
| } | |
| }); | |
| module.exports = router; | |