Spaces:
Sleeping
Sleeping
| from flask import Flask, jsonify, request, redirect, session, Response | |
| import json | |
| import requests | |
| import os | |
| import hashlib | |
| import time | |
| import psycopg2 | |
| from psycopg2.extras import RealDictCursor | |
| from datetime import datetime, timedelta | |
| import threading | |
| import atexit | |
| import uuid | |
| import json | |
| import re | |
| from collections import defaultdict | |
| app = Flask(__name__) | |
| app.secret_key = 'your-secret-key-here' | |
| DISCORD_CLIENT_ID = '1426015278499627058' | |
| DISCORD_CLIENT_SECRET = 'YfTZg5JZA-y3GU1wDyoO3IPilwz9jXmL' | |
| DATABASE_URL = 'postgres://avnadmin:AVNS_o0LDhRmORJQ7lh7ULQS@goapi-hacktv.b.aivencloud.com:11426/defaultdb?sslmode=require' | |
| ADMIN_PASSWORD = "GoodGuys!Funn1" | |
| # Global variables for batching credit deductions | |
| pending_deductions = defaultdict(int) # username -> total_credits_to_deduct | |
| credit_cache = {} # username -> last_known_credits_balance | |
| deduction_lock = threading.Lock() | |
| batch_timer = None | |
| MODEL_MULTIPLIERS = { | |
| 'gpt-5-nano': 1.0, | |
| 'gpt-5-chat': 1.5, | |
| 'gpt-4.1': 1.0, | |
| 'claude-4-sonnet': 3.0, | |
| 'claude-4-opus': 5.0, | |
| 'claude-3.5-sonnet': 3.0, | |
| 'claude-3.7-sonnet': 3.0, | |
| 'claude-3.5-haiku': 2.0 | |
| } | |
| def init_db(): | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| c.execute('''CREATE TABLE IF NOT EXISTS users | |
| (username TEXT PRIMARY KEY, | |
| plan TEXT DEFAULT 'free', | |
| credits INTEGER DEFAULT 500000, | |
| last_reset TEXT)''') | |
| conn.commit() | |
| conn.close() | |
| def get_user(username): | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor(cursor_factory=RealDictCursor) | |
| c.execute('SELECT plan, credits, last_reset FROM users WHERE username = %s', (username,)) | |
| result = c.fetchone() | |
| conn.close() | |
| if result: | |
| return dict(result) | |
| return None | |
| def create_or_update_user(username, plan='free'): | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| now = datetime.now().isoformat() | |
| c.execute('''INSERT INTO users (username, plan, credits, last_reset) | |
| VALUES (%s, %s, 500000, %s) | |
| ON CONFLICT (username) | |
| DO UPDATE SET plan = EXCLUDED.plan, credits = EXCLUDED.credits, last_reset = EXCLUDED.last_reset''', | |
| (username, plan, now)) | |
| conn.commit() | |
| conn.close() | |
| def reset_credits_if_needed(username): | |
| user = get_user(username) | |
| if not user: | |
| return | |
| last_reset = user['last_reset'] | |
| if last_reset: | |
| last_reset_date = datetime.fromisoformat(last_reset).date() | |
| today = datetime.now().date() | |
| if last_reset_date < today: | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| now = datetime.now().isoformat() | |
| c.execute('UPDATE users SET credits = 500000, last_reset = %s WHERE username = %s', | |
| (now, username)) | |
| conn.commit() | |
| conn.close() | |
| def deduct_credits(username, amount): | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| c.execute('UPDATE users SET credits = credits - %s WHERE username = %s AND credits >= %s', | |
| (amount, username, amount)) | |
| success = c.rowcount > 0 | |
| conn.commit() | |
| conn.close() | |
| return success | |
| def get_user_credits(username): | |
| user = get_user(username) | |
| if user: | |
| reset_credits_if_needed(username) | |
| user = get_user(username) | |
| return user['credits'] if user else 0 | |
| return 0 | |
| def get_cached_credits(username): | |
| """Get cached credits balance for a user (estimated: cached_balance - pending_deductions)""" | |
| with deduction_lock: | |
| cached_balance = credit_cache.get(username, 0) | |
| pending_for_user = pending_deductions.get(username, 0) | |
| return cached_balance - pending_for_user | |
| def update_credit_cache(username, new_balance): | |
| """Update the cached credit balance for a user""" | |
| with deduction_lock: | |
| credit_cache[username] = new_balance | |
| print(f"Updated credit cache for {username}: {new_balance} credits") | |
| def clear_cache_entry(username): | |
| """Clear cache entry for a user (called when they run out of credits)""" | |
| with deduction_lock: | |
| credit_cache.pop(username, None) | |
| print(f"Cleared credit cache for {username}") | |
| def add_pending_deduction(username, amount): | |
| """Add a credit deduction to the pending queue""" | |
| with deduction_lock: | |
| pending_deductions[username] += amount | |
| def process_pending_deductions(): | |
| """Process all pending credit deductions in a single batch (runs in background thread)""" | |
| try: | |
| with deduction_lock: | |
| if not pending_deductions: | |
| return | |
| # Create a copy of pending deductions to process | |
| deductions_to_process = dict(pending_deductions) | |
| pending_deductions.clear() | |
| # Process deductions in database | |
| if deductions_to_process: | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| for username, amount in deductions_to_process.items(): | |
| c.execute('UPDATE users SET credits = credits - %s WHERE username = %s AND credits >= %s', | |
| (amount, username, amount)) | |
| # Get updated balances for all affected users | |
| usernames = list(deductions_to_process.keys()) | |
| if usernames: | |
| placeholders = ','.join(['%s'] * len(usernames)) | |
| c.execute(f'SELECT username, credits FROM users WHERE username IN ({placeholders})', usernames) | |
| for row in c.fetchall(): | |
| username, new_balance = row | |
| update_credit_cache(username, new_balance) | |
| conn.commit() | |
| conn.close() | |
| print(f"Processed {len(deductions_to_process)} credit deductions") | |
| except Exception as e: | |
| print(f"Error processing credit deductions: {e}") | |
| # Put deductions back in queue if there was an error | |
| with deduction_lock: | |
| for username, amount in deductions_to_process.items(): | |
| pending_deductions[username] += amount | |
| def start_batch_timer(): | |
| """Start or restart the 3-minute batch timer""" | |
| global batch_timer | |
| if batch_timer: | |
| batch_timer.cancel() | |
| batch_timer = threading.Timer(180.0, lambda: threading.Thread(target=process_pending_deductions, daemon=True).start()) # 3 minutes = 180 seconds | |
| batch_timer.daemon = True | |
| batch_timer.start() | |
| def verify_admin_password(password): | |
| """Verify admin password""" | |
| return password == ADMIN_PASSWORD | |
| def export_database(): | |
| """Export current database to JSON""" | |
| try: | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor(cursor_factory=RealDictCursor) | |
| # Get all users | |
| c.execute('SELECT * FROM users') | |
| users = c.fetchall() | |
| # Convert to list of dicts for JSON serialization | |
| users_data = [dict(user) for user in users] | |
| conn.close() | |
| return {"users": users_data, "exported_at": datetime.now().isoformat()} | |
| except Exception as e: | |
| return {"error": f"Failed to export database: {str(e)}"} | |
| def import_database(data): | |
| """Import database from JSON data""" | |
| try: | |
| if "users" not in data: | |
| return {"error": "Invalid data format: missing 'users' key"} | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| # Clear existing data | |
| c.execute('DELETE FROM users') | |
| # Insert new data | |
| for user_data in data["users"]: | |
| c.execute('''INSERT INTO users (username, plan, credits, last_reset) | |
| VALUES (%s, %s, %s, %s)''', | |
| (user_data['username'], user_data.get('plan', 'free'), | |
| user_data.get('credits', 500000), user_data.get('last_reset'))) | |
| conn.commit() | |
| conn.close() | |
| # Update credit cache for all imported users | |
| for user_data in data["users"]: | |
| update_credit_cache(user_data['username'], user_data.get('credits', 500000)) | |
| return {"success": f"Imported {len(data['users'])} users"} | |
| except Exception as e: | |
| return {"error": f"Failed to import database: {str(e)}"} | |
| init_db() | |
| def get_all_users(): | |
| """Get all users from database""" | |
| try: | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor(cursor_factory=RealDictCursor) | |
| c.execute('SELECT * FROM users ORDER BY username') | |
| users = c.fetchall() | |
| conn.close() | |
| return [dict(user) for user in users] | |
| except Exception as e: | |
| return [] | |
| def db_admin(): | |
| """Database admin interface""" | |
| if request.method == 'GET': | |
| users = get_all_users() | |
| # Generate user management table | |
| users_html = "" | |
| for user in users: | |
| users_html += f''' | |
| <tr> | |
| <td>{user['username']}</td> | |
| <td> | |
| <div class="plan-container"> | |
| <select class="plan-select" data-username="{user['username']}"> | |
| <option value="free" {'selected' if user['plan'] == 'free' else ''}>Free</option> | |
| <option value="premium" {'selected' if user['plan'] == 'premium' else ''}>Premium</option> | |
| <option value="pro" {'selected' if user['plan'] == 'pro' else ''}>Pro</option> | |
| <option value="custom">Custom...</option> | |
| </select> | |
| <input type="text" class="plan-custom-input" data-username="{user['username']}" | |
| placeholder="Enter custom plan" style="display: none; margin-top: 5px;"> | |
| </div> | |
| </td> | |
| <td>{user['credits']:,}</td> | |
| <td> | |
| <input type="number" class="tokens-input" data-username="{user['username']}" | |
| value="{user['credits']}" min="0" max="10000000" step="1000"> | |
| </td> | |
| <td> | |
| <button class="update-btn" data-username="{user['username']}">Update</button> | |
| </td> | |
| </tr> | |
| ''' | |
| return f''' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Database Admin</title> | |
| <style> | |
| body {{ font-family: Arial, sans-serif; max-width: 1200px; margin: 50px auto; padding: 20px; }} | |
| .form-group {{ margin-bottom: 20px; }} | |
| label {{ display: block; margin-bottom: 5px; font-weight: bold; }} | |
| input[type="password"], input[type="file"] {{ width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }} | |
| input[type="text"] {{ width: 300px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }} | |
| button {{ background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 2px; }} | |
| button:hover {{ background: #0056b3; }} | |
| button.update-btn {{ background: #28a745; }} | |
| button.update-btn:hover {{ background: #218838; }} | |
| .error {{ color: red; margin-top: 10px; }} | |
| .success {{ color: green; margin-top: 10px; }} | |
| .section {{ margin-bottom: 40px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }} | |
| .user-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }} | |
| .user-table th, .user-table td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }} | |
| .user-table th {{ background-color: #f2f2f2; }} | |
| .user-table input {{ width: 120px; }} | |
| .plan-container {{ display: flex; flex-direction: column; gap: 5px; }} | |
| .plan-custom-input {{ width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; }} | |
| .search-box {{ margin-bottom: 20px; }} | |
| .status {{ margin-top: 10px; padding: 10px; border-radius: 4px; }} | |
| .status.success {{ background-color: #d4edda; color: #155724; }} | |
| .status.error {{ background-color: #f8d7da; color: #721c24; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Database Administration</h1> | |
| <div class="section"> | |
| <h2>User Management</h2> | |
| <div class="search-box"> | |
| <input type="text" id="searchInput" placeholder="Search users..." onkeyup="filterUsers()"> | |
| </div> | |
| <table class="user-table" id="usersTable"> | |
| <thead> | |
| <tr> | |
| <th>Username</th> | |
| <th>Plan</th> | |
| <th>Credits</th> | |
| <th>Set Tokens/Day</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {users_html} | |
| </tbody> | |
| </table> | |
| <div id="status" class="status" style="display: none;"></div> | |
| </div> | |
| <div class="section"> | |
| <h2>Export Database</h2> | |
| <p>Download the current database as a JSON file.</p> | |
| <form method="post" action="/db/admin/export"> | |
| <div class="form-group"> | |
| <label for="password">Admin Password:</label> | |
| <input type="password" id="password" name="password" required> | |
| </div> | |
| <button type="submit">Export Database</button> | |
| </form> | |
| </div> | |
| <div class="section"> | |
| <h2>Import Database</h2> | |
| <p>Upload a JSON file to replace the current database.</p> | |
| <form method="post" action="/db/admin/import" enctype="multipart/form-data"> | |
| <div class="form-group"> | |
| <label for="password">Admin Password:</label> | |
| <input type="password" id="password" name="password" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="db_file">Database File (JSON):</label> | |
| <input type="file" id="db_file" name="db_file" accept=".json" required> | |
| </div> | |
| <button type="submit">Import Database</button> | |
| </form> | |
| </div> | |
| <script> | |
| function filterUsers() {{ | |
| const input = document.getElementById('searchInput'); | |
| const filter = input.value.toLowerCase(); | |
| const table = document.getElementById('usersTable'); | |
| const rows = table.getElementsByTagName('tr'); | |
| for (let i = 1; i < rows.length; i++) {{ | |
| const cells = rows[i].getElementsByTagName('td'); | |
| if (cells.length > 0) {{ | |
| const username = cells[0].textContent.toLowerCase(); | |
| if (username.indexOf(filter) > -1) {{ | |
| rows[i].style.display = ''; | |
| }} else {{ | |
| rows[i].style.display = 'none'; | |
| }} | |
| }} | |
| }} | |
| }} | |
| function toggleCustomPlan(username) {{ | |
| const planSelect = document.querySelector(`.plan-select[data-username="${{username}}"]`); | |
| const customInput = document.querySelector(`.plan-custom-input[data-username="${{username}}"]`); | |
| if (planSelect.value === 'custom') {{ | |
| customInput.style.display = 'block'; | |
| customInput.focus(); | |
| }} else {{ | |
| customInput.style.display = 'none'; | |
| customInput.value = ''; | |
| }} | |
| }} | |
| async function updateUser(username) {{ | |
| const planSelect = document.querySelector(`.plan-select[data-username="${{username}}"]`); | |
| const customInput = document.querySelector(`.plan-custom-input[data-username="${{username}}"]`); | |
| const tokensInput = document.querySelector(`.tokens-input[data-username="${{username}}"]`); | |
| let plan = planSelect.value; | |
| if (plan === 'custom') {{ | |
| plan = customInput.value.trim(); | |
| if (!plan) {{ | |
| alert('Please enter a custom plan name'); | |
| return; | |
| }} | |
| }} | |
| const tokens = parseInt(tokensInput.value); | |
| try {{ | |
| const response = await fetch('/db/admin/update-user', {{ | |
| method: 'POST', | |
| headers: {{ | |
| 'Content-Type': 'application/json', | |
| }}, | |
| body: JSON.stringify({{ | |
| username: username, | |
| plan: plan, | |
| credits: tokens, | |
| password: prompt('Enter admin password:') | |
| }}) | |
| }}); | |
| const result = await response.json(); | |
| const statusDiv = document.getElementById('status'); | |
| if (response.ok) {{ | |
| statusDiv.textContent = result.success; | |
| statusDiv.className = 'status success'; | |
| // Update the credits column | |
| const creditsCell = document.querySelector(`.tokens-input[data-username="${{username}}"]`).parentElement.previousElementSibling; | |
| creditsCell.textContent = tokens.toLocaleString(); | |
| }} else {{ | |
| statusDiv.textContent = result.error; | |
| statusDiv.className = 'status error'; | |
| }} | |
| statusDiv.style.display = 'block'; | |
| setTimeout(() => statusDiv.style.display = 'none', 3000); | |
| }} catch (error) {{ | |
| console.error('Error updating user:', error); | |
| const statusDiv = document.getElementById('status'); | |
| statusDiv.textContent = 'Error updating user'; | |
| statusDiv.className = 'status error'; | |
| statusDiv.style.display = 'block'; | |
| setTimeout(() => statusDiv.style.display = 'none', 3000); | |
| }} | |
| }} | |
| // Add event listeners and initialize custom plans | |
| document.addEventListener('DOMContentLoaded', function() {{ | |
| // Handle plan selection changes | |
| const planSelects = document.querySelectorAll('.plan-select'); | |
| planSelects.forEach(select => {{ | |
| select.addEventListener('change', function() {{ | |
| const username = this.getAttribute('data-username'); | |
| toggleCustomPlan(username); | |
| }}); | |
| }}); | |
| // Initialize custom plan inputs for existing custom plans | |
| const usersData = {json.dumps(users)}; | |
| usersData.forEach(user => {{ | |
| if (!['free', 'premium', 'pro'].includes(user.plan)) {{ | |
| // This is a custom plan | |
| const select = document.querySelector(`.plan-select[data-username="${{user.username}}"]`); | |
| const customInput = document.querySelector(`.plan-custom-input[data-username="${{user.username}}"]`); | |
| if (select && customInput) {{ | |
| select.value = 'custom'; | |
| customInput.value = user.plan; | |
| customInput.style.display = 'block'; | |
| }} | |
| }} | |
| }}); | |
| // Handle update buttons | |
| const updateBtns = document.querySelectorAll('.update-btn'); | |
| updateBtns.forEach(btn => {{ | |
| btn.addEventListener('click', function() {{ | |
| const username = this.getAttribute('data-username'); | |
| updateUser(username); | |
| }}); | |
| }}); | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| return redirect('/db/admin') | |
| def db_admin_export(): | |
| """Export database as JSON file""" | |
| password = request.form.get('password') | |
| if not verify_admin_password(password): | |
| return jsonify({"error": "Invalid admin password"}), 401 | |
| data = export_database() | |
| if "error" in data: | |
| return jsonify(data), 500 | |
| # Create response with JSON data | |
| response = Response( | |
| json.dumps(data, indent=2), | |
| mimetype='application/json', | |
| headers={'Content-disposition': 'attachment; filename=database_export.json'} | |
| ) | |
| return response | |
| def db_admin_import(): | |
| """Import database from JSON file""" | |
| password = request.form.get('password') | |
| if not verify_admin_password(password): | |
| return jsonify({"error": "Invalid admin password"}), 401 | |
| if 'db_file' not in request.files: | |
| return jsonify({"error": "No file provided"}), 400 | |
| file = request.files['db_file'] | |
| if file.filename == '': | |
| return jsonify({"error": "No file selected"}), 400 | |
| if not file.filename.endswith('.json'): | |
| return jsonify({"error": "File must be a JSON file"}), 400 | |
| try: | |
| file_content = file.read().decode('utf-8') | |
| data = json.loads(file_content) | |
| result = import_database(data) | |
| if "error" in result: | |
| return jsonify(result), 500 | |
| return jsonify(result) | |
| except json.JSONDecodeError: | |
| return jsonify({"error": "Invalid JSON file"}), 400 | |
| except Exception as e: | |
| return jsonify({"error": f"Import failed: {str(e)}"}), 500 | |
| def db_admin_update_user(): | |
| """Update user plan and credits""" | |
| try: | |
| data = request.get_json() | |
| if not data: | |
| return jsonify({"error": "No data provided"}), 400 | |
| password = data.get('password') | |
| if not verify_admin_password(password): | |
| return jsonify({"error": "Invalid admin password"}), 401 | |
| username = data.get('username') | |
| plan = data.get('plan') | |
| credits = data.get('credits') | |
| if not username or not plan or credits is None: | |
| return jsonify({"error": "Missing required fields"}), 400 | |
| # Update user in database | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| # Update plan and credits, reset last_reset to today | |
| now = datetime.now().isoformat() | |
| c.execute('''UPDATE users | |
| SET plan = %s, credits = %s, last_reset = %s | |
| WHERE username = %s''', | |
| (plan, credits, now, username)) | |
| if c.rowcount == 0: | |
| conn.close() | |
| return jsonify({"error": "User not found"}), 404 | |
| conn.commit() | |
| conn.close() | |
| # Update credit cache | |
| update_credit_cache(username, credits) | |
| return jsonify({"success": f"Updated {username}: {plan} plan, {credits:,} credits"}) | |
| except Exception as e: | |
| return jsonify({"error": f"Update failed: {str(e)}"}), 500 | |
| def test_stream(): | |
| """Test endpoint to check upstream API streaming behavior with longer prompt""" | |
| import time | |
| # Longer test message for better streaming analysis | |
| test_data = { | |
| "model": "gpt-5", | |
| "messages": [{"role": "user", "content": "Write a short novel about a detective solving a mystery in Victorian London. Include dialogue, suspense, and a surprising twist ending."}], | |
| "stream": True, | |
| "max_tokens": 500 | |
| } | |
| headers = { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': 'Bearer test-key' # This might not work, but for testing | |
| } | |
| try: | |
| start_time = time.time() | |
| print(f"Starting test stream request at {start_time}") | |
| response = requests.post( | |
| 'https://ch.at/v1/chat/completions', | |
| json=test_data, | |
| headers=headers, | |
| stream=True, | |
| timeout=60 # Longer timeout for novel generation | |
| ) | |
| if response.status_code != 200: | |
| return jsonify({ | |
| "error": f"HTTP {response.status_code}", | |
| "message": response.text, | |
| "total_time": time.time() - start_time | |
| }), response.status_code | |
| chunk_count = 0 | |
| first_chunk_time = None | |
| last_chunk_time = None | |
| total_response = "" | |
| for chunk in response.iter_content(chunk_size=8192): | |
| if chunk: | |
| chunk_count += 1 | |
| current_time = time.time() | |
| if first_chunk_time is None: | |
| first_chunk_time = current_time | |
| print(f"First chunk received at {current_time} ({current_time - start_time:.2f}s after start)") | |
| last_chunk_time = current_time | |
| chunk_str = chunk.decode('utf-8') | |
| total_response += chunk_str | |
| print(f"Chunk {chunk_count} at {current_time} ({current_time - start_time:.2f}s): {len(chunk_str)} chars") | |
| end_time = time.time() | |
| total_time = end_time - start_time | |
| streaming_duration = last_chunk_time - first_chunk_time if first_chunk_time and last_chunk_time else 0 | |
| result = { | |
| "total_time": round(total_time, 2), | |
| "streaming_duration": round(streaming_duration, 2), | |
| "chunk_count": chunk_count, | |
| "response_length": len(total_response), | |
| "average_chunk_interval": round(streaming_duration / max(chunk_count - 1, 1), 2) if chunk_count > 1 else 0, | |
| "chunks_per_second": round(chunk_count / max(streaming_duration, 0.01), 2), | |
| "streaming_efficiency": "Good" if streaming_duration > 1.0 else "Poor (likely buffered)", | |
| "status": "success" | |
| } | |
| print(f"Stream test completed: {result}") | |
| return jsonify(result) | |
| except Exception as e: | |
| end_time = time.time() | |
| return jsonify({ | |
| "error": "Test failed", | |
| "message": str(e), | |
| "total_time": round(end_time - start_time, 2), | |
| "status": "error" | |
| }), 500 | |
| def models(): | |
| models_data = { | |
| "object": "list", | |
| "data": [ | |
| { | |
| "id": "gpt-5-nano", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "openai" | |
| }, | |
| { | |
| "id": "gpt-5-chat", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "openai" | |
| }, | |
| { | |
| "id": "gpt-4.1", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "openai" | |
| }, | |
| { | |
| "id": "claude-4-sonnet", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "anthropic" | |
| }, | |
| { | |
| "id": "claude-4-opus", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "anthropic" | |
| }, | |
| { | |
| "id": "claude-3.5-sonnet", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "anthropic" | |
| }, | |
| { | |
| "id": "claude-3.7-sonnet", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "anthropic" | |
| }, | |
| { | |
| "id": "claude-3.5-haiku", | |
| "object": "model", | |
| "created": 1677610602, | |
| "owned_by": "anthropic" | |
| } | |
| ] | |
| } | |
| return jsonify(models_data) | |
| def chat_completions(): | |
| try: | |
| auth_header = request.headers.get('Authorization') | |
| if not auth_header or not auth_header.startswith('Bearer '): | |
| return jsonify({"error": "Missing or invalid Bearer token"}), 401 | |
| token = auth_header.split(' ')[1] | |
| conn = psycopg2.connect(DATABASE_URL) | |
| c = conn.cursor() | |
| c.execute('SELECT username FROM users') | |
| all_users = c.fetchall() | |
| conn.close() | |
| username = None | |
| for (user,) in all_users: | |
| expected_key = f"sk-{hashlib.sha256(user.encode()).hexdigest()[:32]}" | |
| if token == expected_key: | |
| username = user | |
| break | |
| if not username: | |
| return jsonify({"error": "Invalid API key"}), 401 | |
| # Get cached credits (no database call) | |
| cached_credits = get_cached_credits(username) | |
| # If user has no cached credits, try to load from database once | |
| if username not in credit_cache: | |
| try: | |
| real_credits = get_user_credits(username) | |
| update_credit_cache(username, real_credits) | |
| cached_credits = real_credits | |
| except: | |
| # If database fails, reject request | |
| return jsonify({"error": "Unable to verify credits"}), 402 | |
| # Reject if no credits available (cached or real) | |
| if cached_credits <= 0: | |
| clear_cache_entry(username) # Clear cache since they're out of credits | |
| return jsonify({"error": "Insufficient credits"}), 402 | |
| data = request.get_json() | |
| if not data: | |
| return jsonify({"error": "No JSON data provided"}), 400 | |
| model = data.get('model') | |
| if not model: | |
| return jsonify({"error": "No model specified"}), 400 | |
| original_model = model | |
| if model == 'gpt-5-nano': | |
| target_model = 'gpt-5-nano' | |
| elif model == 'gpt-5-chat': | |
| target_model = 'gpt-5' | |
| elif model == 'gpt-4.1': | |
| target_model = 'gpt-41' | |
| elif model == 'claude-4-sonnet': | |
| target_model = 'claude-4-sonnet' | |
| elif model == 'claude-4-opus': | |
| target_model = 'claude-4-opus' | |
| elif model == 'claude-3.5-sonnet': | |
| target_model = 'claude-3.5-sonnet' | |
| elif model == 'claude-3.7-sonnet': | |
| target_model = 'claude-3.7-sonnet' | |
| elif model == 'claude-3.5-haiku': | |
| target_model = 'claude-3.5-haiku' | |
| else: | |
| return jsonify({"error": f"Unsupported model: {model}"}), 400 | |
| data['model'] = target_model | |
| messages = data.get('messages', []) | |
| # Count total words in all messages (input and assistant) | |
| input_words = sum(len(str(msg.get('content', '')).split()) for msg in messages) | |
| # Estimate output words (roughly half the input length, min 100 words) | |
| estimated_output_words = max(100, input_words // 2) | |
| total_words = input_words + estimated_output_words | |
| multiplier = MODEL_MULTIPLIERS.get(model, 1.0) | |
| estimated_cost = int(total_words * multiplier) | |
| # Check against cached credits (no database call) | |
| if estimated_cost > cached_credits: | |
| clear_cache_entry(username) # Clear cache since insufficient credits detected | |
| return jsonify({"error": "Insufficient credits for this request"}), 402 | |
| # Add to pending deductions instead of immediate database update | |
| add_pending_deduction(username, estimated_cost) | |
| start_batch_timer() # Start/restart the 3-minute timer | |
| if 'claude' in model.lower() and messages: | |
| messages = data['messages'] | |
| if messages and messages[0]['role'] == 'system': | |
| system_content = messages[0]['content'] | |
| data['messages'] = [ | |
| {'role': 'user', 'content': system_content}, | |
| {'role': 'assistant', 'content': system_content} | |
| ] + messages[1:] | |
| auth_header = request.headers.get('Authorization') | |
| if not auth_header or not auth_header.startswith('Bearer '): | |
| return jsonify({"error": "Missing or invalid Bearer token"}), 401 | |
| headers = { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': auth_header | |
| } | |
| max_retries = 3 | |
| retry_delay = 1 | |
| for attempt in range(max_retries): | |
| response = requests.post( | |
| 'https://ch.at/v1/chat/completions', | |
| json=data, | |
| headers=headers, | |
| stream=True | |
| ) | |
| if response.status_code == 200: | |
| break | |
| if attempt < max_retries - 1: | |
| time.sleep(retry_delay) | |
| else: | |
| return | |
| # Ensure we have a valid streaming response | |
| if not response: | |
| return jsonify({"error": "Failed to get response from upstream API"}), 502 | |
| def generate(): | |
| # Generate a UUID for this chat completion | |
| completion_id = f"chatcmpl-{uuid.uuid4().hex[:16]}" | |
| try: | |
| for chunk in response.iter_content(chunk_size=8192): | |
| if chunk: | |
| chunk_str = chunk.decode('utf-8', errors='replace') | |
| # Replace the chat completion ID with our UUID | |
| chunk_str = re.sub(r'"id":"chatcmpl-[^"]*"', f'"id":"{completion_id}"', chunk_str) | |
| # Replace model IDs in the response to match the original request | |
| if original_model == 'gpt-5-chat': | |
| chunk_str = chunk_str.replace('"model":"gpt-5"', '"model":"gpt-5-chat"') | |
| elif original_model == 'gpt-4.1': | |
| chunk_str = chunk_str.replace('"model":"gpt-41"', '"model":"gpt-4.1"') | |
| # Check if this chunk contains [DONE] - handle edge case where [DONE] might be split across chunks | |
| if 'data: [DONE]' in chunk_str: | |
| print("Stream ended with [DONE], triggering retry...") | |
| # Return a special response that indicates retry is needed | |
| return Response('data: [RETRY_REQUEST]\n\n', content_type='text/plain') | |
| # Ensure proper SSE formatting and only yield non-empty chunks | |
| if chunk_str.strip(): | |
| yield f"{chunk_str}\n\n".encode('utf-8') | |
| except Exception as e: | |
| print(f"Error in streaming: {e}") | |
| yield b'data: {"error": "Streaming error"}\n\n' | |
| response_obj = Response( | |
| generate(), | |
| content_type='text/event-stream; charset=utf-8', | |
| status=response.status_code | |
| ) | |
| return response_obj | |
| except Exception as e: | |
| return jsonify({"error": f"Internal server error: {str(e)}"}), 500 | |
| def auth(): | |
| redirect_uri = f"{request.scheme}://{request.host}/auth/callback" | |
| discord_auth_url = f"https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&redirect_uri={redirect_uri}&response_type=code&scope=identify" | |
| return redirect(discord_auth_url) | |
| def auth_callback(): | |
| code = request.args.get('code') | |
| if not code: | |
| return "Error: No authorization code provided" | |
| redirect_uri = f"{request.scheme}://{request.host}/auth/callback" | |
| data = { | |
| 'client_id': DISCORD_CLIENT_ID, | |
| 'client_secret': DISCORD_CLIENT_SECRET, | |
| 'grant_type': 'authorization_code', | |
| 'code': code, | |
| 'redirect_uri': redirect_uri | |
| } | |
| headers = {'Content-Type': 'application/x-www-form-urlencoded'} | |
| token_response = requests.post('https://discord.com/api/oauth2/token', data=data, headers=headers) | |
| token_json = token_response.json() | |
| if 'access_token' not in token_json: | |
| return "Error: Failed to get access token" | |
| access_token = token_json['access_token'] | |
| user_response = requests.get('https://discord.com/api/users/@me', headers={'Authorization': f'Bearer {access_token}'}) | |
| user_json = user_response.json() | |
| username = user_json.get('username', 'Unknown') | |
| session['username'] = username | |
| create_or_update_user(username) | |
| # Initialize credit cache for this user | |
| try: | |
| initial_credits = get_user_credits(username) | |
| update_credit_cache(username, initial_credits) | |
| except: | |
| # If database fails during auth, user will need to refresh credits later | |
| pass | |
| return redirect('/dashboard') | |
| def logout(): | |
| # Clear credit cache for this user | |
| if 'username' in session: | |
| username = session.get('username') | |
| with deduction_lock: | |
| credit_cache.pop(username, None) | |
| pending_deductions.pop(username, None) | |
| session.clear() | |
| return redirect('/') | |
| def get_user_credits_api(): | |
| """API endpoint to get user credits and plan asynchronously""" | |
| if 'username' not in session: | |
| return jsonify({"error": "Unauthorized"}), 401 | |
| username = session.get('username', 'Unknown') | |
| # Get user data from database | |
| user = get_user(username) | |
| if not user: | |
| return jsonify({"error": "User not found"}), 404 | |
| # Return cached credits if available, otherwise use database value | |
| cached_credits = get_cached_credits(username) | |
| if username in credit_cache: | |
| credits = cached_credits | |
| else: | |
| credits = user['credits'] | |
| update_credit_cache(username, credits) | |
| return jsonify({ | |
| "credits": credits, | |
| "plan": user['plan'] | |
| }) | |
| def dashboard(): | |
| if 'username' not in session: | |
| return redirect('/') | |
| username = session.get('username', 'Unknown') | |
| key = f"sk-{hashlib.sha256(username.encode()).hexdigest()[:32]}" | |
| # Get user data from database | |
| user = get_user(username) | |
| plan = user['plan'] if user else 'Free' | |
| try: | |
| response = app.test_client().get('/v1/models') | |
| models_data = json.loads(response.data.decode('utf-8')) | |
| models = models_data['data'] | |
| except Exception as e: | |
| models = [] | |
| return f''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <title>Dashboard - GoAPI</title> | |
| <style> | |
| body {{ | |
| font-family: 'Inter', sans-serif; | |
| max-width: 600px; | |
| margin: 50px auto; | |
| padding: 30px; | |
| text-align: center; | |
| background: #f8f9fa; | |
| border-radius: 15px; | |
| border: 2px solid #000; | |
| }} | |
| .username {{ | |
| font-size: 28px; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 10px; | |
| }} | |
| .plan-info {{ | |
| font-size: 16px; | |
| color: #666; | |
| margin-bottom: 10px; | |
| font-weight: 500; | |
| }} | |
| .credits-info {{ | |
| font-size: 16px; | |
| color: #28a745; | |
| margin-bottom: 20px; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| }} | |
| .refresh-btn {{ | |
| background: none; | |
| border: 1px solid #28a745; | |
| color: #28a745; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| transition: all 0.3s ease; | |
| }} | |
| .refresh-btn:hover {{ | |
| background: #28a745; | |
| color: white; | |
| }} | |
| .key-container {{ | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| gap: 10px; | |
| }} | |
| .key {{ | |
| font-family: monospace; | |
| font-size: 16px; | |
| color: #666; | |
| background: #fff; | |
| padding: 10px; | |
| border-radius: 5px; | |
| border: 1px solid #ddd; | |
| flex: 1; | |
| word-break: break-all; | |
| }} | |
| .copy-btn {{ | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| padding: 10px 15px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background 0.3s ease; | |
| }} | |
| .copy-btn:hover {{ | |
| background: #0056b3; | |
| }} | |
| .copy-btn.copied {{ | |
| background: #28a745; | |
| }} | |
| .logout-btn {{ | |
| background: #dc3545; | |
| color: white; | |
| border: none; | |
| padding: 12px 30px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| text-decoration: none; | |
| display: inline-block; | |
| transition: background 0.3s ease; | |
| }} | |
| .logout-btn i {{ | |
| margin-right: 8px; | |
| }} | |
| .chat-btn {{ | |
| background: #28a745; | |
| color: white; | |
| border: none; | |
| padding: 12px 30px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| text-decoration: none; | |
| display: inline-block; | |
| transition: background 0.3s ease; | |
| margin-right: 15px; | |
| }} | |
| .chat-btn:hover {{ | |
| background: #218838; | |
| }} | |
| .chat-btn i {{ | |
| margin-right: 8px; | |
| }} | |
| .chat-modal {{ | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0,0,0,0.8); | |
| }} | |
| .chat-modal-content {{ | |
| background-color: #fff; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 90%; | |
| max-width: 800px; | |
| height: 80%; | |
| border-radius: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| }} | |
| .chat-header {{ | |
| padding: 20px; | |
| border-bottom: 1px solid #e9ecef; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| }} | |
| .chat-header h3 {{ | |
| margin: 0; | |
| color: #333; | |
| }} | |
| .close-chat {{ | |
| background: none; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: #666; | |
| padding: 0; | |
| width: 30px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| }} | |
| .chat-controls {{ | |
| padding: 15px 20px; | |
| border-bottom: 1px solid #e9ecef; | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| }} | |
| .model-select {{ | |
| padding: 8px 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| min-width: 200px; | |
| }} | |
| .chat-messages {{ | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| }} | |
| .message {{ | |
| margin-bottom: 15px; | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| max-width: 70%; | |
| word-wrap: break-word; | |
| }} | |
| .message.user {{ | |
| background: #007bff; | |
| color: white; | |
| }} | |
| .message.assistant {{ | |
| background: #e9ecef; | |
| color: #333; | |
| }} | |
| .chat-input-area {{ | |
| padding: 20px; | |
| border-top: 1px solid #e9ecef; | |
| display: flex; | |
| gap: 10px; | |
| }} | |
| .chat-input {{ | |
| flex: 1; | |
| padding: 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-size: 16px; | |
| font-family: 'Inter', sans-serif; | |
| resize: none; | |
| }} | |
| .send-btn {{ | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background 0.3s ease; | |
| }} | |
| .send-btn:hover {{ | |
| background: #0056b3; | |
| }} | |
| .send-btn:disabled {{ | |
| background: #ccc; | |
| cursor: not-allowed; | |
| }} | |
| .modal {{ | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0,0,0,0.5); | |
| }} | |
| .modal-content {{ | |
| background-color: #fff; | |
| margin: 15% auto; | |
| padding: 30px; | |
| border-radius: 10px; | |
| width: 90%; | |
| max-width: 400px; | |
| text-align: center; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
| }} | |
| .modal h3 {{ | |
| margin-top: 0; | |
| color: #333; | |
| }} | |
| .modal-buttons {{ | |
| display: flex; | |
| gap: 15px; | |
| justify-content: center; | |
| margin-top: 25px; | |
| }} | |
| .modal-btn {{ | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background 0.3s ease; | |
| }} | |
| .confirm-btn {{ | |
| background: #dc3545; | |
| color: white; | |
| }} | |
| .confirm-btn:hover {{ | |
| background: #c82333; | |
| }} | |
| .cancel-btn {{ | |
| background: #6c757d; | |
| color: white; | |
| }} | |
| .cancel-btn:hover {{ | |
| background: #5a6268; | |
| }} | |
| .models-section {{ | |
| margin-top: 40px; | |
| text-align: left; | |
| }} | |
| .models-section h3 {{ | |
| color: #333; | |
| margin-bottom: 20px; | |
| font-size: 24px; | |
| }} | |
| .search-container {{ | |
| margin-bottom: 20px; | |
| }} | |
| .search-input {{ | |
| width: 100%; | |
| padding: 12px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .search-input:focus {{ | |
| outline: none; | |
| border-color: #007bff; | |
| }} | |
| .models-list {{ | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| }} | |
| .model-item {{ | |
| background: #f8f9fa; | |
| border: 1px solid #e9ecef; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| transition: background 0.3s ease; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| }} | |
| .model-item:hover {{ | |
| background: #e9ecef; | |
| }} | |
| .model-info {{ | |
| flex: 1; | |
| }} | |
| .model-name {{ | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| }} | |
| .model-id {{ | |
| color: #666; | |
| font-family: monospace; | |
| font-size: 14px; | |
| }} | |
| .model-multiplier {{ | |
| font-weight: 600; | |
| color: #007bff; | |
| font-size: 14px; | |
| background: #e7f3ff; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| }} | |
| .no-models {{ | |
| text-align: center; | |
| color: #666; | |
| padding: 40px; | |
| font-style: italic; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="username">{username}</div> | |
| <div class="plan-info">Plan: {plan.title()}</div> | |
| <div class="credits-info" id="creditsDisplay"> | |
| Credits: <span id="creditsValue">Loading...</span> | |
| <button onclick="loadCredits()" class="refresh-btn" title="Refresh Credits"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| </div> | |
| <div class="key-container"> | |
| <div class="key" id="api-key">{key}</div> | |
| <button class="copy-btn" onclick="copyToClipboard()"><i class="fas fa-copy"></i></button> | |
| </div> | |
| <div class="models-section"> | |
| <h3>Available Models</h3> | |
| <div class="search-container"> | |
| <input type="text" class="search-input" id="searchInput" placeholder="Search models..." onkeyup="filterModels()"> | |
| </div> | |
| <ul class="models-list" id="modelsList"> | |
| {"".join([f'<li class="model-item"><div class="model-info"><div class="model-name">{model["id"].replace("-", " ").title().replace("Gpt", "GPT")}</div><div class="model-id">{model["id"]}</div></div><div class="model-multiplier">{MODEL_MULTIPLIERS.get(model["id"], 1.0):.1f}x</div></li>' for model in models])} | |
| </ul> | |
| {"<div class='no-models'>No models found</div>" if not models else ""} | |
| </div> | |
| <button onclick="showChatModal()" class="chat-btn"><i class="fas fa-comments"></i> Chat</button> | |
| <button onclick="showLogoutModal()" class="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</button> | |
| <div id="logoutModal" class="modal"> | |
| <div class="modal-content"> | |
| <h3>Confirm Logout</h3> | |
| <p>Are you sure you want to logout?</p> | |
| <div class="modal-buttons"> | |
| <button onclick="confirmLogout()" class="modal-btn confirm-btn">Logout</button> | |
| <button onclick="hideLogoutModal()" class="modal-btn cancel-btn">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="chatModal" class="chat-modal"> | |
| <div class="chat-modal-content"> | |
| <div class="chat-header"> | |
| <h3>Chat with AI</h3> | |
| <button onclick="hideChatModal()" class="close-chat">×</button> | |
| </div> | |
| <div class="chat-controls"> | |
| <select class="model-select" id="modelSelect"> | |
| {"".join([f'<option value="{model["id"]}">{model["id"].replace("-", " ").title().replace("Gpt", "GPT")}</option>' for model in models])} | |
| </select> | |
| </div> | |
| <div class="chat-messages" id="chatMessages"></div> | |
| <div class="chat-input-area"> | |
| <textarea class="chat-input" id="chatInput" placeholder="Type your message..." rows="1" onkeypress="handleKeyPress(event)"></textarea> | |
| <button onclick="sendMessage()" class="send-btn" id="sendBtn">Send</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| function copyToClipboard() {{ | |
| const keyElement = document.getElementById('api-key'); | |
| const keyText = keyElement.textContent; | |
| navigator.clipboard.writeText(keyText).then(() => {{ | |
| const copyBtn = document.querySelector('.copy-btn'); | |
| copyBtn.innerHTML = '<i class="fas fa-check"></i>'; | |
| copyBtn.classList.add('copied'); | |
| setTimeout(() => {{ | |
| copyBtn.innerHTML = '<i class="fas fa-copy"></i>'; | |
| copyBtn.classList.remove('copied'); | |
| }}, 2000); | |
| }}); | |
| }} | |
| function showLogoutModal() {{ | |
| document.getElementById('logoutModal').style.display = 'block'; | |
| }} | |
| function hideLogoutModal() {{ | |
| document.getElementById('logoutModal').style.display = 'none'; | |
| }} | |
| function confirmLogout() {{ | |
| window.location.href = '/logout'; | |
| }} | |
| window.onclick = function(event) {{ | |
| const modal = document.getElementById('logoutModal'); | |
| if (event.target == modal) {{ | |
| hideLogoutModal(); | |
| }} | |
| }} | |
| async function loadCredits() {{ | |
| try {{ | |
| const response = await fetch('/api/user/credits'); | |
| if (response.ok) {{ | |
| const data = await response.json(); | |
| document.getElementById('creditsValue').textContent = data.credits.toLocaleString(); | |
| }} else {{ | |
| document.getElementById('creditsValue').textContent = 'Error loading credits'; | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error loading credits:', error); | |
| document.getElementById('creditsValue').textContent = 'Error loading credits'; | |
| }} | |
| }} | |
| // Load credits when page loads | |
| document.addEventListener('DOMContentLoaded', loadCredits); | |
| function filterModels() {{ | |
| const input = document.getElementById('searchInput'); | |
| const filter = input.value.toLowerCase(); | |
| const modelsList = document.getElementById('modelsList'); | |
| const models = modelsList.getElementsByTagName('li'); | |
| for (let i = 0; i < models.length; i++) {{ | |
| const modelName = models[i].getElementsByClassName('model-name')[0]; | |
| const modelId = models[i].getElementsByClassName('model-id')[0]; | |
| const nameText = modelName.textContent || modelName.innerText; | |
| const idText = modelId.textContent || modelId.innerText; | |
| if (nameText.toLowerCase().indexOf(filter) > -1 || idText.toLowerCase().indexOf(filter) > -1) {{ | |
| models[i].style.display = ''; | |
| }} else {{ | |
| models[i].style.display = 'none'; | |
| }} | |
| }} | |
| }} | |
| let messages = []; | |
| function showChatModal() {{ | |
| document.getElementById('chatModal').style.display = 'block'; | |
| document.getElementById('chatInput').focus(); | |
| }} | |
| function hideChatModal() {{ | |
| document.getElementById('chatModal').style.display = 'none'; | |
| }} | |
| function handleKeyPress(event) {{ | |
| if (event.key === 'Enter' && !event.shiftKey) {{ | |
| event.preventDefault(); | |
| sendMessage(); | |
| }} | |
| }} | |
| async function sendMessage(retryCount = 0) {{ | |
| const input = document.getElementById('chatInput'); | |
| const message = input.value.trim(); | |
| if (!message && retryCount === 0) return; // Only check for empty message on first attempt | |
| const modelSelect = document.getElementById('modelSelect'); | |
| const model = modelSelect.value; | |
| const apiKey = document.getElementById('api-key').textContent; | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const originalText = sendBtn.textContent; | |
| sendBtn.disabled = true; | |
| sendBtn.textContent = 'Sending...'; | |
| // Add user message to conversation (only on first attempt) | |
| if (retryCount === 0) {{ | |
| messages.push({{role: 'user', content: message}}); | |
| addMessage(message, 'user'); | |
| input.value = ''; | |
| }} | |
| try {{ | |
| const messagesData = messages.map(msg => ({{ | |
| role: msg.role, | |
| content: msg.content | |
| }})); | |
| const response = await fetch('/v1/chat/completions', {{ | |
| method: 'POST', | |
| headers: {{ | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${{apiKey}}` | |
| }}, | |
| body: JSON.stringify({{ | |
| model: model, | |
| messages: messagesData, | |
| stream: true | |
| }}) | |
| }}); | |
| if (!response.ok) {{ | |
| throw new Error(`HTTP error! status: ${{response.status}}`); | |
| }} | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let assistantMessage = ''; | |
| while (true) {{ | |
| const {{done, value}} = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split('\\n'); | |
| for (const line of lines) {{ | |
| if (line.startsWith('data: ')) {{ | |
| const data = line.slice(6); | |
| if (data === '[DONE]') {{ | |
| // Check if we got actual content or just [DONE] | |
| if (!assistantMessage.trim()) {{ | |
| console.log('Received empty response with [DONE], retrying...'); | |
| if (retryCount < 3) {{ | |
| console.log(`Retry attempt ${{retryCount + 1}}/3`); | |
| // Wait a bit before retrying | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| return sendMessage(retryCount + 1); | |
| }} else {{ | |
| console.log('Max retries reached, showing error'); | |
| addMessage('Request failed after multiple retries. Please try again.', 'assistant'); | |
| messages.push({{role: 'assistant', content: 'Request failed after multiple retries. Please try again.'}}); | |
| return; | |
| }} | |
| }} | |
| // Add complete assistant message to conversation | |
| messages.push({{role: 'assistant', content: assistantMessage}}); | |
| break; | |
| }} | |
| try {{ | |
| const parsed = JSON.parse(data); | |
| const content = parsed.choices[0]?.delta?.content; | |
| if (content) {{ | |
| assistantMessage += content; | |
| updateLastMessage(assistantMessage, 'assistant'); | |
| }} | |
| }} catch (e) {{ | |
| console.log('Parse error:', e); | |
| }} | |
| }} | |
| }} | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error:', error); | |
| const errorMsg = 'Sorry, there was an error processing your request.'; | |
| addMessage(errorMsg, 'assistant'); | |
| messages.push({{role: 'assistant', content: errorMsg}}); | |
| }} finally {{ | |
| sendBtn.disabled = false; | |
| sendBtn.textContent = originalText; | |
| }} | |
| }} | |
| function addMessage(content, type) {{ | |
| const messagesDiv = document.getElementById('chatMessages'); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${{type}}`; | |
| messageDiv.textContent = content; | |
| messagesDiv.appendChild(messageDiv); | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| }} | |
| function updateLastMessage(content, type) {{ | |
| const messagesDiv = document.getElementById('chatMessages'); | |
| let lastMessage = messagesDiv.lastElementChild; | |
| if (!lastMessage || !lastMessage.classList.contains(type)) {{ | |
| lastMessage = document.createElement('div'); | |
| lastMessage.className = `message ${{type}}`; | |
| messagesDiv.appendChild(lastMessage); | |
| }} | |
| lastMessage.textContent = content; | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| def hello_world(): | |
| try: | |
| response = app.test_client().get('/v1/models') | |
| models_data = json.loads(response.data.decode('utf-8')) | |
| model_items = [] | |
| for model in models_data['data']: | |
| display_name = model['id'].replace('-', ' ').title().replace('Gpt', 'GPT') | |
| model_items.append(f'<li>{display_name}</li>') | |
| models_html = '\n '.join(model_items) | |
| except Exception as e: | |
| models_html = '<li>Error loading models</li>' | |
| return f''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <title>GoAPI</title> | |
| <style> | |
| body {{ | |
| max-width: 800px; | |
| margin: 50px auto 0 auto; | |
| padding: 20px; | |
| border: 2px solid #000; | |
| border-radius: 10px; | |
| text-align: center; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .pricing {{ | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| margin: 30px 0; | |
| text-align: center; | |
| }} | |
| .plan {{ | |
| padding: 20px; | |
| border: 1px solid #ccc; | |
| border-radius: 5px; | |
| }} | |
| .plan h3 {{ | |
| margin: 0 0 10px 0; | |
| }} | |
| .price {{ | |
| font-size: 24px; | |
| font-weight: bold; | |
| margin: 10px 0; | |
| }} | |
| .features {{ | |
| font-size: 14px; | |
| color: #666; | |
| margin: 10px 0; | |
| }} | |
| ul {{ | |
| border: 1px solid #ccc; | |
| border-radius: 5px; | |
| padding: 10px; | |
| list-style-type: none; | |
| display: inline-block; | |
| }} | |
| li {{ | |
| border-bottom: 1px solid #eee; | |
| padding: 5px 0; | |
| }} | |
| li:last-child {{ | |
| border-bottom: none; | |
| }} | |
| a:hover {{ | |
| background-color: #333; | |
| color: white; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>GoAPI</h1> | |
| </div> | |
| <a href="/auth" style="background-color: transparent; color: #333; border: 2px solid #333; padding: 10px 20px; border-radius: 0; font-size: 16px; cursor: pointer; margin: 20px 0; transition: all 0.2s ease; text-decoration: none; display: inline-block;">Get Started</a> | |
| <h2>Model List</h2> | |
| <ul> | |
| {models_html} | |
| </ul> | |
| <div class="pricing"> | |
| <div class="plan"> | |
| <h3>Free</h3> | |
| <div class="price">$0/month</div> | |
| <div class="features">500,000 daily tokens</div> | |
| <a href="/auth" style="background-color: transparent; color: #333; border: 2px solid #333; padding: 10px 20px; border-radius: 15px; font-size: 16px; cursor: pointer; margin: 20px 0; transition: all 0.2s ease; text-decoration: none; display: inline-block;">Get Started</a> | |
| </div> | |
| <div class="plan"> | |
| <h3>Explorer</h3> | |
| <div class="price">$4.99/month</div> | |
| <div class="features">5 million daily tokens</div> | |
| <button style="background-color: transparent; color: #666; border: 2px solid #ccc; padding: 10px 20px; border-radius: 15px; font-size: 16px; cursor: not-allowed; margin: 20px 0; text-decoration: none; display: inline-block;" disabled>Coming Soon</button> | |
| </div> | |
| <div class="plan"> | |
| <h3>Voyager</h3> | |
| <div class="price">$19.99/month</div> | |
| <div class="features">50 million daily tokens</div> | |
| <button style="background-color: transparent; color: #666; border: 2px solid #ccc; padding: 10px 20px; border-radius: 15px; font-size: 16px; cursor: not-allowed; margin: 20px 0; text-decoration: none; display: inline-block;" disabled>Coming Soon</button> | |
| </div> | |
| </body> | |
| </html> | |
| ''' | |
| def process_deductions_now(): | |
| """Admin endpoint to manually process pending deductions""" | |
| if request.remote_addr != '127.0.0.1': # Only allow from localhost | |
| return jsonify({"error": "Unauthorized"}), 403 | |
| # Run in background thread to avoid blocking the response | |
| threading.Thread(target=process_pending_deductions, daemon=True).start() | |
| return jsonify({"message": "Pending deductions processing started"}) | |
| def cleanup_pending_deductions(): | |
| """Process any remaining pending deductions when the app shuts down""" | |
| if pending_deductions: | |
| print("Processing remaining pending deductions on shutdown...") | |
| # Run in a separate thread to avoid blocking shutdown | |
| cleanup_thread = threading.Thread(target=process_pending_deductions, daemon=True) | |
| cleanup_thread.start() | |
| cleanup_thread.join(timeout=5) # Wait up to 5 seconds for completion | |
| if __name__ == '__main__': | |
| # Start the background timer when the app starts | |
| start_batch_timer() | |
| app.run(debug=True, port=7860, host="0.0.0.0") | |