|
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); |
|
} |
|
|
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
background-color: var(--bg-primary); |
|
color: var(--text-primary); |
|
line-height: 1.6; |
|
transition: background-color 0.3s, color 0.3s; |
|
padding: 20px; |
|
max-width: 100vw; |
|
} |
|
|
|
h1, h2, h3, h4 { |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
background: var(--bg-secondary); |
|
border-radius: 10px; |
|
box-shadow: var(--card-shadow); |
|
overflow: hidden; |
|
} |
|
|
|
.header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 15px 20px; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.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); |
|
} |
|
|
|
.dashboard { |
|
display: flex; |
|
flex-wrap: wrap; |
|
padding: 20px; |
|
gap: 20px; |
|
} |
|
|
|
.server-info, .models-section, .guide-section { |
|
flex: 1 1 300px; |
|
background: var(--bg-tertiary); |
|
border-radius: 8px; |
|
padding: 15px; |
|
overflow: hidden; |
|
max-height: 500px; |
|
} |
|
|
|
.server-info { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
} |
|
|
|
.info-item { |
|
padding: 10px; |
|
background: var(--bg-secondary); |
|
border-radius: 6px; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.info-item strong { |
|
display: block; |
|
font-size: 0.8rem; |
|
color: var(--text-secondary); |
|
margin-bottom: 3px; |
|
} |
|
|
|
.info-item span { |
|
font-size: 0.9rem; |
|
word-break: break-all; |
|
} |
|
|
|
.status-badge { |
|
display: inline-block; |
|
padding: 3px 8px; |
|
border-radius: 12px; |
|
font-size: 0.8rem; |
|
font-weight: 500; |
|
} |
|
|
|
.status-enabled { |
|
background: var(--success-bg); |
|
color: var(--success-border); |
|
} |
|
|
|
.status-disabled { |
|
background: var(--warning-bg); |
|
color: var(--warning-border); |
|
} |
|
|
|
.section-header { |
|
margin-bottom: 15px; |
|
font-size: 1rem; |
|
} |
|
|
|
.models-grid { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 5px; |
|
overflow-y: auto; |
|
max-height: 435px; |
|
padding-right: 5px; |
|
} |
|
|
|
.model-tag { |
|
background: var(--bg-secondary); |
|
border: 1px solid var(--border-color); |
|
border-radius: 15px; |
|
padding: 4px 10px; |
|
font-size: 0.8rem; |
|
white-space: nowrap; |
|
} |
|
|
|
.guide-steps { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
overflow-y: auto; |
|
max-height: 435px; |
|
padding-right: 5px; |
|
} |
|
|
|
.guide-step { |
|
background: var(--bg-secondary); |
|
border-radius: 8px; |
|
padding: 10px; |
|
border-left: 3px solid var(--accent-color); |
|
} |
|
|
|
.guide-step h4 { |
|
font-size: 0.9rem; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.guide-step p { |
|
font-size: 0.85rem; |
|
} |
|
|
|
.code-example { |
|
margin-top: 8px; |
|
background: var(--bg-primary); |
|
padding: 8px; |
|
border-radius: 4px; |
|
font-family: monospace; |
|
font-size: 0.75rem; |
|
overflow-x: auto; |
|
white-space: pre; |
|
} |
|
|
|
.tabs { |
|
display: flex; |
|
border-bottom: 1px solid var(--border-color); |
|
margin-bottom: 15px; |
|
} |
|
|
|
.tab { |
|
padding: 8px 15px; |
|
cursor: pointer; |
|
font-size: 0.85rem; |
|
border-bottom: 2px solid transparent; |
|
} |
|
|
|
.tab.active { |
|
color: var(--accent-color); |
|
border-bottom-color: var(--accent-color); |
|
} |
|
|
|
/* Scrollbar styling */ |
|
.models-grid::-webkit-scrollbar, .guide-steps::-webkit-scrollbar { |
|
width: 6px; |
|
} |
|
|
|
.models-grid::-webkit-scrollbar-track, .guide-steps::-webkit-scrollbar-track { |
|
background: var(--bg-tertiary); |
|
} |
|
|
|
.models-grid::-webkit-scrollbar-thumb, .guide-steps::-webkit-scrollbar-thumb { |
|
background-color: var(--border-color); |
|
border-radius: 6px; |
|
} |
|
|
|
/* Responsive adjustments */ |
|
@media (max-width: 768px) { |
|
.dashboard { |
|
flex-direction: column; |
|
} |
|
|
|
.server-info, .models-section, .guide-section { |
|
max-height: none; |
|
} |
|
|
|
.models-grid, .guide-steps { |
|
max-height: 200px; |
|
} |
|
} |
|
</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="dashboard"> |
|
<!-- Server Information --> |
|
<div class="server-info"> |
|
<h3 class="section-header">Server Information</h3> |
|
|
|
<div class="info-item"> |
|
<strong>API Endpoint</strong> |
|
<span id="endpoint-url">Loading...</span> |
|
</div> |
|
|
|
<div class="info-item"> |
|
<strong>Authentication</strong> |
|
<span>Cursor Cookie (user_...)</span> |
|
</div> |
|
|
|
<div class="info-item"> |
|
<strong>Proxy Status</strong> |
|
<span> |
|
<span class="status-badge ${proxyConfig ? 'status-enabled' : 'status-disabled'}"> |
|
${proxyConfig ? 'Enabled' : 'Disabled'} |
|
</span> |
|
${proxyConfig ? `<div style="margin-top:5px">Server: ${proxyConfig.host}:${proxyConfig.port}</div>` : ''} |
|
</span> |
|
</div> |
|
|
|
<div class="info-item"> |
|
<strong>Usage</strong> |
|
<span>Send requests to the endpoint with your Cursor cookie as the Bearer token in the Authorization header.</span> |
|
</div> |
|
</div> |
|
|
|
<!-- Models Section --> |
|
<div class="models-section"> |
|
<h3 class="section-header">Supported Models</h3> |
|
|
|
<div class="tabs"> |
|
<div class="tab active" data-models-tab="all">All Models</div> |
|
<div class="tab" data-models-tab="claude">Claude</div> |
|
<div class="tab" data-models-tab="gpt">GPT</div> |
|
<div class="tab" data-models-tab="other">Other</div> |
|
</div> |
|
|
|
<div id="all-models" class="models-grid tab-content active"> |
|
Loading models... |
|
</div> |
|
|
|
<div id="claude-models" class="models-grid tab-content"> |
|
Loading Claude models... |
|
</div> |
|
|
|
<div id="gpt-models" class="models-grid tab-content"> |
|
Loading GPT models... |
|
</div> |
|
|
|
<div id="other-models" class="models-grid tab-content"> |
|
Loading other models... |
|
</div> |
|
</div> |
|
|
|
<!-- Integration Guide --> |
|
<div class="guide-section"> |
|
<h3 class="section-header">Integration Guide</h3> |
|
|
|
<div class="guide-steps"> |
|
<div class="guide-step"> |
|
<h4>1. Authentication</h4> |
|
<p>Obtain your Cursor cookie that starts with "user_..." from browser cookies after logging in to Cursor.</p> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>2. API Configuration</h4> |
|
<p>Set up your API client with:</p> |
|
<p><strong>Base URL:</strong> <span id="endpoint-guide">Loading...</span></p> |
|
<p><strong>Headers:</strong></p> |
|
<div class="code-example">Content-Type: application/json |
|
Authorization: Bearer user_your_cookie</div> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>3. Making Requests</h4> |
|
<p>Send chat completion requests:</p> |
|
<div class="code-example">POST /chat/completions |
|
{ |
|
"model": "claude-3.7-sonnet", |
|
"messages": [ |
|
{"role": "user", "content": "Hello!"} |
|
], |
|
"temperature": 0.7 |
|
}</div> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>4. Code Example</h4> |
|
<div class="code-example">const response = await fetch('${req.protocol}://${req.get('host')}/hf/v1/chat/completions', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': 'Bearer user_your_cookie' |
|
}, |
|
body: JSON.stringify({ |
|
model: 'claude-3.7-sonnet', |
|
messages: [ |
|
{ role: 'user', content: 'Hello' } |
|
], |
|
temperature: 0.7 |
|
}) |
|
}); |
|
|
|
const data = await response.json();</div> |
|
</div> |
|
</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'; |
|
|
|
const endpointElements = document.querySelectorAll('#endpoint-url, #endpoint-guide'); |
|
for (let i = 0; i < endpointElements.length; i++) { |
|
endpointElements[i].textContent = link; |
|
} |
|
|
|
// Model tabs |
|
const modelTabs = document.querySelectorAll('[data-models-tab]'); |
|
const modelContents = document.querySelectorAll('.models-grid.tab-content'); |
|
|
|
for (let i = 0; i < modelTabs.length; i++) { |
|
modelTabs[i].addEventListener('click', function() { |
|
// Remove active class from all tabs |
|
for (let j = 0; j < modelTabs.length; j++) { |
|
modelTabs[j].classList.remove('active'); |
|
} |
|
|
|
// Hide all tab contents |
|
for (let j = 0; j < modelContents.length; j++) { |
|
modelContents[j].classList.remove('active'); |
|
} |
|
|
|
// Add active class to clicked tab |
|
this.classList.add('active'); |
|
|
|
// Show corresponding content |
|
const tabId = this.getAttribute('data-models-tab') + '-models'; |
|
const tabContent = document.getElementById(tabId); |
|
if (tabContent) { |
|
tabContent.classList.add('active'); |
|
} else if (tabId === 'all-models') { |
|
document.getElementById('all-models').classList.add('active'); |
|
} |
|
}); |
|
} |
|
|
|
// Load models list |
|
fetch('/hf/v1/models') |
|
.then(response => response.json()) |
|
.then(data => { |
|
// Sort models alphabetically |
|
data.data.sort(function(a, b) { |
|
return a.id.localeCompare(b.id); |
|
}); |
|
|
|
const allModelsEl = document.getElementById('all-models'); |
|
const claudeModelsEl = document.getElementById('claude-models'); |
|
const gptModelsEl = document.getElementById('gpt-models'); |
|
const otherModelsEl = document.getElementById('other-models'); |
|
|
|
// Clear loading messages |
|
allModelsEl.innerHTML = ''; |
|
claudeModelsEl.innerHTML = ''; |
|
gptModelsEl.innerHTML = ''; |
|
otherModelsEl.innerHTML = ''; |
|
|
|
data.data.forEach(function(model) { |
|
// Create model tag |
|
const modelEl = document.createElement('div'); |
|
modelEl.className = 'model-tag'; |
|
modelEl.textContent = model.id; |
|
|
|
// Add to all models |
|
allModelsEl.appendChild(modelEl.cloneNode(true)); |
|
|
|
// Add to specific category |
|
if (model.id.includes('claude')) { |
|
claudeModelsEl.appendChild(modelEl.cloneNode(true)); |
|
} else if (model.id.includes('gpt')) { |
|
gptModelsEl.appendChild(modelEl.cloneNode(true)); |
|
} else { |
|
otherModelsEl.appendChild(modelEl.cloneNode(true)); |
|
} |
|
}); |
|
}) |
|
.catch(function(err) { |
|
const allModelsEl = document.getElementById('all-models'); |
|
allModelsEl.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'}`); |
|
}); |