|
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> |
|
/* Core Variables */ |
|
:root { |
|
--bg-primary: #ffffff; |
|
--bg-secondary: #f5f7fa; |
|
--bg-tertiary: #edf0f7; |
|
--text-primary: #2c3e50; |
|
--text-secondary: #596877; |
|
--border-color: #dce1e8; |
|
--accent-color: #5D5CDE; |
|
--accent-hover: #4a49c8; |
|
--success-bg: #e1f5e1; |
|
--success-border: #c3e6cb; |
|
--warning-bg: #fff3cd; |
|
--warning-border: #ffeeba; |
|
--card-shadow: 0 2px 5px rgba(0,0,0,0.05); |
|
--hover-shadow: 0 5px 15px rgba(0,0,0,0.08); |
|
--transition: all 0.2s ease-in-out; |
|
} |
|
|
|
[data-theme="dark"] { |
|
--bg-primary: #121821; |
|
--bg-secondary: #1a2332; |
|
--bg-tertiary: #243044; |
|
--text-primary: #e0e6ed; |
|
--text-secondary: #9ba9b9; |
|
--border-color: #324156; |
|
--accent-color: #7D7CED; |
|
--accent-hover: #9795f0; |
|
--success-bg: #1e3a1e; |
|
--success-border: #2a5a2a; |
|
--warning-bg: #3a3018; |
|
--warning-border: #5a4820; |
|
--card-shadow: 0 2px 5px rgba(0,0,0,0.2); |
|
--hover-shadow: 0 5px 15px rgba(0,0,0,0.3); |
|
} |
|
|
|
/* Reset & Base Styles */ |
|
*, *::before, *::after { |
|
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.5; |
|
transition: var(--transition); |
|
padding: 0; |
|
overflow-x: hidden; |
|
max-width: 100vw; |
|
} |
|
|
|
/* Typography */ |
|
h1, h2, h3, h4 { |
|
margin: 0; |
|
font-weight: 600; |
|
line-height: 1.2; |
|
} |
|
|
|
h1 { font-size: 1.5rem; } |
|
h2 { font-size: 1.25rem; } |
|
h3 { font-size: 1.1rem; } |
|
h4 { font-size: 1rem; } |
|
|
|
/* Layout */ |
|
.dashboard { |
|
display: grid; |
|
grid-template-columns: 250px 1fr; |
|
min-height: 100vh; |
|
} |
|
|
|
/* Sidebar */ |
|
.sidebar { |
|
background: var(--bg-secondary); |
|
padding: 1rem; |
|
border-right: 1px solid var(--border-color); |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.logo { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
padding-bottom: 1rem; |
|
margin-bottom: 1rem; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.nav-item { |
|
display: flex; |
|
align-items: center; |
|
padding: 0.5rem 0.75rem; |
|
margin-bottom: 0.5rem; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
transition: var(--transition); |
|
color: var(--text-secondary); |
|
} |
|
|
|
.nav-item:hover, .nav-item.active { |
|
background: var(--bg-tertiary); |
|
color: var(--accent-color); |
|
} |
|
|
|
.nav-item i { |
|
margin-right: 8px; |
|
width: 20px; |
|
text-align: center; |
|
} |
|
|
|
/* Main Content */ |
|
.main-content { |
|
padding: 1rem; |
|
overflow-y: auto; |
|
height: 100vh; |
|
} |
|
|
|
/* Stats Grid */ |
|
.stats-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
|
gap: 1rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.stat-card { |
|
background: var(--bg-secondary); |
|
border-radius: 8px; |
|
padding: 1rem; |
|
box-shadow: var(--card-shadow); |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.stat-card .label { |
|
color: var(--text-secondary); |
|
font-size: 0.875rem; |
|
margin-bottom: 0.25rem; |
|
} |
|
|
|
.stat-card .value { |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
/* Tabs */ |
|
.tabs-container { |
|
margin-top: 1rem; |
|
} |
|
|
|
.tabs-header { |
|
display: flex; |
|
border-bottom: 1px solid var(--border-color); |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.tab-button { |
|
padding: 0.75rem 1rem; |
|
background: none; |
|
border: none; |
|
border-bottom: 2px solid transparent; |
|
color: var(--text-secondary); |
|
cursor: pointer; |
|
font-weight: 500; |
|
transition: var(--transition); |
|
} |
|
|
|
.tab-button.active { |
|
color: var(--accent-color); |
|
border-bottom-color: var(--accent-color); |
|
} |
|
|
|
.tab-content { |
|
display: none; |
|
} |
|
|
|
.tab-content.active { |
|
display: block; |
|
} |
|
|
|
/* Models Wall */ |
|
.models-container { |
|
margin-top: 1rem; |
|
} |
|
|
|
.models-grid { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 0.5rem; |
|
max-height: 150px; |
|
overflow-y: auto; |
|
padding: 0.5rem; |
|
background: var(--bg-tertiary); |
|
border-radius: 8px; |
|
} |
|
|
|
.model-tag { |
|
background: var(--bg-secondary); |
|
color: var(--text-primary); |
|
border: 1px solid var(--border-color); |
|
border-radius: 15px; |
|
padding: 0.25rem 0.75rem; |
|
font-size: 0.8rem; |
|
white-space: nowrap; |
|
transition: var(--transition); |
|
} |
|
|
|
.model-tag:hover { |
|
background: var(--accent-color); |
|
color: white; |
|
border-color: var(--accent-color); |
|
transform: translateY(-2px); |
|
box-shadow: var(--hover-shadow); |
|
} |
|
|
|
/* Setup Guide */ |
|
.guide-container { |
|
padding: 1rem; |
|
background: var(--bg-secondary); |
|
border-radius: 8px; |
|
} |
|
|
|
.guide-steps { |
|
display: flex; |
|
margin-top: 1rem; |
|
overflow-x: auto; |
|
gap: 1rem; |
|
padding-bottom: 0.5rem; |
|
} |
|
|
|
.guide-step { |
|
min-width: 250px; |
|
padding: 1rem; |
|
background: var(--bg-tertiary); |
|
border-radius: 8px; |
|
border-left: 3px solid var(--accent-color); |
|
flex: 1; |
|
} |
|
|
|
.guide-step h4 { |
|
margin-bottom: 0.5rem; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.guide-step h4 .step-number { |
|
display: inline-flex; |
|
align-items: center; |
|
justify-content: center; |
|
width: 24px; |
|
height: 24px; |
|
background: var(--accent-color); |
|
color: white; |
|
border-radius: 50%; |
|
margin-right: 0.5rem; |
|
font-size: 0.8rem; |
|
} |
|
|
|
.code-example { |
|
margin-top: 0.75rem; |
|
background: var(--bg-primary); |
|
padding: 0.75rem; |
|
border-radius: 6px; |
|
font-family: monospace; |
|
font-size: 0.8rem; |
|
overflow-x: auto; |
|
position: relative; |
|
} |
|
|
|
.copy-btn { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
background: var(--bg-secondary); |
|
border: none; |
|
border-radius: 4px; |
|
width: 28px; |
|
height: 28px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
opacity: 0.7; |
|
transition: var(--transition); |
|
} |
|
|
|
.copy-btn:hover { |
|
opacity: 1; |
|
background: var(--accent-color); |
|
color: white; |
|
} |
|
|
|
/* Theme Toggle */ |
|
.theme-toggle { |
|
margin-top: auto; |
|
padding-top: 1rem; |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
} |
|
|
|
.toggle-switch { |
|
position: relative; |
|
display: inline-block; |
|
width: 46px; |
|
height: 24px; |
|
} |
|
|
|
.toggle-switch input { |
|
opacity: 0; |
|
width: 0; |
|
height: 0; |
|
} |
|
|
|
.toggle-slider { |
|
position: absolute; |
|
cursor: pointer; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background-color: var(--bg-tertiary); |
|
transition: var(--transition); |
|
border-radius: 24px; |
|
} |
|
|
|
.toggle-slider:before { |
|
position: absolute; |
|
content: ""; |
|
height: 18px; |
|
width: 18px; |
|
left: 3px; |
|
bottom: 3px; |
|
background-color: var(--accent-color); |
|
transition: var(--transition); |
|
border-radius: 50%; |
|
} |
|
|
|
input:checked + .toggle-slider { |
|
background-color: var(--bg-tertiary); |
|
} |
|
|
|
input:checked + .toggle-slider:before { |
|
transform: translateX(22px); |
|
} |
|
|
|
/* Responsive Layout */ |
|
@media (max-width: 768px) { |
|
.dashboard { |
|
grid-template-columns: 1fr; |
|
} |
|
|
|
.sidebar { |
|
display: none; |
|
} |
|
|
|
.sidebar.active { |
|
display: flex; |
|
position: fixed; |
|
width: 250px; |
|
height: 100vh; |
|
z-index: 1000; |
|
} |
|
|
|
.mobile-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 1rem; |
|
background: var(--bg-secondary); |
|
box-shadow: var(--card-shadow); |
|
} |
|
|
|
.menu-toggle { |
|
display: block; |
|
background: none; |
|
border: none; |
|
color: var(--text-primary); |
|
cursor: pointer; |
|
font-size: 1.5rem; |
|
} |
|
} |
|
|
|
@media (min-width: 769px) { |
|
.mobile-header { |
|
display: none; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="dashboard"> |
|
<!-- Mobile Header (shows only on mobile) --> |
|
<div class="mobile-header"> |
|
<button class="menu-toggle" id="menu-toggle">☰</button> |
|
<h1>Cursor API</h1> |
|
<div></div> <!-- Empty div for flexbox spacing --> |
|
</div> |
|
|
|
<!-- Sidebar --> |
|
<aside class="sidebar" id="sidebar"> |
|
<div class="logo"> |
|
<h1>Cursor API</h1> |
|
</div> |
|
|
|
<div class="nav-item active" data-tab="dashboard"> |
|
<i>📊</i> Dashboard |
|
</div> |
|
<div class="nav-item" data-tab="models"> |
|
<i>🤖</i> Models |
|
</div> |
|
<div class="nav-item" data-tab="guide"> |
|
<i>📚</i> Integration Guide |
|
</div> |
|
<div class="nav-item" data-tab="tester"> |
|
<i>🧪</i> API Tester |
|
</div> |
|
|
|
<!-- Theme Toggle --> |
|
<div class="theme-toggle"> |
|
<span>Dark Mode</span> |
|
<label class="toggle-switch"> |
|
<input type="checkbox" id="theme-toggle"> |
|
<span class="toggle-slider"></span> |
|
</label> |
|
</div> |
|
</aside> |
|
|
|
<!-- Main Content --> |
|
<main class="main-content"> |
|
<!-- Stats Cards --> |
|
<div class="stats-grid"> |
|
<div class="stat-card"> |
|
<div class="label">API Endpoint</div> |
|
<div class="value" id="endpoint-url">Loading...</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">Authentication</div> |
|
<div class="value">Cursor Cookie (user_...)</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">Proxy Status</div> |
|
<div class="value">${proxyConfig ? 'Enabled' : 'Disabled'}</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">Available Models</div> |
|
<div class="value" id="model-count">Loading...</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Tabs Container --> |
|
<div class="tabs-container"> |
|
<div class="tabs-header"> |
|
<button class="tab-button active" data-tab="dashboard">Dashboard</button> |
|
<button class="tab-button" data-tab="models">Models</button> |
|
<button class="tab-button" data-tab="guide">Integration Guide</button> |
|
<button class="tab-button" data-tab="tester">API Tester</button> |
|
</div> |
|
|
|
<!-- Dashboard Tab --> |
|
<div class="tab-content active" id="dashboard-tab"> |
|
<h2>Server Information</h2> |
|
<div class="models-container"> |
|
<h3>Featured Models</h3> |
|
<div class="models-grid" id="featured-models"> |
|
Loading... |
|
</div> |
|
</div> |
|
|
|
<!-- Connection Guide Summary --> |
|
<div class="guide-container"> |
|
<h3>Quick Start Guide</h3> |
|
<div class="guide-steps"> |
|
<div class="guide-step"> |
|
<h4><span class="step-number">1</span>Authentication</h4> |
|
<p>Get your Cursor cookie that starts with "user_..." from browser cookies after logging in to Cursor.</p> |
|
</div> |
|
<div class="guide-step"> |
|
<h4><span class="step-number">2</span>API Requests</h4> |
|
<p>Send POST requests to <span id="endpoint-shorthand">Loading...</span> with your Cursor cookie as Bearer token.</p> |
|
</div> |
|
<div class="guide-step"> |
|
<h4><span class="step-number">3</span>Request Format</h4> |
|
<p>Use OpenAI-compatible format with model, messages array, and optional parameters.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Models Tab --> |
|
<div class="tab-content" id="models-tab"> |
|
<h2>Available Models</h2> |
|
<div class="models-grid" id="all-models"> |
|
Loading... |
|
</div> |
|
|
|
<div class="models-container"> |
|
<h3>Model Categories</h3> |
|
<div class="stats-grid"> |
|
<div class="stat-card"> |
|
<div class="label">Claude Models</div> |
|
<div class="value" id="claude-count">-</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">GPT Models</div> |
|
<div class="value" id="gpt-count">-</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">Gemini Models</div> |
|
<div class="value" id="gemini-count">-</div> |
|
</div> |
|
<div class="stat-card"> |
|
<div class="label">Other Models</div> |
|
<div class="value" id="other-count">-</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Integration Guide Tab --> |
|
<div class="tab-content" id="guide-tab"> |
|
<h2>Integration Guide</h2> |
|
|
|
<div class="guide-steps"> |
|
<div class="guide-step"> |
|
<h4><span class="step-number">1</span>Authentication</h4> |
|
<p>To authenticate with the Cursor API:</p> |
|
<ol style="margin-left: 1rem; margin-top: 0.5rem;"> |
|
<li>Log in to <a href="https://cursor.so" target="_blank">Cursor.so</a></li> |
|
<li>Open Developer Tools (F12)</li> |
|
<li>Go to Application → Cookies</li> |
|
<li>Find cookie with name starting with "user_"</li> |
|
<li>Use this value as your API key</li> |
|
</ol> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4><span class="step-number">2</span>API Configuration</h4> |
|
<p>Set up your API client with:</p> |
|
<ul style="margin-left: 1rem; margin-top: 0.5rem;"> |
|
<li>Base URL: <span id="endpoint-guide">Loading...</span></li> |
|
<li>Headers:</li> |
|
<div class="code-example"> |
|
Content-Type: application/json |
|
Authorization: Bearer user_your_cookie_value |
|
</div> |
|
</ul> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4><span class="step-number">3</span>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 |
|
} |
|
<button class="copy-btn" title="Copy to clipboard">📋</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="guide-container"> |
|
<h3>Code Examples</h3> |
|
<div class="tabs-header" style="margin-top: 0.5rem;"> |
|
<button class="tab-button active" data-code-tab="js">JavaScript</button> |
|
<button class="tab-button" data-code-tab="python">Python</button> |
|
<button class="tab-button" data-code-tab="curl">cURL</button> |
|
</div> |
|
|
|
<div class="code-tab active" id="js-code"> |
|
<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_value' |
|
}, |
|
body: JSON.stringify({ |
|
model: 'claude-3.7-sonnet', |
|
messages: [ |
|
{ role: 'user', content: 'Hello, who are you?' } |
|
], |
|
temperature: 0.7 |
|
}) |
|
}); |
|
|
|
const data = await response.json(); |
|
console.log(data); |
|
<button class="copy-btn" title="Copy to clipboard">📋</button> |
|
</div> |
|
</div> |
|
|
|
<div class="code-tab" id="python-code"> |
|
<div class="code-example"> |
|
import requests |
|
|
|
url = "${req.protocol}://${req.get('host')}/hf/v1/chat/completions" |
|
headers = { |
|
"Content-Type": "application/json", |
|
"Authorization": "Bearer user_your_cookie_value" |
|
} |
|
payload = { |
|
"model": "claude-3.7-sonnet", |
|
"messages": [ |
|
{"role": "user", "content": "Hello, who are you?"} |
|
], |
|
"temperature": 0.7 |
|
} |
|
|
|
response = requests.post(url, headers=headers, json=payload) |
|
data = response.json() |
|
print(data) |
|
<button class="copy-btn" title="Copy to clipboard">📋</button> |
|
</div> |
|
</div> |
|
|
|
<div class="code-tab" id="curl-code"> |
|
<div class="code-example"> |
|
curl -X POST "${req.protocol}://${req.get('host')}/hf/v1/chat/completions" \\ |
|
-H "Content-Type: application/json" \\ |
|
-H "Authorization: Bearer user_your_cookie_value" \\ |
|
-d '{ |
|
"model": "claude-3.7-sonnet", |
|
"messages": [ |
|
{"role": "user", "content": "Hello, who are you?"} |
|
], |
|
"temperature": 0.7 |
|
}' |
|
<button class="copy-btn" title="Copy to clipboard">📋</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- API Tester Tab --> |
|
<div class="tab-content" id="tester-tab"> |
|
<h2>API Tester</h2> |
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem;"> |
|
<div style="display: flex; flex-direction: column; gap: 1rem;"> |
|
<div class="guide-step"> |
|
<h4>Request</h4> |
|
<div style="margin-top: 0.5rem;"> |
|
<label for="api-key" style="display: block; margin-bottom: 0.25rem;">API Key:</label> |
|
<input type="password" id="api-key" placeholder="user_..." style="width: 100%; padding: 0.5rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);"> |
|
</div> |
|
|
|
<div style="margin-top: 0.5rem;"> |
|
<label for="model-select" style="display: block; margin-bottom: 0.25rem;">Model:</label> |
|
<select id="model-select" style="width: 100%; padding: 0.5rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);"> |
|
<option value="claude-3.7-sonnet">claude-3.7-sonnet</option> |
|
</select> |
|
</div> |
|
|
|
<div style="margin-top: 0.5rem;"> |
|
<label for="prompt-input" style="display: block; margin-bottom: 0.25rem;">Prompt:</label> |
|
<textarea id="prompt-input" placeholder="Enter your prompt here..." style="width: 100%; height: 120px; padding: 0.5rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); resize: vertical;">Hello, can you introduce yourself?</textarea> |
|
</div> |
|
|
|
<div style="margin-top: 0.5rem; display: flex; gap: 0.5rem; align-items: center;"> |
|
<label for="temperature" style="flex-shrink: 0;">Temperature:</label> |
|
<input type="range" id="temperature" min="0" max="1" step="0.1" value="0.7" style="flex-grow: 1;"> |
|
<span id="temperature-value">0.7</span> |
|
</div> |
|
|
|
<button id="submit-api" style="margin-top: 1rem; width: 100%; padding: 0.75rem; background: var(--accent-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: var(--transition);">Send Request</button> |
|
</div> |
|
</div> |
|
|
|
<div class="guide-step"> |
|
<h4>Response</h4> |
|
<div style="height: 300px; overflow-y: auto; margin-top: 0.5rem; padding: 0.75rem; background: var(--bg-tertiary); border-radius: 4px; font-family: monospace; white-space: pre-wrap; font-size: 0.8rem;" id="api-response"> |
|
Response will appear here... |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<script> |
|
// Theme Toggle |
|
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 |
|
themeToggle.addEventListener('change', function() { |
|
if (this.checked) { |
|
document.documentElement.setAttribute('data-theme', 'dark'); |
|
} else { |
|
document.documentElement.removeAttribute('data-theme'); |
|
} |
|
}); |
|
|
|
// Mobile Menu Toggle |
|
const menuToggle = document.getElementById('menu-toggle'); |
|
const sidebar = document.getElementById('sidebar'); |
|
|
|
if (menuToggle) { |
|
menuToggle.addEventListener('click', function() { |
|
sidebar.classList.toggle('active'); |
|
}); |
|
} |
|
|
|
// Main Tab Navigation |
|
const tabButtons = document.querySelectorAll('.tab-button'); |
|
const tabContents = document.querySelectorAll('.tab-content'); |
|
const navItems = document.querySelectorAll('.nav-item'); |
|
|
|
function setActiveTab(tabId) { |
|
// Deactivate all tabs |
|
tabButtons.forEach(button => button.classList.remove('active')); |
|
tabContents.forEach(content => content.classList.remove('active')); |
|
navItems.forEach(item => item.classList.remove('active')); |
|
|
|
// Activate the selected tab |
|
document.querySelector(`.tab-button[data-tab="${tabId}"]`)?.classList.add('active'); |
|
document.getElementById(`${tabId}-tab`)?.classList.add('active'); |
|
document.querySelector(`.nav-item[data-tab="${tabId}"]`)?.classList.add('active'); |
|
} |
|
|
|
// Set up tab click handlers |
|
tabButtons.forEach(button => { |
|
button.addEventListener('click', function() { |
|
const tabId = this.getAttribute('data-tab'); |
|
setActiveTab(tabId); |
|
}); |
|
}); |
|
|
|
// Set up nav item click handlers |
|
navItems.forEach(item => { |
|
item.addEventListener('click', function() { |
|
const tabId = this.getAttribute('data-tab'); |
|
setActiveTab(tabId); |
|
|
|
// Close mobile menu if open |
|
if (window.innerWidth < 769) { |
|
sidebar.classList.remove('active'); |
|
} |
|
}); |
|
}); |
|
|
|
// Code example tabs |
|
const codeTabButtons = document.querySelectorAll('[data-code-tab]'); |
|
const codeTabs = document.querySelectorAll('.code-tab'); |
|
|
|
codeTabButtons.forEach(button => { |
|
button.addEventListener('click', function() { |
|
codeTabButtons.forEach(btn => btn.classList.remove('active')); |
|
codeTabs.forEach(tab => tab.classList.remove('active')); |
|
|
|
const tabId = this.getAttribute('data-code-tab'); |
|
this.classList.add('active'); |
|
document.getElementById(`${tabId}-code`).classList.add('active'); |
|
}); |
|
}); |
|
|
|
// Copy buttons |
|
document.querySelectorAll('.copy-btn').forEach(button => { |
|
button.addEventListener('click', function() { |
|
const codeBlock = this.parentElement; |
|
const code = codeBlock.textContent.trim(); |
|
|
|
navigator.clipboard.writeText(code).then(() => { |
|
this.textContent = '✓'; |
|
setTimeout(() => { |
|
this.textContent = '📋'; |
|
}, 1500); |
|
}); |
|
}); |
|
}); |
|
|
|
// Get and display endpoint URL |
|
const url = new URL(window.location.href); |
|
const link = url.protocol + '//' + url.host + '/hf/v1'; |
|
|
|
// Update all endpoint URL displays |
|
document.querySelectorAll('#endpoint-url, #endpoint-guide').forEach(el => { |
|
el.textContent = link; |
|
}); |
|
|
|
document.getElementById('endpoint-shorthand').textContent = link + '/chat/completions'; |
|
|
|
// Temperature slider |
|
const temperatureSlider = document.getElementById('temperature'); |
|
const temperatureValue = document.getElementById('temperature-value'); |
|
|
|
if (temperatureSlider) { |
|
temperatureSlider.addEventListener('input', function() { |
|
temperatureValue.textContent = this.value; |
|
}); |
|
} |
|
|
|
// Load models list |
|
fetch('/hf/v1/models') |
|
.then(response => response.json()) |
|
.then(data => { |
|
// Update model counts |
|
document.getElementById('model-count').textContent = data.data.length; |
|
|
|
// Count models by category |
|
let claudeCount = 0; |
|
let gptCount = 0; |
|
let geminiCount = 0; |
|
let otherCount = 0; |
|
|
|
data.data.forEach(model => { |
|
if (model.id.includes('claude')) claudeCount++; |
|
else if (model.id.includes('gpt')) gptCount++; |
|
else if (model.id.includes('gemini')) geminiCount++; |
|
else otherCount++; |
|
}); |
|
|
|
document.getElementById('claude-count').textContent = claudeCount; |
|
document.getElementById('gpt-count').textContent = gptCount; |
|
document.getElementById('gemini-count').textContent = geminiCount; |
|
document.getElementById('other-count').textContent = otherCount; |
|
|
|
// Featured models (for dashboard) |
|
const featuredModels = ['claude-3.7-sonnet', 'gpt-4o', 'claude-3.5-sonnet', 'gemini-1.5-flash-500k']; |
|
const featuredContainer = document.getElementById('featured-models'); |
|
featuredContainer.innerHTML = ''; |
|
|
|
featuredModels.forEach(modelId => { |
|
const found = data.data.find(m => m.id === modelId); |
|
if (found) { |
|
const modelEl = document.createElement('div'); |
|
modelEl.className = 'model-tag'; |
|
modelEl.textContent = found.id; |
|
featuredContainer.appendChild(modelEl); |
|
} |
|
}); |
|
|
|
// All models |
|
const allModelsContainer = document.getElementById('all-models'); |
|
allModelsContainer.innerHTML = ''; |
|
|
|
// Sort models alphabetically |
|
data.data.sort((a, b) => a.id.localeCompare(b.id)); |
|
|
|
data.data.forEach(model => { |
|
const modelEl = document.createElement('div'); |
|
modelEl.className = 'model-tag'; |
|
modelEl.textContent = model.id; |
|
allModelsContainer.appendChild(modelEl); |
|
|
|
// Add to model select dropdown |
|
const option = document.createElement('option'); |
|
option.value = model.id; |
|
option.textContent = model.id; |
|
document.getElementById('model-select').appendChild(option); |
|
}); |
|
}) |
|
.catch(err => { |
|
document.querySelectorAll('#all-models, #featured-models').forEach(el => { |
|
el.textContent = 'Failed to load models: ' + err.message; |
|
}); |
|
}); |
|
|
|
// API Tester |
|
const submitApiBtn = document.getElementById('submit-api'); |
|
if (submitApiBtn) { |
|
submitApiBtn.addEventListener('click', async function() { |
|
const apiKey = document.getElementById('api-key').value; |
|
const model = document.getElementById('model-select').value; |
|
const prompt = document.getElementById('prompt-input').value; |
|
const temperature = parseFloat(document.getElementById('temperature').value); |
|
|
|
const responseContainer = document.getElementById('api-response'); |
|
responseContainer.textContent = 'Loading...'; |
|
|
|
try { |
|
const response = await fetch(`${link}/chat/completions`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${apiKey}` |
|
}, |
|
body: JSON.stringify({ |
|
model: model, |
|
messages: [{ role: 'user', content: prompt }], |
|
temperature: temperature |
|
}) |
|
}); |
|
|
|
const data = await response.json(); |
|
responseContainer.textContent = JSON.stringify(data, null, 2); |
|
} catch (error) { |
|
responseContainer.textContent = 'Error: ' + (error.message || 'Unknown error'); |
|
} |
|
}); |
|
} |
|
</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'}`); |
|
}); |