| 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 |
| const tabButton = document.querySelector('.tab-button[data-tab="' + tabId + '"]'); |
| if (tabButton) { |
| tabButton.classList.add('active'); |
| } |
| |
| const tabContent = document.getElementById(tabId + '-tab'); |
| if (tabContent) { |
| tabContent.classList.add('active'); |
| } |
| |
| const navItem = document.querySelector('.nav-item[data-tab="' + tabId + '"]'); |
| if (navItem) { |
| navItem.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'); |
| |
| const codeTab = document.getElementById(tabId + '-code'); |
| if (codeTab) { |
| codeTab.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; |
| }); |
| |
| const endpointShorthand = document.getElementById('endpoint-shorthand'); |
| if (endpointShorthand) { |
| endpointShorthand.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 |
| const modelCount = document.getElementById('model-count'); |
| if (modelCount) { |
| modelCount.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++; |
| }); |
| |
| const claudeCountEl = document.getElementById('claude-count'); |
| if (claudeCountEl) { |
| claudeCountEl.textContent = claudeCount; |
| } |
| |
| const gptCountEl = document.getElementById('gpt-count'); |
| if (gptCountEl) { |
| gptCountEl.textContent = gptCount; |
| } |
| |
| const geminiCountEl = document.getElementById('gemini-count'); |
| if (geminiCountEl) { |
| geminiCountEl.textContent = geminiCount; |
| } |
| |
| const otherCountEl = document.getElementById('other-count'); |
| if (otherCountEl) { |
| otherCountEl.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'); |
| if (featuredContainer) { |
| 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'); |
| if (allModelsContainer) { |
| 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 modelSelect = document.getElementById('model-select'); |
| if (modelSelect) { |
| const option = document.createElement('option'); |
| option.value = model.id; |
| option.textContent = model.id; |
| modelSelect.appendChild(option); |
| } |
| }); |
| } |
| }) |
| .catch(err => { |
| const allModelsEl = document.getElementById('all-models'); |
| if (allModelsEl) { |
| allModelsEl.textContent = 'Failed to load models: ' + err.message; |
| } |
| |
| const featuredModelsEl = document.getElementById('featured-models'); |
| if (featuredModelsEl) { |
| featuredModelsEl.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'}`); |
| }); |