Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Anthropic ↔ OpenAI Proxy</title> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{font-family:'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh} | |
| .container{max-width:900px;margin:0 auto;padding:2rem} | |
| h1{font-size:1.8rem;font-weight:700;color:#7dd3fc;margin-bottom:.3rem} | |
| .subtitle{color:#94a3b8;margin-bottom:2rem;font-size:.9rem} | |
| .card{background:#1e293b;border-radius:12px;padding:1.5rem;margin-bottom:1.5rem;border:1px solid #334155} | |
| .card h2{font-size:1.1rem;font-weight:600;color:#bae6fd;margin-bottom:1rem} | |
| .tabs{display:flex;gap:.5rem;margin-bottom:1.5rem} | |
| .tab{padding:.5rem 1.2rem;border-radius:8px;border:1px solid #334155;background:#1e293b;color:#94a3b8;cursor:pointer;font-size:.9rem;transition:all .2s} | |
| .tab.active{background:#0284c7;color:#fff;border-color:#0284c7} | |
| .form-group{margin-bottom:1rem} | |
| label{display:block;font-size:.85rem;color:#94a3b8;margin-bottom:.4rem} | |
| input{width:100%;padding:.6rem .9rem;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#e2e8f0;font-size:.95rem;outline:none;transition:border .2s} | |
| input:focus{border-color:#0284c7} | |
| .btn{padding:.6rem 1.4rem;border-radius:8px;border:none;cursor:pointer;font-size:.9rem;font-weight:600;transition:all .2s} | |
| .btn-primary{background:#0284c7;color:#fff}.btn-primary:hover{background:#0369a1} | |
| .btn-danger{background:#dc2626;color:#fff;padding:.4rem .9rem;font-size:.8rem}.btn-danger:hover{background:#b91c1c} | |
| .btn-secondary{background:#334155;color:#e2e8f0}.btn-secondary:hover{background:#475569} | |
| .proxy-card{background:#0f172a;border:1px solid #334155;border-radius:10px;padding:1.2rem;margin-bottom:1rem} | |
| .proxy-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.8rem} | |
| .proxy-name{font-weight:600;color:#7dd3fc} | |
| .url-box{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:.6rem .9rem;font-family:monospace;font-size:.85rem;color:#a5f3fc;word-break:break-all;display:flex;justify-content:space-between;align-items:center;gap:.5rem} | |
| .copy-btn{background:#0284c7;border:none;color:#fff;padding:.3rem .7rem;border-radius:5px;cursor:pointer;font-size:.75rem;white-space:nowrap} | |
| .copy-btn:hover{background:#0369a1} | |
| .meta{font-size:.78rem;color:#64748b;margin-top:.6rem} | |
| .alert{padding:.8rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem} | |
| .alert-error{background:#450a0a;border:1px solid #dc2626;color:#fca5a5} | |
| .alert-success{background:#052e16;border:1px solid #16a34a;color:#86efac} | |
| .hidden{display:none} | |
| .mapping-row{display:flex;gap:.5rem;margin-bottom:.5rem} | |
| .mapping-row input{flex:1} | |
| .header-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem} | |
| .user-badge{background:#0284c7;padding:.3rem .8rem;border-radius:20px;font-size:.8rem;font-weight:600} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header-row"> | |
| <div> | |
| <h1>🔄 Anthropic ↔ OpenAI Proxy</h1> | |
| <p class="subtitle">Use any OpenAI-compatible backend with Anthropic SDK</p> | |
| </div> | |
| <div id="user-info" class="hidden"> | |
| <span class="user-badge" id="username-badge"></span> | |
| <button class="btn btn-secondary" style="margin-left:.5rem" onclick="logout()">Logout</button> | |
| </div> | |
| </div> | |
| <div id="auth-section"> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('login')">Login</button> | |
| <button class="tab" onclick="switchTab('signup')">Sign Up</button> | |
| </div> | |
| <div class="card"> | |
| <div id="auth-alert" class="hidden"></div> | |
| <div id="login-form"> | |
| <h2>Login</h2> | |
| <div class="form-group"><label>Username</label><input id="l-username" placeholder="your_username"/></div> | |
| <div class="form-group"><label>Password</label><input id="l-password" type="password" placeholder="••••••"/></div> | |
| <button class="btn btn-primary" onclick="login()">Login</button> | |
| </div> | |
| <div id="signup-form" class="hidden"> | |
| <h2>Create Account</h2> | |
| <div class="form-group"><label>Username</label><input id="s-username" placeholder="your_username"/></div> | |
| <div class="form-group"><label>Email</label><input id="s-email" type="email" placeholder="you@example.com"/></div> | |
| <div class="form-group"><label>Password</label><input id="s-password" type="password" placeholder="Min 6 characters"/></div> | |
| <button class="btn btn-primary" onclick="signup()">Create Account</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="dashboard-section" class="hidden"> | |
| <div class="card"> | |
| <h2>➕ Add New Proxy</h2> | |
| <div id="create-alert" class="hidden"></div> | |
| <div class="form-group"><label>Proxy Name</label><input id="p-name" placeholder="My LLM Server"/></div> | |
| <div class="form-group"><label>OpenAI-Compatible Base URL</label><input id="p-url" placeholder="http://localhost:8000/v1"/></div> | |
| <div class="form-group"><label>API Key</label><input id="p-key" type="password" placeholder="sk-... (or any string)"/></div> | |
| <div class="form-group"> | |
| <label>Model Mapping (Anthropic → Your model) <span style="color:#64748b;font-size:.8rem">optional</span></label> | |
| <div id="mapping-rows"></div> | |
| <button class="btn btn-secondary" style="font-size:.8rem;padding:.4rem .8rem;margin-top:.4rem" onclick="addMappingRow()">+ Add Mapping</button> | |
| </div> | |
| <button class="btn btn-primary" onclick="createProxy()" style="margin-top:.5rem">Create Proxy</button> | |
| </div> | |
| <div class="card"> | |
| <h2>🔗 Your Proxy Endpoints</h2> | |
| <div id="proxies-alert" class="hidden"></div> | |
| <div id="proxies-list"><p style="color:#64748b;font-size:.9rem">Loading...</p></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API = ''; | |
| let TOKEN = localStorage.getItem('token') || ''; | |
| function setAlert(id, msg, type='error') { | |
| const el = document.getElementById(id); | |
| el.className = `alert alert-${type}`; | |
| el.textContent = msg; | |
| el.classList.remove('hidden'); | |
| setTimeout(() => el.classList.add('hidden'), 5000); | |
| } | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', (tab==='login'?0:1)===i)); | |
| document.getElementById('login-form').classList.toggle('hidden', tab !== 'login'); | |
| document.getElementById('signup-form').classList.toggle('hidden', tab !== 'signup'); | |
| document.getElementById('auth-alert').classList.add('hidden'); | |
| } | |
| async function apiFetch(path, opts={}) { | |
| const headers = {'Content-Type':'application/json', ...(opts.headers||{})}; | |
| if (TOKEN) headers['Authorization'] = `Bearer ${TOKEN}`; | |
| const res = await fetch(API + path, {...opts, headers}); | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(data.detail || 'Request failed'); | |
| return data; | |
| } | |
| async function login() { | |
| try { | |
| const data = await apiFetch('/auth/login', { | |
| method:'POST', | |
| body: JSON.stringify({username: document.getElementById('l-username').value, password: document.getElementById('l-password').value}) | |
| }); | |
| TOKEN = data.access_token; | |
| localStorage.setItem('token', TOKEN); | |
| showDashboard(data.user.username); | |
| } catch(e) { setAlert('auth-alert', e.message); } | |
| } | |
| async function signup() { | |
| try { | |
| const data = await apiFetch('/auth/signup', { | |
| method:'POST', | |
| body: JSON.stringify({username: document.getElementById('s-username').value, email: document.getElementById('s-email').value, password: document.getElementById('s-password').value}) | |
| }); | |
| TOKEN = data.access_token; | |
| localStorage.setItem('token', TOKEN); | |
| showDashboard(data.user.username); | |
| } catch(e) { setAlert('auth-alert', e.message); } | |
| } | |
| function logout() { | |
| TOKEN = ''; localStorage.removeItem('token'); | |
| document.getElementById('dashboard-section').classList.add('hidden'); | |
| document.getElementById('auth-section').classList.remove('hidden'); | |
| document.getElementById('user-info').classList.add('hidden'); | |
| } | |
| function showDashboard(username) { | |
| document.getElementById('auth-section').classList.add('hidden'); | |
| document.getElementById('dashboard-section').classList.remove('hidden'); | |
| document.getElementById('user-info').classList.remove('hidden'); | |
| document.getElementById('username-badge').textContent = '👤 ' + username; | |
| loadProxies(); | |
| } | |
| function addMappingRow(anthr='', oai='') { | |
| const row = document.createElement('div'); | |
| row.className = 'mapping-row'; | |
| row.innerHTML = `<input placeholder="claude-3-opus-20240229" value="${anthr}"/><span style="color:#94a3b8;display:flex;align-items:center">→</span><input placeholder="gpt-4o" value="${oai}"/><button class="btn btn-danger" onclick="this.parentElement.remove()">✕</button>`; | |
| document.getElementById('mapping-rows').appendChild(row); | |
| } | |
| function getMappings() { | |
| const rows = document.querySelectorAll('#mapping-rows .mapping-row'); | |
| const mapping = {}; | |
| rows.forEach(r => { | |
| const inputs = r.querySelectorAll('input'); | |
| if (inputs[0].value && inputs[1].value) mapping[inputs[0].value.trim()] = inputs[1].value.trim(); | |
| }); | |
| return mapping; | |
| } | |
| async function createProxy() { | |
| try { | |
| const data = await apiFetch('/proxies', { | |
| method:'POST', | |
| body: JSON.stringify({ | |
| name: document.getElementById('p-name').value, | |
| openai_base_url: document.getElementById('p-url').value, | |
| openai_api_key: document.getElementById('p-key').value, | |
| model_mapping: getMappings(), | |
| }) | |
| }); | |
| setAlert('create-alert', `Proxy "${data.name}" created!`, 'success'); | |
| document.getElementById('p-name').value=''; | |
| document.getElementById('p-url').value=''; | |
| document.getElementById('p-key').value=''; | |
| document.getElementById('mapping-rows').innerHTML=''; | |
| loadProxies(); | |
| } catch(e) { setAlert('create-alert', e.message); } | |
| } | |
| async function loadProxies() { | |
| const list = document.getElementById('proxies-list'); | |
| try { | |
| const proxies = await apiFetch('/proxies'); | |
| if (!proxies.length) { list.innerHTML='<p style="color:#64748b;font-size:.9rem">No proxies yet. Create one above.</p>'; return; } | |
| list.innerHTML = proxies.map(p => ` | |
| <div class="proxy-card"> | |
| <div class="proxy-header"> | |
| <span class="proxy-name">${p.name}</span> | |
| <button class="btn btn-danger" onclick="deleteProxy(${p.id})">Delete</button> | |
| </div> | |
| <label style="font-size:.78rem;color:#64748b">Proxy URL (use as Anthropic base_url)</label> | |
| <div class="url-box"> | |
| <span>${p.proxy_url}</span> | |
| <button class="copy-btn" onclick="copyText('${p.proxy_url}',this)">Copy</button> | |
| </div> | |
| <div class="meta"> | |
| Forwards to: <code style="color:#7dd3fc">${p.openai_base_url}</code> | Created: ${new Date(p.created_at).toLocaleDateString()} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } catch(e) { list.innerHTML=`<p style="color:#fca5a5">${e.message}</p>`; } | |
| } | |
| async function deleteProxy(id) { | |
| if (!confirm('Delete this proxy?')) return; | |
| try { | |
| await apiFetch(`/proxies/${id}`, {method:'DELETE'}); | |
| loadProxies(); | |
| } catch(e) { setAlert('proxies-alert', e.message); } | |
| } | |
| function copyText(text, btn) { | |
| navigator.clipboard.writeText(text); | |
| btn.textContent='Copied!'; | |
| setTimeout(() => btn.textContent='Copy', 2000); | |
| } | |
| if (TOKEN) { | |
| apiFetch('/auth/me').then(u => showDashboard(u.username)).catch(() => { TOKEN=''; localStorage.removeItem('token'); }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |