|
const express = require('express'); |
|
const morgan = require('morgan'); |
|
const { createProxyMiddleware } = require('http-proxy-middleware'); |
|
const url = require('url'); |
|
const app = express(); |
|
|
|
app.use(morgan('dev')); |
|
|
|
|
|
const proxyUrl = process.env.PROXY || ''; |
|
console.log(`Proxy configuration: ${proxyUrl ? 'Configured' : 'Not configured'}`); |
|
console.log(`Raw proxy URL: ${proxyUrl}`); |
|
|
|
|
|
let proxyConfig = null; |
|
if (proxyUrl) { |
|
try { |
|
const parsedUrl = url.parse(proxyUrl); |
|
proxyConfig = { |
|
host: parsedUrl.hostname, |
|
port: parsedUrl.port || 80, |
|
auth: parsedUrl.auth ? { |
|
username: parsedUrl.auth.split(':')[0], |
|
password: parsedUrl.auth.split(':')[1] |
|
} : undefined |
|
}; |
|
|
|
|
|
console.log('Using proxy with EXACT credentials:', JSON.stringify(proxyConfig)); |
|
|
|
if (proxyConfig.auth) { |
|
console.log('EXACT AUTH DETAILS:'); |
|
console.log('Username:', proxyConfig.auth.username); |
|
console.log('Password:', proxyConfig.auth.password); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Failed to parse proxy URL:', error.message); |
|
} |
|
} |
|
|
|
|
|
app.get('/hf/v1/models', (req, res) => { |
|
const models = { |
|
"object": "list", |
|
"data": [ |
|
{ |
|
"id": "claude-3.5-sonnet", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-4", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-4o", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3-opus", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-3.5-turbo", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-4-turbo-2024-04-09", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-4o-128k", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gemini-1.5-flash-500k", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3-haiku-200k", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3-5-sonnet-200k", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3-5-sonnet-20241022", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gpt-4o-mini", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "o1-mini", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "o1-preview", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "o1", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3.5-haiku", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gemini-exp-1206", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gemini-2.0-flash-thinking-exp", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "gemini-2.0-flash-exp", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "deepseek-v3", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "deepseek-r1", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
|
|
{ |
|
"id": "claude-3.7-sonnet", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
}, |
|
{ |
|
"id": "claude-3.7-sonnet-thinking", |
|
"object": "model", |
|
"created": 1706745938, |
|
"owned_by": "cursor" |
|
} |
|
] |
|
}; |
|
res.json(models); |
|
}); |
|
|
|
|
|
app.use('/hf/v1/chat/completions', createProxyMiddleware({ |
|
target: 'http://localhost:3010/v1/chat/completions', |
|
changeOrigin: true, |
|
|
|
proxy: proxyConfig, |
|
|
|
onError: (err, req, res) => { |
|
console.error('Proxy error:', err); |
|
res.status(500).send('Proxy error occurred: ' + err.message); |
|
}, |
|
onProxyReq: (proxyReq, req, res) => { |
|
console.log(`Proxying request to chat completions ${proxyConfig ? 'using proxy' : 'directly'}`); |
|
}, |
|
onProxyRes: (proxyRes, req, res) => { |
|
console.log(`Received response with status: ${proxyRes.statusCode}`); |
|
} |
|
})); |
|
|
|
app.get('/', (req, res) => { |
|
const htmlContent = ` |
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Cursor To OpenAI</title> |
|
<style> |
|
:root { |
|
--bg-primary: #ffffff; |
|
--bg-secondary: #f9f9f9; |
|
--bg-tertiary: #f0f0f0; |
|
--text-primary: #333333; |
|
--text-secondary: #666666; |
|
--border-color: #e0e0e0; |
|
--accent-color: #5D5CDE; |
|
--success-bg: #e1f5e1; |
|
--success-border: #c3e6cb; |
|
--warning-bg: #fff3cd; |
|
--warning-border: #ffeeba; |
|
--card-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|
} |
|
|
|
[data-theme="dark"] { |
|
--bg-primary: #181818; |
|
--bg-secondary: #222222; |
|
--bg-tertiary: #2a2a2a; |
|
--text-primary: #e0e0e0; |
|
--text-secondary: #b0b0b0; |
|
--border-color: #444444; |
|
--accent-color: #7D7CED; |
|
--success-bg: #1e3a1e; |
|
--success-border: #2a5a2a; |
|
--warning-bg: #3a3018; |
|
--warning-border: #5a4820; |
|
--card-shadow: 0 2px 10px rgba(0,0,0,0.3); |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
line-height: 1.6; |
|
background-color: var(--bg-primary); |
|
color: var(--text-primary); |
|
transition: background-color 0.3s, color 0.3s; |
|
} |
|
|
|
.container { |
|
background: var(--bg-secondary); |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: var(--card-shadow); |
|
} |
|
|
|
.header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.theme-toggle { |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.switch { |
|
position: relative; |
|
display: inline-block; |
|
width: 50px; |
|
height: 24px; |
|
margin-left: 10px; |
|
} |
|
|
|
.switch input { |
|
opacity: 0; |
|
width: 0; |
|
height: 0; |
|
} |
|
|
|
.slider { |
|
position: absolute; |
|
cursor: pointer; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background-color: var(--bg-tertiary); |
|
transition: .4s; |
|
border-radius: 24px; |
|
} |
|
|
|
.slider:before { |
|
position: absolute; |
|
content: ""; |
|
height: 16px; |
|
width: 16px; |
|
left: 4px; |
|
bottom: 4px; |
|
background-color: var(--accent-color); |
|
transition: .4s; |
|
border-radius: 50%; |
|
} |
|
|
|
input:checked + .slider:before { |
|
transform: translateX(26px); |
|
} |
|
|
|
.info-item { |
|
margin-bottom: 10px; |
|
} |
|
|
|
.status { |
|
background: ${proxyConfig ? 'var(--success-bg)' : 'var(--warning-bg)'}; |
|
padding: 10px; |
|
border-radius: 5px; |
|
margin-top: 20px; |
|
border: 1px solid ${proxyConfig ? 'var(--success-border)' : 'var(--warning-border)'}; |
|
} |
|
|
|
.models-container { |
|
margin-top: 20px; |
|
border-top: 1px solid var(--border-color); |
|
padding-top: 20px; |
|
} |
|
|
|
.model-list { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
|
gap: 10px; |
|
margin-top: 15px; |
|
} |
|
|
|
.model-item { |
|
background: var(--bg-tertiary); |
|
padding: 8px 12px; |
|
border-radius: 4px; |
|
font-size: 0.9em; |
|
} |
|
|
|
.guide-section { |
|
margin-top: 20px; |
|
border-top: 1px solid var(--border-color); |
|
padding-top: 20px; |
|
} |
|
|
|
.guide-step { |
|
margin-bottom: 15px; |
|
padding: 10px; |
|
background: var(--bg-secondary); |
|
border-radius: 5px; |
|
border-left: 3px solid var(--accent-color); |
|
} |
|
|
|
.code-example { |
|
background-color: var(--bg-tertiary); |
|
padding: 10px; |
|
border-radius: 5px; |
|
overflow-x: auto; |
|
font-family: monospace; |
|
margin: 10px 0; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>Cursor To OpenAI Server</h1> |
|
<div class="theme-toggle"> |
|
Dark Mode |
|
<label class="switch"> |
|
<input type="checkbox" id="theme-toggle"> |
|
<span class="slider"></span> |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<div class="info-item"> |
|
<strong>Chat Source:</strong> Custom (OpenAI Compatible) |
|
</div> |
|
<div class="info-item"> |
|
<strong>Custom Endpoint (Base URL):</strong> <span id="endpoint-url"></span> |
|
</div> |
|
<div class="info-item"> |
|
<strong>Custom API Key:</strong> [Cursor Cookie in format user_...] |
|
</div> |
|
|
|
<div class="status"> |
|
<strong>Proxy Status:</strong> ${proxyConfig ? 'Enabled' : 'Disabled'} |
|
${proxyConfig ? `<p>Proxy Server: ${proxyConfig.host}:${proxyConfig.port}</p>` : ''} |
|
</div> |
|
|
|
<div class="models-container"> |
|
<h3>Supported Models</h3> |
|
<div id="model-list" class="model-list">Loading...</div> |
|
</div> |
|
|
|
<div class="guide-section"> |
|
<h3>How to Connect to Cursor API</h3> |
|
|
|
<div class="guide-step"> |
|
<h4>Step 1: Authentication</h4> |
|
<p>To authenticate with the Cursor API, you need to obtain your Cursor cookie:</p> |
|
<ol> |
|
<li>Log in to <a href="https://cursor.so" target="_blank">Cursor.so</a></li> |
|
<li>Open browser developer tools (F12)</li> |
|
<li>Go to Application tab → Cookies</li> |
|
<li>Find the cookie that starts with "user_..."</li> |
|
<li>Use this value as your API key in the Authorization header</li> |
|
</ol> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>Step 2: Set up your API client</h4> |
|
<p>Configure your API client with the following settings:</p> |
|
<ul> |
|
<li>Base URL: <span id="endpoint-url-guide"></span></li> |
|
<li>Headers: <code>Authorization: Bearer your_cursor_cookie</code></li> |
|
<li>Content-Type: application/json</li> |
|
</ul> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>Step 3: Make API requests</h4> |
|
<p>Example request to generate a completion:</p> |
|
<div class="code-example"> |
|
fetch('<span id="endpoint-url-code"></span>/chat/completions', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': 'Bearer your_cursor_cookie' |
|
}, |
|
body: JSON.stringify({ |
|
model: 'claude-3.7-sonnet', |
|
messages: [ |
|
{ role: 'user', content: 'Hello, who are you?' } |
|
], |
|
temperature: 0.7 |
|
}) |
|
}) |
|
.then(response => response.json()) |
|
.then(data => console.log(data)); |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// Theme toggling |
|
const themeToggle = document.getElementById('theme-toggle'); |
|
|
|
// Check system preference |
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { |
|
document.documentElement.setAttribute('data-theme', 'dark'); |
|
themeToggle.checked = true; |
|
} |
|
|
|
// Toggle theme when switch is clicked |
|
themeToggle.addEventListener('change', function() { |
|
if (this.checked) { |
|
document.documentElement.setAttribute('data-theme', 'dark'); |
|
} else { |
|
document.documentElement.removeAttribute('data-theme'); |
|
} |
|
}); |
|
|
|
// Listen for system theme changes |
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { |
|
if (event.matches) { |
|
document.documentElement.setAttribute('data-theme', 'dark'); |
|
themeToggle.checked = true; |
|
} else { |
|
document.documentElement.removeAttribute('data-theme'); |
|
themeToggle.checked = false; |
|
} |
|
}); |
|
|
|
// Get and display endpoint URL |
|
const url = new URL(window.location.href); |
|
const link = url.protocol + '//' + url.host + '/hf/v1'; |
|
document.getElementById('endpoint-url').textContent = link; |
|
|
|
// Also update the guide URLs |
|
const guideUrlElements = document.querySelectorAll('#endpoint-url-guide, #endpoint-url-code'); |
|
guideUrlElements.forEach(el => { |
|
el.textContent = link; |
|
}); |
|
|
|
// Load models list |
|
fetch('/hf/v1/models') |
|
.then(response => response.json()) |
|
.then(data => { |
|
const modelListEl = document.getElementById('model-list'); |
|
modelListEl.innerHTML = ''; |
|
|
|
data.data.forEach(model => { |
|
const modelEl = document.createElement('div'); |
|
modelEl.className = 'model-item'; |
|
modelEl.textContent = model.id; |
|
modelListEl.appendChild(modelEl); |
|
}); |
|
}) |
|
.catch(err => { |
|
document.getElementById('model-list').textContent = 'Failed to load models: ' + err.message; |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
`; |
|
res.send(htmlContent); |
|
}); |
|
|
|
const port = process.env.HF_PORT || 7860; |
|
app.listen(port, () => { |
|
console.log(`HF Proxy server is running at PORT: ${port}`); |
|
console.log(`Proxy status: ${proxyConfig ? 'Enabled' : 'Disabled'}`); |
|
}); |