GamerC0der commited on
Commit
e4f54f1
·
verified ·
1 Parent(s): 392e7f2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1805 -0
app.py ADDED
@@ -0,0 +1,1805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request, redirect, session, Response
2
+ import json
3
+ import requests
4
+ import os
5
+ import hashlib
6
+ import time
7
+ import psycopg2
8
+ from psycopg2.extras import RealDictCursor
9
+ from datetime import datetime, timedelta
10
+ import threading
11
+ import atexit
12
+ import uuid
13
+ from collections import defaultdict
14
+
15
+ app = Flask(__name__)
16
+ app.secret_key = 'your-secret-key-here'
17
+ DISCORD_CLIENT_ID = '1426015278499627058'
18
+ DISCORD_CLIENT_SECRET = 'YfTZg5JZA-y3GU1wDyoO3IPilwz9jXmL'
19
+ DATABASE_URL = 'postgres://avnadmin:AVNS_o0LDhRmORJQ7lh7ULQS@goapi-hacktv.b.aivencloud.com:11426/defaultdb?sslmode=require'
20
+ ADMIN_PASSWORD = "GoodGuys!Funn1"
21
+ # Global variables for batching credit deductions
22
+ pending_deductions = defaultdict(int) # username -> total_credits_to_deduct
23
+ credit_cache = {} # username -> last_known_credits_balance
24
+ deduction_lock = threading.Lock()
25
+ batch_timer = None
26
+
27
+ MODEL_MULTIPLIERS = {
28
+ 'gpt-5-nano': 1.0,
29
+ 'gpt-5-chat': 1.5,
30
+ 'gpt-4.1': 1.0,
31
+ 'claude-4-sonnet': 3.0,
32
+ 'claude-4-opus': 5.0,
33
+ 'claude-3.5-sonnet': 3.0,
34
+ 'claude-3.7-sonnet': 3.0,
35
+ 'claude-3.5-haiku': 2.0
36
+ }
37
+
38
+ def init_db():
39
+ conn = psycopg2.connect(DATABASE_URL)
40
+ c = conn.cursor()
41
+ c.execute('''CREATE TABLE IF NOT EXISTS users
42
+ (username TEXT PRIMARY KEY,
43
+ plan TEXT DEFAULT 'free',
44
+ credits INTEGER DEFAULT 500000,
45
+ last_reset TEXT)''')
46
+ conn.commit()
47
+ conn.close()
48
+
49
+ def get_user(username):
50
+ conn = psycopg2.connect(DATABASE_URL)
51
+ c = conn.cursor(cursor_factory=RealDictCursor)
52
+ c.execute('SELECT plan, credits, last_reset FROM users WHERE username = %s', (username,))
53
+ result = c.fetchone()
54
+ conn.close()
55
+
56
+ if result:
57
+ return dict(result)
58
+ return None
59
+
60
+ def create_or_update_user(username, plan='free'):
61
+ conn = psycopg2.connect(DATABASE_URL)
62
+ c = conn.cursor()
63
+ now = datetime.now().isoformat()
64
+
65
+ c.execute('''INSERT INTO users (username, plan, credits, last_reset)
66
+ VALUES (%s, %s, 500000, %s)
67
+ ON CONFLICT (username)
68
+ DO UPDATE SET plan = EXCLUDED.plan, credits = EXCLUDED.credits, last_reset = EXCLUDED.last_reset''',
69
+ (username, plan, now))
70
+ conn.commit()
71
+ conn.close()
72
+
73
+ def reset_credits_if_needed(username):
74
+ user = get_user(username)
75
+ if not user:
76
+ return
77
+
78
+ last_reset = user['last_reset']
79
+ if last_reset:
80
+ last_reset_date = datetime.fromisoformat(last_reset).date()
81
+ today = datetime.now().date()
82
+
83
+ if last_reset_date < today:
84
+ conn = psycopg2.connect(DATABASE_URL)
85
+ c = conn.cursor()
86
+ now = datetime.now().isoformat()
87
+ c.execute('UPDATE users SET credits = 500000, last_reset = %s WHERE username = %s',
88
+ (now, username))
89
+ conn.commit()
90
+ conn.close()
91
+
92
+ def deduct_credits(username, amount):
93
+ conn = psycopg2.connect(DATABASE_URL)
94
+ c = conn.cursor()
95
+ c.execute('UPDATE users SET credits = credits - %s WHERE username = %s AND credits >= %s',
96
+ (amount, username, amount))
97
+ success = c.rowcount > 0
98
+ conn.commit()
99
+ conn.close()
100
+ return success
101
+
102
+ def get_user_credits(username):
103
+ user = get_user(username)
104
+ if user:
105
+ reset_credits_if_needed(username)
106
+ user = get_user(username)
107
+ return user['credits'] if user else 0
108
+ return 0
109
+
110
+ def get_cached_credits(username):
111
+ """Get cached credits balance for a user (estimated: cached_balance - pending_deductions)"""
112
+ with deduction_lock:
113
+ cached_balance = credit_cache.get(username, 0)
114
+ pending_for_user = pending_deductions.get(username, 0)
115
+ return cached_balance - pending_for_user
116
+
117
+ def update_credit_cache(username, new_balance):
118
+ """Update the cached credit balance for a user"""
119
+ with deduction_lock:
120
+ credit_cache[username] = new_balance
121
+ print(f"Updated credit cache for {username}: {new_balance} credits")
122
+
123
+ def clear_cache_entry(username):
124
+ """Clear cache entry for a user (called when they run out of credits)"""
125
+ with deduction_lock:
126
+ credit_cache.pop(username, None)
127
+ print(f"Cleared credit cache for {username}")
128
+
129
+ def add_pending_deduction(username, amount):
130
+ """Add a credit deduction to the pending queue"""
131
+ with deduction_lock:
132
+ pending_deductions[username] += amount
133
+
134
+ def process_pending_deductions():
135
+ """Process all pending credit deductions in a single batch (runs in background thread)"""
136
+ try:
137
+ with deduction_lock:
138
+ if not pending_deductions:
139
+ return
140
+
141
+ # Create a copy of pending deductions to process
142
+ deductions_to_process = dict(pending_deductions)
143
+ pending_deductions.clear()
144
+
145
+ # Process deductions in database
146
+ if deductions_to_process:
147
+ conn = psycopg2.connect(DATABASE_URL)
148
+ c = conn.cursor()
149
+
150
+ for username, amount in deductions_to_process.items():
151
+ c.execute('UPDATE users SET credits = credits - %s WHERE username = %s AND credits >= %s',
152
+ (amount, username, amount))
153
+
154
+ # Get updated balances for all affected users
155
+ usernames = list(deductions_to_process.keys())
156
+ if usernames:
157
+ placeholders = ','.join(['%s'] * len(usernames))
158
+ c.execute(f'SELECT username, credits FROM users WHERE username IN ({placeholders})', usernames)
159
+
160
+ for row in c.fetchall():
161
+ username, new_balance = row
162
+ update_credit_cache(username, new_balance)
163
+
164
+ conn.commit()
165
+ conn.close()
166
+ print(f"Processed {len(deductions_to_process)} credit deductions")
167
+ except Exception as e:
168
+ print(f"Error processing credit deductions: {e}")
169
+ # Put deductions back in queue if there was an error
170
+ with deduction_lock:
171
+ for username, amount in deductions_to_process.items():
172
+ pending_deductions[username] += amount
173
+
174
+ def start_batch_timer():
175
+ """Start or restart the 3-minute batch timer"""
176
+ global batch_timer
177
+
178
+ if batch_timer:
179
+ batch_timer.cancel()
180
+
181
+ batch_timer = threading.Timer(180.0, lambda: threading.Thread(target=process_pending_deductions, daemon=True).start()) # 3 minutes = 180 seconds
182
+ batch_timer.daemon = True
183
+ batch_timer.start()
184
+
185
+ def verify_admin_password(password):
186
+ """Verify admin password"""
187
+ return password == ADMIN_PASSWORD
188
+
189
+ def export_database():
190
+ """Export current database to JSON"""
191
+ try:
192
+ conn = psycopg2.connect(DATABASE_URL)
193
+ c = conn.cursor(cursor_factory=RealDictCursor)
194
+
195
+ # Get all users
196
+ c.execute('SELECT * FROM users')
197
+ users = c.fetchall()
198
+
199
+ # Convert to list of dicts for JSON serialization
200
+ users_data = [dict(user) for user in users]
201
+
202
+ conn.close()
203
+ return {"users": users_data, "exported_at": datetime.now().isoformat()}
204
+
205
+ except Exception as e:
206
+ return {"error": f"Failed to export database: {str(e)}"}
207
+
208
+ def import_database(data):
209
+ """Import database from JSON data"""
210
+ try:
211
+ if "users" not in data:
212
+ return {"error": "Invalid data format: missing 'users' key"}
213
+
214
+ conn = psycopg2.connect(DATABASE_URL)
215
+ c = conn.cursor()
216
+
217
+ # Clear existing data
218
+ c.execute('DELETE FROM users')
219
+
220
+ # Insert new data
221
+ for user_data in data["users"]:
222
+ c.execute('''INSERT INTO users (username, plan, credits, last_reset)
223
+ VALUES (%s, %s, %s, %s)''',
224
+ (user_data['username'], user_data.get('plan', 'free'),
225
+ user_data.get('credits', 500000), user_data.get('last_reset')))
226
+
227
+ conn.commit()
228
+ conn.close()
229
+
230
+ # Update credit cache for all imported users
231
+ for user_data in data["users"]:
232
+ update_credit_cache(user_data['username'], user_data.get('credits', 500000))
233
+
234
+ return {"success": f"Imported {len(data['users'])} users"}
235
+
236
+ except Exception as e:
237
+ return {"error": f"Failed to import database: {str(e)}"}
238
+
239
+ init_db()
240
+
241
+ def get_all_users():
242
+ """Get all users from database"""
243
+ try:
244
+ conn = psycopg2.connect(DATABASE_URL)
245
+ c = conn.cursor(cursor_factory=RealDictCursor)
246
+ c.execute('SELECT * FROM users ORDER BY username')
247
+ users = c.fetchall()
248
+ conn.close()
249
+ return [dict(user) for user in users]
250
+ except Exception as e:
251
+ return []
252
+
253
+ @app.route('/db/admin', methods=['GET', 'POST'])
254
+ def db_admin():
255
+ """Database admin interface"""
256
+ if request.method == 'GET':
257
+ users = get_all_users()
258
+
259
+ # Generate user management table
260
+ users_html = ""
261
+ for user in users:
262
+ users_html += f'''
263
+ <tr>
264
+ <td>{user['username']}</td>
265
+ <td>
266
+ <div class="plan-container">
267
+ <select class="plan-select" data-username="{user['username']}">
268
+ <option value="free" {'selected' if user['plan'] == 'free' else ''}>Free</option>
269
+ <option value="premium" {'selected' if user['plan'] == 'premium' else ''}>Premium</option>
270
+ <option value="pro" {'selected' if user['plan'] == 'pro' else ''}>Pro</option>
271
+ <option value="custom">Custom...</option>
272
+ </select>
273
+ <input type="text" class="plan-custom-input" data-username="{user['username']}"
274
+ placeholder="Enter custom plan" style="display: none; margin-top: 5px;">
275
+ </div>
276
+ </td>
277
+ <td>{user['credits']:,}</td>
278
+ <td>
279
+ <input type="number" class="tokens-input" data-username="{user['username']}"
280
+ value="{user['credits']}" min="0" max="10000000" step="1000">
281
+ </td>
282
+ <td>
283
+ <button class="update-btn" data-username="{user['username']}">Update</button>
284
+ </td>
285
+ </tr>
286
+ '''
287
+
288
+ return f'''
289
+ <!DOCTYPE html>
290
+ <html>
291
+ <head>
292
+ <title>Database Admin</title>
293
+ <style>
294
+ body {{ font-family: Arial, sans-serif; max-width: 1200px; margin: 50px auto; padding: 20px; }}
295
+ .form-group {{ margin-bottom: 20px; }}
296
+ label {{ display: block; margin-bottom: 5px; font-weight: bold; }}
297
+ input[type="password"], input[type="file"] {{ width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }}
298
+ input[type="text"] {{ width: 300px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }}
299
+ button {{ background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 2px; }}
300
+ button:hover {{ background: #0056b3; }}
301
+ button.update-btn {{ background: #28a745; }}
302
+ button.update-btn:hover {{ background: #218838; }}
303
+ .error {{ color: red; margin-top: 10px; }}
304
+ .success {{ color: green; margin-top: 10px; }}
305
+ .section {{ margin-bottom: 40px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }}
306
+ .user-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
307
+ .user-table th, .user-table td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
308
+ .user-table th {{ background-color: #f2f2f2; }}
309
+ .user-table input {{ width: 120px; }}
310
+ .plan-container {{ display: flex; flex-direction: column; gap: 5px; }}
311
+ .plan-custom-input {{ width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; }}
312
+ .search-box {{ margin-bottom: 20px; }}
313
+ .status {{ margin-top: 10px; padding: 10px; border-radius: 4px; }}
314
+ .status.success {{ background-color: #d4edda; color: #155724; }}
315
+ .status.error {{ background-color: #f8d7da; color: #721c24; }}
316
+ </style>
317
+ </head>
318
+ <body>
319
+ <h1>Database Administration</h1>
320
+
321
+ <div class="section">
322
+ <h2>User Management</h2>
323
+ <div class="search-box">
324
+ <input type="text" id="searchInput" placeholder="Search users..." onkeyup="filterUsers()">
325
+ </div>
326
+ <table class="user-table" id="usersTable">
327
+ <thead>
328
+ <tr>
329
+ <th>Username</th>
330
+ <th>Plan</th>
331
+ <th>Credits</th>
332
+ <th>Set Tokens/Day</th>
333
+ <th>Actions</th>
334
+ </tr>
335
+ </thead>
336
+ <tbody>
337
+ {users_html}
338
+ </tbody>
339
+ </table>
340
+ <div id="status" class="status" style="display: none;"></div>
341
+ </div>
342
+
343
+ <div class="section">
344
+ <h2>Export Database</h2>
345
+ <p>Download the current database as a JSON file.</p>
346
+ <form method="post" action="/db/admin/export">
347
+ <div class="form-group">
348
+ <label for="password">Admin Password:</label>
349
+ <input type="password" id="password" name="password" required>
350
+ </div>
351
+ <button type="submit">Export Database</button>
352
+ </form>
353
+ </div>
354
+
355
+ <div class="section">
356
+ <h2>Import Database</h2>
357
+ <p>Upload a JSON file to replace the current database.</p>
358
+ <form method="post" action="/db/admin/import" enctype="multipart/form-data">
359
+ <div class="form-group">
360
+ <label for="password">Admin Password:</label>
361
+ <input type="password" id="password" name="password" required>
362
+ </div>
363
+ <div class="form-group">
364
+ <label for="db_file">Database File (JSON):</label>
365
+ <input type="file" id="db_file" name="db_file" accept=".json" required>
366
+ </div>
367
+ <button type="submit">Import Database</button>
368
+ </form>
369
+ </div>
370
+
371
+ <script>
372
+ function filterUsers() {{
373
+ const input = document.getElementById('searchInput');
374
+ const filter = input.value.toLowerCase();
375
+ const table = document.getElementById('usersTable');
376
+ const rows = table.getElementsByTagName('tr');
377
+
378
+ for (let i = 1; i < rows.length; i++) {{
379
+ const cells = rows[i].getElementsByTagName('td');
380
+ if (cells.length > 0) {{
381
+ const username = cells[0].textContent.toLowerCase();
382
+ if (username.indexOf(filter) > -1) {{
383
+ rows[i].style.display = '';
384
+ }} else {{
385
+ rows[i].style.display = 'none';
386
+ }}
387
+ }}
388
+ }}
389
+ }}
390
+
391
+ function toggleCustomPlan(username) {{
392
+ const planSelect = document.querySelector(`.plan-select[data-username="${{username}}"]`);
393
+ const customInput = document.querySelector(`.plan-custom-input[data-username="${{username}}"]`);
394
+
395
+ if (planSelect.value === 'custom') {{
396
+ customInput.style.display = 'block';
397
+ customInput.focus();
398
+ }} else {{
399
+ customInput.style.display = 'none';
400
+ customInput.value = '';
401
+ }}
402
+ }}
403
+
404
+ async function updateUser(username) {{
405
+ const planSelect = document.querySelector(`.plan-select[data-username="${{username}}"]`);
406
+ const customInput = document.querySelector(`.plan-custom-input[data-username="${{username}}"]`);
407
+ const tokensInput = document.querySelector(`.tokens-input[data-username="${{username}}"]`);
408
+
409
+ let plan = planSelect.value;
410
+ if (plan === 'custom') {{
411
+ plan = customInput.value.trim();
412
+ if (!plan) {{
413
+ alert('Please enter a custom plan name');
414
+ return;
415
+ }}
416
+ }}
417
+ const tokens = parseInt(tokensInput.value);
418
+
419
+ try {{
420
+ const response = await fetch('/db/admin/update-user', {{
421
+ method: 'POST',
422
+ headers: {{
423
+ 'Content-Type': 'application/json',
424
+ }},
425
+ body: JSON.stringify({{
426
+ username: username,
427
+ plan: plan,
428
+ credits: tokens,
429
+ password: prompt('Enter admin password:')
430
+ }})
431
+ }});
432
+
433
+ const result = await response.json();
434
+ const statusDiv = document.getElementById('status');
435
+
436
+ if (response.ok) {{
437
+ statusDiv.textContent = result.success;
438
+ statusDiv.className = 'status success';
439
+ // Update the credits column
440
+ const creditsCell = document.querySelector(`.tokens-input[data-username="${{username}}"]`).parentElement.previousElementSibling;
441
+ creditsCell.textContent = tokens.toLocaleString();
442
+ }} else {{
443
+ statusDiv.textContent = result.error;
444
+ statusDiv.className = 'status error';
445
+ }}
446
+
447
+ statusDiv.style.display = 'block';
448
+ setTimeout(() => statusDiv.style.display = 'none', 3000);
449
+
450
+ }} catch (error) {{
451
+ console.error('Error updating user:', error);
452
+ const statusDiv = document.getElementById('status');
453
+ statusDiv.textContent = 'Error updating user';
454
+ statusDiv.className = 'status error';
455
+ statusDiv.style.display = 'block';
456
+ setTimeout(() => statusDiv.style.display = 'none', 3000);
457
+ }}
458
+ }}
459
+
460
+ // Add event listeners and initialize custom plans
461
+ document.addEventListener('DOMContentLoaded', function() {{
462
+ // Handle plan selection changes
463
+ const planSelects = document.querySelectorAll('.plan-select');
464
+ planSelects.forEach(select => {{
465
+ select.addEventListener('change', function() {{
466
+ const username = this.getAttribute('data-username');
467
+ toggleCustomPlan(username);
468
+ }});
469
+ }});
470
+
471
+ // Initialize custom plan inputs for existing custom plans
472
+ const usersData = {json.dumps(users)};
473
+ usersData.forEach(user => {{
474
+ if (!['free', 'premium', 'pro'].includes(user.plan)) {{
475
+ // This is a custom plan
476
+ const select = document.querySelector(`.plan-select[data-username="${{user.username}}"]`);
477
+ const customInput = document.querySelector(`.plan-custom-input[data-username="${{user.username}}"]`);
478
+ if (select && customInput) {{
479
+ select.value = 'custom';
480
+ customInput.value = user.plan;
481
+ customInput.style.display = 'block';
482
+ }}
483
+ }}
484
+ }});
485
+
486
+ // Handle update buttons
487
+ const updateBtns = document.querySelectorAll('.update-btn');
488
+ updateBtns.forEach(btn => {{
489
+ btn.addEventListener('click', function() {{
490
+ const username = this.getAttribute('data-username');
491
+ updateUser(username);
492
+ }});
493
+ }});
494
+ }});
495
+ </script>
496
+ </body>
497
+ </html>
498
+ '''
499
+
500
+ return redirect('/db/admin')
501
+
502
+ @app.route('/db/admin/export', methods=['POST'])
503
+ def db_admin_export():
504
+ """Export database as JSON file"""
505
+ password = request.form.get('password')
506
+
507
+ if not verify_admin_password(password):
508
+ return jsonify({"error": "Invalid admin password"}), 401
509
+
510
+ data = export_database()
511
+
512
+ if "error" in data:
513
+ return jsonify(data), 500
514
+
515
+ # Create response with JSON data
516
+ response = Response(
517
+ json.dumps(data, indent=2),
518
+ mimetype='application/json',
519
+ headers={'Content-disposition': 'attachment; filename=database_export.json'}
520
+ )
521
+ return response
522
+
523
+ @app.route('/db/admin/import', methods=['POST'])
524
+ def db_admin_import():
525
+ """Import database from JSON file"""
526
+ password = request.form.get('password')
527
+
528
+ if not verify_admin_password(password):
529
+ return jsonify({"error": "Invalid admin password"}), 401
530
+
531
+ if 'db_file' not in request.files:
532
+ return jsonify({"error": "No file provided"}), 400
533
+
534
+ file = request.files['db_file']
535
+ if file.filename == '':
536
+ return jsonify({"error": "No file selected"}), 400
537
+
538
+ if not file.filename.endswith('.json'):
539
+ return jsonify({"error": "File must be a JSON file"}), 400
540
+
541
+ try:
542
+ file_content = file.read().decode('utf-8')
543
+ data = json.loads(file_content)
544
+
545
+ result = import_database(data)
546
+
547
+ if "error" in result:
548
+ return jsonify(result), 500
549
+
550
+ return jsonify(result)
551
+
552
+ except json.JSONDecodeError:
553
+ return jsonify({"error": "Invalid JSON file"}), 400
554
+ except Exception as e:
555
+ return jsonify({"error": f"Import failed: {str(e)}"}), 500
556
+
557
+ @app.route('/db/admin/update-user', methods=['POST'])
558
+ def db_admin_update_user():
559
+ """Update user plan and credits"""
560
+ try:
561
+ data = request.get_json()
562
+ if not data:
563
+ return jsonify({"error": "No data provided"}), 400
564
+
565
+ password = data.get('password')
566
+ if not verify_admin_password(password):
567
+ return jsonify({"error": "Invalid admin password"}), 401
568
+
569
+ username = data.get('username')
570
+ plan = data.get('plan')
571
+ credits = data.get('credits')
572
+
573
+ if not username or not plan or credits is None:
574
+ return jsonify({"error": "Missing required fields"}), 400
575
+
576
+ # Update user in database
577
+ conn = psycopg2.connect(DATABASE_URL)
578
+ c = conn.cursor()
579
+
580
+ # Update plan and credits, reset last_reset to today
581
+ now = datetime.now().isoformat()
582
+ c.execute('''UPDATE users
583
+ SET plan = %s, credits = %s, last_reset = %s
584
+ WHERE username = %s''',
585
+ (plan, credits, now, username))
586
+
587
+ if c.rowcount == 0:
588
+ conn.close()
589
+ return jsonify({"error": "User not found"}), 404
590
+
591
+ conn.commit()
592
+ conn.close()
593
+
594
+ # Update credit cache
595
+ update_credit_cache(username, credits)
596
+
597
+ return jsonify({"success": f"Updated {username}: {plan} plan, {credits:,} credits"})
598
+
599
+ except Exception as e:
600
+ return jsonify({"error": f"Update failed: {str(e)}"}), 500
601
+
602
+ @app.route('/test_stream')
603
+ def test_stream():
604
+ """Test endpoint to check upstream API streaming behavior with longer prompt"""
605
+ import time
606
+
607
+ # Longer test message for better streaming analysis
608
+ test_data = {
609
+ "model": "gpt-5",
610
+ "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."}],
611
+ "stream": True,
612
+ "max_tokens": 500
613
+ }
614
+
615
+ headers = {
616
+ 'Content-Type': 'application/json',
617
+ 'Authorization': 'Bearer test-key' # This might not work, but for testing
618
+ }
619
+
620
+ try:
621
+ start_time = time.time()
622
+ print(f"Starting test stream request at {start_time}")
623
+
624
+ response = requests.post(
625
+ 'https://ch.at/v1/chat/completions',
626
+ json=test_data,
627
+ headers=headers,
628
+ stream=True,
629
+ timeout=60 # Longer timeout for novel generation
630
+ )
631
+
632
+ if response.status_code != 200:
633
+ return jsonify({
634
+ "error": f"HTTP {response.status_code}",
635
+ "message": response.text,
636
+ "total_time": time.time() - start_time
637
+ }), response.status_code
638
+
639
+ chunk_count = 0
640
+ first_chunk_time = None
641
+ last_chunk_time = None
642
+ total_response = ""
643
+
644
+ for chunk in response.iter_content(chunk_size=8192):
645
+ if chunk:
646
+ chunk_count += 1
647
+ current_time = time.time()
648
+
649
+ if first_chunk_time is None:
650
+ first_chunk_time = current_time
651
+ print(f"First chunk received at {current_time} ({current_time - start_time:.2f}s after start)")
652
+
653
+ last_chunk_time = current_time
654
+ chunk_str = chunk.decode('utf-8')
655
+ total_response += chunk_str
656
+
657
+ print(f"Chunk {chunk_count} at {current_time} ({current_time - start_time:.2f}s): {len(chunk_str)} chars")
658
+
659
+ end_time = time.time()
660
+ total_time = end_time - start_time
661
+ streaming_duration = last_chunk_time - first_chunk_time if first_chunk_time and last_chunk_time else 0
662
+
663
+ result = {
664
+ "total_time": round(total_time, 2),
665
+ "streaming_duration": round(streaming_duration, 2),
666
+ "chunk_count": chunk_count,
667
+ "response_length": len(total_response),
668
+ "average_chunk_interval": round(streaming_duration / max(chunk_count - 1, 1), 2) if chunk_count > 1 else 0,
669
+ "chunks_per_second": round(chunk_count / max(streaming_duration, 0.01), 2),
670
+ "streaming_efficiency": "Good" if streaming_duration > 1.0 else "Poor (likely buffered)",
671
+ "status": "success"
672
+ }
673
+
674
+ print(f"Stream test completed: {result}")
675
+
676
+ return jsonify(result)
677
+
678
+ except Exception as e:
679
+ end_time = time.time()
680
+ return jsonify({
681
+ "error": "Test failed",
682
+ "message": str(e),
683
+ "total_time": round(end_time - start_time, 2),
684
+ "status": "error"
685
+ }), 500
686
+
687
+ @app.route('/v1/models')
688
+ def models():
689
+ models_data = {
690
+ "object": "list",
691
+ "data": [
692
+ {
693
+ "id": "gpt-5-nano",
694
+ "object": "model",
695
+ "created": 1677610602,
696
+ "owned_by": "openai"
697
+ },
698
+ {
699
+ "id": "gpt-5-chat",
700
+ "object": "model",
701
+ "created": 1677610602,
702
+ "owned_by": "openai"
703
+ },
704
+ {
705
+ "id": "gpt-4.1",
706
+ "object": "model",
707
+ "created": 1677610602,
708
+ "owned_by": "openai"
709
+ },
710
+ {
711
+ "id": "claude-4-sonnet",
712
+ "object": "model",
713
+ "created": 1677610602,
714
+ "owned_by": "anthropic"
715
+ },
716
+ {
717
+ "id": "claude-4-opus",
718
+ "object": "model",
719
+ "created": 1677610602,
720
+ "owned_by": "anthropic"
721
+ },
722
+ {
723
+ "id": "claude-3.5-sonnet",
724
+ "object": "model",
725
+ "created": 1677610602,
726
+ "owned_by": "anthropic"
727
+ },
728
+ {
729
+ "id": "claude-3.7-sonnet",
730
+ "object": "model",
731
+ "created": 1677610602,
732
+ "owned_by": "anthropic"
733
+ },
734
+ {
735
+ "id": "claude-3.5-haiku",
736
+ "object": "model",
737
+ "created": 1677610602,
738
+ "owned_by": "anthropic"
739
+ }
740
+ ]
741
+ }
742
+ return jsonify(models_data)
743
+
744
+ @app.route('/v1/chat/completions', methods=['POST'])
745
+ def chat_completions():
746
+ try:
747
+ auth_header = request.headers.get('Authorization')
748
+ if not auth_header or not auth_header.startswith('Bearer '):
749
+ return jsonify({"error": "Missing or invalid Bearer token"}), 401
750
+
751
+ token = auth_header.split(' ')[1]
752
+
753
+ conn = psycopg2.connect(DATABASE_URL)
754
+ c = conn.cursor()
755
+ c.execute('SELECT username FROM users')
756
+ all_users = c.fetchall()
757
+ conn.close()
758
+
759
+ username = None
760
+ for (user,) in all_users:
761
+ expected_key = f"sk-{hashlib.sha256(user.encode()).hexdigest()[:32]}"
762
+ if token == expected_key:
763
+ username = user
764
+ break
765
+
766
+ if not username:
767
+ return jsonify({"error": "Invalid API key"}), 401
768
+
769
+ # Get cached credits (no database call)
770
+ cached_credits = get_cached_credits(username)
771
+
772
+ # If user has no cached credits, try to load from database once
773
+ if username not in credit_cache:
774
+ try:
775
+ real_credits = get_user_credits(username)
776
+ update_credit_cache(username, real_credits)
777
+ cached_credits = real_credits
778
+ except:
779
+ # If database fails, reject request
780
+ return jsonify({"error": "Unable to verify credits"}), 402
781
+
782
+ # Reject if no credits available (cached or real)
783
+ if cached_credits <= 0:
784
+ clear_cache_entry(username) # Clear cache since they're out of credits
785
+ return jsonify({"error": "Insufficient credits"}), 402
786
+
787
+ data = request.get_json()
788
+ if not data:
789
+ return jsonify({"error": "No JSON data provided"}), 400
790
+
791
+ model = data.get('model')
792
+ if not model:
793
+ return jsonify({"error": "No model specified"}), 400
794
+
795
+ original_model = model
796
+ if model == 'gpt-5-nano':
797
+ target_model = 'gpt-5-nano'
798
+ elif model == 'gpt-5-chat':
799
+ target_model = 'gpt-5'
800
+ elif model == 'gpt-4.1':
801
+ target_model = 'gpt-41'
802
+ elif model == 'claude-4-sonnet':
803
+ target_model = 'claude-4-sonnet'
804
+ elif model == 'claude-4-opus':
805
+ target_model = 'claude-4-opus'
806
+ elif model == 'claude-3.5-sonnet':
807
+ target_model = 'claude-3.5-sonnet'
808
+ elif model == 'claude-3.7-sonnet':
809
+ target_model = 'claude-3.7-sonnet'
810
+ elif model == 'claude-3.5-haiku':
811
+ target_model = 'claude-3.5-haiku'
812
+ else:
813
+ return jsonify({"error": f"Unsupported model: {model}"}), 400
814
+
815
+ data['model'] = target_model
816
+
817
+ messages = data.get('messages', [])
818
+
819
+ # Count total words in all messages (input and assistant)
820
+ input_words = sum(len(str(msg.get('content', '')).split()) for msg in messages)
821
+
822
+ # Estimate output words (roughly half the input length, min 100 words)
823
+ estimated_output_words = max(100, input_words // 2)
824
+
825
+ total_words = input_words + estimated_output_words
826
+
827
+ multiplier = MODEL_MULTIPLIERS.get(model, 1.0)
828
+ estimated_cost = int(total_words * multiplier)
829
+
830
+ # Check against cached credits (no database call)
831
+ if estimated_cost > cached_credits:
832
+ clear_cache_entry(username) # Clear cache since insufficient credits detected
833
+ return jsonify({"error": "Insufficient credits for this request"}), 402
834
+
835
+ # Add to pending deductions instead of immediate database update
836
+ add_pending_deduction(username, estimated_cost)
837
+ start_batch_timer() # Start/restart the 3-minute timer
838
+
839
+ if 'claude' in model.lower() and messages:
840
+ messages = data['messages']
841
+ if messages and messages[0]['role'] == 'system':
842
+ system_content = messages[0]['content']
843
+ data['messages'] = [
844
+ {'role': 'user', 'content': system_content},
845
+ {'role': 'assistant', 'content': system_content}
846
+ ] + messages[1:]
847
+
848
+ auth_header = request.headers.get('Authorization')
849
+ if not auth_header or not auth_header.startswith('Bearer '):
850
+ return jsonify({"error": "Missing or invalid Bearer token"}), 401
851
+
852
+ headers = {
853
+ 'Content-Type': 'application/json',
854
+ 'Authorization': auth_header
855
+ }
856
+
857
+ max_retries = 3
858
+ retry_delay = 1
859
+
860
+ for attempt in range(max_retries):
861
+ response = requests.post(
862
+ 'https://ch.at/v1/chat/completions',
863
+ json=data,
864
+ headers=headers,
865
+ stream=True
866
+ )
867
+
868
+ if response.status_code == 200:
869
+ break
870
+
871
+ if attempt < max_retries - 1:
872
+ time.sleep(retry_delay)
873
+ else:
874
+ return
875
+
876
+ # Ensure we have a valid streaming response
877
+ if not response:
878
+ return jsonify({"error": "Failed to get response from upstream API"}), 502
879
+
880
+ def generate():
881
+ # Generate a UUID for this chat completion
882
+ completion_id = f"chatcmpl-{uuid.uuid4().hex[:16]}"
883
+
884
+ try:
885
+ for chunk in response.iter_content(chunk_size=8192):
886
+ if chunk:
887
+ chunk_str = chunk.decode('utf-8', errors='replace')
888
+
889
+ # Replace the chat completion ID with our UUID
890
+ chunk_str = chunk_str.replace('"id":"chatcmpl-', f'"id":"{completion_id}"')
891
+
892
+ # Replace model IDs in the response to match the original request
893
+ if original_model == 'gpt-5-chat':
894
+ chunk_str = chunk_str.replace('"model":"gpt-5"', '"model":"gpt-5-chat"')
895
+ elif original_model == 'gpt-4.1':
896
+ chunk_str = chunk_str.replace('"model":"gpt-41"', '"model":"gpt-4.1"')
897
+
898
+ # Check if this chunk contains [DONE] - handle edge case where [DONE] might be split across chunks
899
+ if 'data: [DONE]' in chunk_str:
900
+ print("Stream ended with [DONE], triggering retry...")
901
+ # Return a special response that indicates retry is needed
902
+ return Response('data: [RETRY_REQUEST]\n\n', content_type='text/plain')
903
+
904
+ # Ensure proper SSE formatting and only yield non-empty chunks
905
+ if chunk_str.strip():
906
+ yield f"{chunk_str}\n\n".encode('utf-8')
907
+
908
+ except Exception as e:
909
+ print(f"Error in streaming: {e}")
910
+ yield b'data: {"error": "Streaming error"}\n\n'
911
+
912
+ response_obj = Response(
913
+ generate(),
914
+ content_type='text/event-stream; charset=utf-8',
915
+ status=response.status_code
916
+ )
917
+
918
+ return response_obj
919
+
920
+ except Exception as e:
921
+ return jsonify({"error": f"Internal server error: {str(e)}"}), 500
922
+
923
+ @app.route('/auth')
924
+ def auth():
925
+ redirect_uri = f"{request.scheme}://{request.host}/auth/callback"
926
+ discord_auth_url = f"https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&redirect_uri={redirect_uri}&response_type=code&scope=identify"
927
+ return redirect(discord_auth_url)
928
+
929
+ @app.route('/auth/callback')
930
+ def auth_callback():
931
+ code = request.args.get('code')
932
+ if not code:
933
+ return "Error: No authorization code provided"
934
+
935
+ redirect_uri = f"{request.scheme}://{request.host}/auth/callback"
936
+ data = {
937
+ 'client_id': DISCORD_CLIENT_ID,
938
+ 'client_secret': DISCORD_CLIENT_SECRET,
939
+ 'grant_type': 'authorization_code',
940
+ 'code': code,
941
+ 'redirect_uri': redirect_uri
942
+ }
943
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
944
+ token_response = requests.post('https://discord.com/api/oauth2/token', data=data, headers=headers)
945
+ token_json = token_response.json()
946
+
947
+ if 'access_token' not in token_json:
948
+ return "Error: Failed to get access token"
949
+
950
+ access_token = token_json['access_token']
951
+ user_response = requests.get('https://discord.com/api/users/@me', headers={'Authorization': f'Bearer {access_token}'})
952
+ user_json = user_response.json()
953
+
954
+ username = user_json.get('username', 'Unknown')
955
+ session['username'] = username
956
+ create_or_update_user(username)
957
+
958
+ # Initialize credit cache for this user
959
+ try:
960
+ initial_credits = get_user_credits(username)
961
+ update_credit_cache(username, initial_credits)
962
+ except:
963
+ # If database fails during auth, user will need to refresh credits later
964
+ pass
965
+
966
+ return redirect('/dashboard')
967
+
968
+ @app.route('/logout')
969
+ def logout():
970
+ # Clear credit cache for this user
971
+ if 'username' in session:
972
+ username = session.get('username')
973
+ with deduction_lock:
974
+ credit_cache.pop(username, None)
975
+ pending_deductions.pop(username, None)
976
+ session.clear()
977
+ return redirect('/')
978
+
979
+ @app.route('/api/user/credits')
980
+ def get_user_credits_api():
981
+ """API endpoint to get user credits and plan asynchronously"""
982
+ if 'username' not in session:
983
+ return jsonify({"error": "Unauthorized"}), 401
984
+
985
+ username = session.get('username', 'Unknown')
986
+
987
+ # Get user data from database
988
+ user = get_user(username)
989
+ if not user:
990
+ return jsonify({"error": "User not found"}), 404
991
+
992
+ # Return cached credits if available, otherwise use database value
993
+ cached_credits = get_cached_credits(username)
994
+ if username in credit_cache:
995
+ credits = cached_credits
996
+ else:
997
+ credits = user['credits']
998
+ update_credit_cache(username, credits)
999
+
1000
+ return jsonify({
1001
+ "credits": credits,
1002
+ "plan": user['plan']
1003
+ })
1004
+
1005
+ @app.route('/dashboard')
1006
+ def dashboard():
1007
+ if 'username' not in session:
1008
+ return redirect('/')
1009
+
1010
+ username = session.get('username', 'Unknown')
1011
+ key = f"sk-{hashlib.sha256(username.encode()).hexdigest()[:32]}"
1012
+
1013
+ # Get user data from database
1014
+ user = get_user(username)
1015
+ plan = user['plan'] if user else 'Free'
1016
+
1017
+ try:
1018
+ response = app.test_client().get('/v1/models')
1019
+ models_data = json.loads(response.data.decode('utf-8'))
1020
+ models = models_data['data']
1021
+ except Exception as e:
1022
+ models = []
1023
+
1024
+ return f'''
1025
+ <!DOCTYPE html>
1026
+ <html lang="en">
1027
+ <head>
1028
+ <meta charset="UTF-8">
1029
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1030
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
1031
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
1032
+ <title>Dashboard - GoAPI</title>
1033
+ <style>
1034
+ body {{
1035
+ font-family: 'Inter', sans-serif;
1036
+ max-width: 600px;
1037
+ margin: 50px auto;
1038
+ padding: 30px;
1039
+ text-align: center;
1040
+ background: #f8f9fa;
1041
+ border-radius: 15px;
1042
+ border: 2px solid #000;
1043
+ }}
1044
+ .username {{
1045
+ font-size: 28px;
1046
+ font-weight: 600;
1047
+ color: #333;
1048
+ margin-bottom: 10px;
1049
+ }}
1050
+ .plan-info {{
1051
+ font-size: 16px;
1052
+ color: #666;
1053
+ margin-bottom: 10px;
1054
+ font-weight: 500;
1055
+ }}
1056
+ .credits-info {{
1057
+ font-size: 16px;
1058
+ color: #28a745;
1059
+ margin-bottom: 20px;
1060
+ font-weight: 500;
1061
+ display: flex;
1062
+ align-items: center;
1063
+ gap: 10px;
1064
+ }}
1065
+ .refresh-btn {{
1066
+ background: none;
1067
+ border: 1px solid #28a745;
1068
+ color: #28a745;
1069
+ width: 24px;
1070
+ height: 24px;
1071
+ border-radius: 50%;
1072
+ cursor: pointer;
1073
+ display: flex;
1074
+ align-items: center;
1075
+ justify-content: center;
1076
+ font-size: 12px;
1077
+ transition: all 0.3s ease;
1078
+ }}
1079
+ .refresh-btn:hover {{
1080
+ background: #28a745;
1081
+ color: white;
1082
+ }}
1083
+ .key-container {{
1084
+ display: flex;
1085
+ align-items: center;
1086
+ margin-bottom: 30px;
1087
+ gap: 10px;
1088
+ }}
1089
+ .key {{
1090
+ font-family: monospace;
1091
+ font-size: 16px;
1092
+ color: #666;
1093
+ background: #fff;
1094
+ padding: 10px;
1095
+ border-radius: 5px;
1096
+ border: 1px solid #ddd;
1097
+ flex: 1;
1098
+ word-break: break-all;
1099
+ }}
1100
+ .copy-btn {{
1101
+ background: #007bff;
1102
+ color: white;
1103
+ border: none;
1104
+ padding: 10px 15px;
1105
+ border-radius: 5px;
1106
+ cursor: pointer;
1107
+ font-size: 14px;
1108
+ transition: background 0.3s ease;
1109
+ }}
1110
+ .copy-btn:hover {{
1111
+ background: #0056b3;
1112
+ }}
1113
+ .copy-btn.copied {{
1114
+ background: #28a745;
1115
+ }}
1116
+ .logout-btn {{
1117
+ background: #dc3545;
1118
+ color: white;
1119
+ border: none;
1120
+ padding: 12px 30px;
1121
+ border-radius: 8px;
1122
+ font-size: 16px;
1123
+ cursor: pointer;
1124
+ text-decoration: none;
1125
+ display: inline-block;
1126
+ transition: background 0.3s ease;
1127
+ }}
1128
+ .logout-btn i {{
1129
+ margin-right: 8px;
1130
+ }}
1131
+ .chat-btn {{
1132
+ background: #28a745;
1133
+ color: white;
1134
+ border: none;
1135
+ padding: 12px 30px;
1136
+ border-radius: 8px;
1137
+ font-size: 16px;
1138
+ cursor: pointer;
1139
+ text-decoration: none;
1140
+ display: inline-block;
1141
+ transition: background 0.3s ease;
1142
+ margin-right: 15px;
1143
+ }}
1144
+ .chat-btn:hover {{
1145
+ background: #218838;
1146
+ }}
1147
+ .chat-btn i {{
1148
+ margin-right: 8px;
1149
+ }}
1150
+ .chat-modal {{
1151
+ display: none;
1152
+ position: fixed;
1153
+ z-index: 1000;
1154
+ left: 0;
1155
+ top: 0;
1156
+ width: 100%;
1157
+ height: 100%;
1158
+ background-color: rgba(0,0,0,0.8);
1159
+ }}
1160
+ .chat-modal-content {{
1161
+ background-color: #fff;
1162
+ position: absolute;
1163
+ top: 50%;
1164
+ left: 50%;
1165
+ transform: translate(-50%, -50%);
1166
+ width: 90%;
1167
+ max-width: 800px;
1168
+ height: 80%;
1169
+ border-radius: 12px;
1170
+ display: flex;
1171
+ flex-direction: column;
1172
+ overflow: hidden;
1173
+ }}
1174
+ .chat-header {{
1175
+ padding: 20px;
1176
+ border-bottom: 1px solid #e9ecef;
1177
+ display: flex;
1178
+ align-items: center;
1179
+ justify-content: space-between;
1180
+ }}
1181
+ .chat-header h3 {{
1182
+ margin: 0;
1183
+ color: #333;
1184
+ }}
1185
+ .close-chat {{
1186
+ background: none;
1187
+ border: none;
1188
+ font-size: 24px;
1189
+ cursor: pointer;
1190
+ color: #666;
1191
+ padding: 0;
1192
+ width: 30px;
1193
+ height: 30px;
1194
+ display: flex;
1195
+ align-items: center;
1196
+ justify-content: center;
1197
+ }}
1198
+ .chat-controls {{
1199
+ padding: 15px 20px;
1200
+ border-bottom: 1px solid #e9ecef;
1201
+ display: flex;
1202
+ gap: 15px;
1203
+ align-items: center;
1204
+ }}
1205
+ .model-select {{
1206
+ padding: 8px 12px;
1207
+ border: 1px solid #ddd;
1208
+ border-radius: 6px;
1209
+ font-size: 14px;
1210
+ min-width: 200px;
1211
+ }}
1212
+ .chat-messages {{
1213
+ flex: 1;
1214
+ overflow-y: auto;
1215
+ padding: 20px;
1216
+ background: #f8f9fa;
1217
+ }}
1218
+ .message {{
1219
+ margin-bottom: 15px;
1220
+ padding: 12px 16px;
1221
+ border-radius: 8px;
1222
+ max-width: 70%;
1223
+ word-wrap: break-word;
1224
+ }}
1225
+ .message.user {{
1226
+ background: #007bff;
1227
+ color: white;
1228
+ }}
1229
+ .message.assistant {{
1230
+ background: #e9ecef;
1231
+ color: #333;
1232
+ }}
1233
+ .chat-input-area {{
1234
+ padding: 20px;
1235
+ border-top: 1px solid #e9ecef;
1236
+ display: flex;
1237
+ gap: 10px;
1238
+ }}
1239
+ .chat-input {{
1240
+ flex: 1;
1241
+ padding: 12px;
1242
+ border: 1px solid #ddd;
1243
+ border-radius: 6px;
1244
+ font-size: 16px;
1245
+ font-family: 'Inter', sans-serif;
1246
+ resize: none;
1247
+ }}
1248
+ .send-btn {{
1249
+ background: #007bff;
1250
+ color: white;
1251
+ border: none;
1252
+ padding: 12px 20px;
1253
+ border-radius: 6px;
1254
+ cursor: pointer;
1255
+ font-size: 16px;
1256
+ transition: background 0.3s ease;
1257
+ }}
1258
+ .send-btn:hover {{
1259
+ background: #0056b3;
1260
+ }}
1261
+ .send-btn:disabled {{
1262
+ background: #ccc;
1263
+ cursor: not-allowed;
1264
+ }}
1265
+ .modal {{
1266
+ display: none;
1267
+ position: fixed;
1268
+ z-index: 1000;
1269
+ left: 0;
1270
+ top: 0;
1271
+ width: 100%;
1272
+ height: 100%;
1273
+ background-color: rgba(0,0,0,0.5);
1274
+ }}
1275
+ .modal-content {{
1276
+ background-color: #fff;
1277
+ margin: 15% auto;
1278
+ padding: 30px;
1279
+ border-radius: 10px;
1280
+ width: 90%;
1281
+ max-width: 400px;
1282
+ text-align: center;
1283
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
1284
+ }}
1285
+ .modal h3 {{
1286
+ margin-top: 0;
1287
+ color: #333;
1288
+ }}
1289
+ .modal-buttons {{
1290
+ display: flex;
1291
+ gap: 15px;
1292
+ justify-content: center;
1293
+ margin-top: 25px;
1294
+ }}
1295
+ .modal-btn {{
1296
+ padding: 10px 20px;
1297
+ border: none;
1298
+ border-radius: 5px;
1299
+ cursor: pointer;
1300
+ font-size: 16px;
1301
+ transition: background 0.3s ease;
1302
+ }}
1303
+ .confirm-btn {{
1304
+ background: #dc3545;
1305
+ color: white;
1306
+ }}
1307
+ .confirm-btn:hover {{
1308
+ background: #c82333;
1309
+ }}
1310
+ .cancel-btn {{
1311
+ background: #6c757d;
1312
+ color: white;
1313
+ }}
1314
+ .cancel-btn:hover {{
1315
+ background: #5a6268;
1316
+ }}
1317
+ .models-section {{
1318
+ margin-top: 40px;
1319
+ text-align: left;
1320
+ }}
1321
+ .models-section h3 {{
1322
+ color: #333;
1323
+ margin-bottom: 20px;
1324
+ font-size: 24px;
1325
+ }}
1326
+ .search-container {{
1327
+ margin-bottom: 20px;
1328
+ }}
1329
+ .search-input {{
1330
+ width: 100%;
1331
+ padding: 12px;
1332
+ border: 2px solid #ddd;
1333
+ border-radius: 8px;
1334
+ font-size: 16px;
1335
+ font-family: 'Inter', sans-serif;
1336
+ }}
1337
+ .search-input:focus {{
1338
+ outline: none;
1339
+ border-color: #007bff;
1340
+ }}
1341
+ .models-list {{
1342
+ list-style: none;
1343
+ padding: 0;
1344
+ margin: 0;
1345
+ }}
1346
+ .model-item {{
1347
+ background: #f8f9fa;
1348
+ border: 1px solid #e9ecef;
1349
+ border-radius: 8px;
1350
+ padding: 15px;
1351
+ margin-bottom: 10px;
1352
+ transition: background 0.3s ease;
1353
+ display: flex;
1354
+ justify-content: space-between;
1355
+ align-items: center;
1356
+ }}
1357
+ .model-item:hover {{
1358
+ background: #e9ecef;
1359
+ }}
1360
+ .model-info {{
1361
+ flex: 1;
1362
+ }}
1363
+ .model-name {{
1364
+ font-weight: 600;
1365
+ color: #333;
1366
+ margin-bottom: 5px;
1367
+ }}
1368
+ .model-id {{
1369
+ color: #666;
1370
+ font-family: monospace;
1371
+ font-size: 14px;
1372
+ }}
1373
+ .model-multiplier {{
1374
+ font-weight: 600;
1375
+ color: #007bff;
1376
+ font-size: 14px;
1377
+ background: #e7f3ff;
1378
+ padding: 4px 8px;
1379
+ border-radius: 4px;
1380
+ }}
1381
+ .no-models {{
1382
+ text-align: center;
1383
+ color: #666;
1384
+ padding: 40px;
1385
+ font-style: italic;
1386
+ }}
1387
+ </style>
1388
+ </head>
1389
+ <body>
1390
+ <div class="username">{username}</div>
1391
+ <div class="plan-info">Plan: {plan.title()}</div>
1392
+ <div class="credits-info" id="creditsDisplay">
1393
+ Credits: <span id="creditsValue">Loading...</span>
1394
+ <button onclick="loadCredits()" class="refresh-btn" title="Refresh Credits">
1395
+ <i class="fas fa-sync-alt"></i>
1396
+ </button>
1397
+ </div>
1398
+ <div class="key-container">
1399
+ <div class="key" id="api-key">{key}</div>
1400
+ <button class="copy-btn" onclick="copyToClipboard()"><i class="fas fa-copy"></i></button>
1401
+ </div>
1402
+
1403
+ <div class="models-section">
1404
+ <h3>Available Models</h3>
1405
+ <div class="search-container">
1406
+ <input type="text" class="search-input" id="searchInput" placeholder="Search models..." onkeyup="filterModels()">
1407
+ </div>
1408
+ <ul class="models-list" id="modelsList">
1409
+ {"".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])}
1410
+ </ul>
1411
+ {"<div class='no-models'>No models found</div>" if not models else ""}
1412
+ </div>
1413
+
1414
+ <button onclick="showChatModal()" class="chat-btn"><i class="fas fa-comments"></i> Chat</button>
1415
+ <button onclick="showLogoutModal()" class="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</button>
1416
+
1417
+ <div id="logoutModal" class="modal">
1418
+ <div class="modal-content">
1419
+ <h3>Confirm Logout</h3>
1420
+ <p>Are you sure you want to logout?</p>
1421
+ <div class="modal-buttons">
1422
+ <button onclick="confirmLogout()" class="modal-btn confirm-btn">Logout</button>
1423
+ <button onclick="hideLogoutModal()" class="modal-btn cancel-btn">Cancel</button>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+
1428
+ <div id="chatModal" class="chat-modal">
1429
+ <div class="chat-modal-content">
1430
+ <div class="chat-header">
1431
+ <h3>Chat with AI</h3>
1432
+ <button onclick="hideChatModal()" class="close-chat">&times;</button>
1433
+ </div>
1434
+ <div class="chat-controls">
1435
+ <select class="model-select" id="modelSelect">
1436
+ {"".join([f'<option value="{model["id"]}">{model["id"].replace("-", " ").title().replace("Gpt", "GPT")}</option>' for model in models])}
1437
+ </select>
1438
+ </div>
1439
+ <div class="chat-messages" id="chatMessages"></div>
1440
+ <div class="chat-input-area">
1441
+ <textarea class="chat-input" id="chatInput" placeholder="Type your message..." rows="1" onkeypress="handleKeyPress(event)"></textarea>
1442
+ <button onclick="sendMessage()" class="send-btn" id="sendBtn">Send</button>
1443
+ </div>
1444
+ </div>
1445
+ </div>
1446
+
1447
+ <script>
1448
+ function copyToClipboard() {{
1449
+ const keyElement = document.getElementById('api-key');
1450
+ const keyText = keyElement.textContent;
1451
+ navigator.clipboard.writeText(keyText).then(() => {{
1452
+ const copyBtn = document.querySelector('.copy-btn');
1453
+ copyBtn.innerHTML = '<i class="fas fa-check"></i>';
1454
+ copyBtn.classList.add('copied');
1455
+ setTimeout(() => {{
1456
+ copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
1457
+ copyBtn.classList.remove('copied');
1458
+ }}, 2000);
1459
+ }});
1460
+ }}
1461
+
1462
+ function showLogoutModal() {{
1463
+ document.getElementById('logoutModal').style.display = 'block';
1464
+ }}
1465
+
1466
+ function hideLogoutModal() {{
1467
+ document.getElementById('logoutModal').style.display = 'none';
1468
+ }}
1469
+
1470
+ function confirmLogout() {{
1471
+ window.location.href = '/logout';
1472
+ }}
1473
+
1474
+ window.onclick = function(event) {{
1475
+ const modal = document.getElementById('logoutModal');
1476
+ if (event.target == modal) {{
1477
+ hideLogoutModal();
1478
+ }}
1479
+ }}
1480
+
1481
+ async function loadCredits() {{
1482
+ try {{
1483
+ const response = await fetch('/api/user/credits');
1484
+ if (response.ok) {{
1485
+ const data = await response.json();
1486
+ document.getElementById('creditsValue').textContent = data.credits.toLocaleString();
1487
+ }} else {{
1488
+ document.getElementById('creditsValue').textContent = 'Error loading credits';
1489
+ }}
1490
+ }} catch (error) {{
1491
+ console.error('Error loading credits:', error);
1492
+ document.getElementById('creditsValue').textContent = 'Error loading credits';
1493
+ }}
1494
+ }}
1495
+
1496
+ // Load credits when page loads
1497
+ document.addEventListener('DOMContentLoaded', loadCredits);
1498
+
1499
+ function filterModels() {{
1500
+ const input = document.getElementById('searchInput');
1501
+ const filter = input.value.toLowerCase();
1502
+ const modelsList = document.getElementById('modelsList');
1503
+ const models = modelsList.getElementsByTagName('li');
1504
+
1505
+ for (let i = 0; i < models.length; i++) {{
1506
+ const modelName = models[i].getElementsByClassName('model-name')[0];
1507
+ const modelId = models[i].getElementsByClassName('model-id')[0];
1508
+ const nameText = modelName.textContent || modelName.innerText;
1509
+ const idText = modelId.textContent || modelId.innerText;
1510
+
1511
+ if (nameText.toLowerCase().indexOf(filter) > -1 || idText.toLowerCase().indexOf(filter) > -1) {{
1512
+ models[i].style.display = '';
1513
+ }} else {{
1514
+ models[i].style.display = 'none';
1515
+ }}
1516
+ }}
1517
+ }}
1518
+
1519
+ let messages = [];
1520
+
1521
+ function showChatModal() {{
1522
+ document.getElementById('chatModal').style.display = 'block';
1523
+ document.getElementById('chatInput').focus();
1524
+ }}
1525
+
1526
+ function hideChatModal() {{
1527
+ document.getElementById('chatModal').style.display = 'none';
1528
+ }}
1529
+
1530
+ function handleKeyPress(event) {{
1531
+ if (event.key === 'Enter' && !event.shiftKey) {{
1532
+ event.preventDefault();
1533
+ sendMessage();
1534
+ }}
1535
+ }}
1536
+
1537
+ async function sendMessage(retryCount = 0) {{
1538
+ const input = document.getElementById('chatInput');
1539
+ const message = input.value.trim();
1540
+ if (!message && retryCount === 0) return; // Only check for empty message on first attempt
1541
+
1542
+ const modelSelect = document.getElementById('modelSelect');
1543
+ const model = modelSelect.value;
1544
+ const apiKey = document.getElementById('api-key').textContent;
1545
+
1546
+ const sendBtn = document.getElementById('sendBtn');
1547
+ const originalText = sendBtn.textContent;
1548
+ sendBtn.disabled = true;
1549
+ sendBtn.textContent = 'Sending...';
1550
+
1551
+ // Add user message to conversation (only on first attempt)
1552
+ if (retryCount === 0) {{
1553
+ messages.push({{role: 'user', content: message}});
1554
+ addMessage(message, 'user');
1555
+ input.value = '';
1556
+ }}
1557
+
1558
+ try {{
1559
+ const messagesData = messages.map(msg => ({{
1560
+ role: msg.role,
1561
+ content: msg.content
1562
+ }}));
1563
+
1564
+ const response = await fetch('/v1/chat/completions', {{
1565
+ method: 'POST',
1566
+ headers: {{
1567
+ 'Content-Type': 'application/json',
1568
+ 'Authorization': `Bearer ${{apiKey}}`
1569
+ }},
1570
+ body: JSON.stringify({{
1571
+ model: model,
1572
+ messages: messagesData,
1573
+ stream: true
1574
+ }})
1575
+ }});
1576
+
1577
+ if (!response.ok) {{
1578
+ throw new Error(`HTTP error! status: ${{response.status}}`);
1579
+ }}
1580
+
1581
+ const reader = response.body.getReader();
1582
+ const decoder = new TextDecoder();
1583
+ let assistantMessage = '';
1584
+
1585
+ while (true) {{
1586
+ const {{done, value}} = await reader.read();
1587
+ if (done) break;
1588
+
1589
+ const chunk = decoder.decode(value);
1590
+ const lines = chunk.split('\\n');
1591
+
1592
+ for (const line of lines) {{
1593
+ if (line.startsWith('data: ')) {{
1594
+ const data = line.slice(6);
1595
+ if (data === '[DONE]') {{
1596
+ // Check if we got actual content or just [DONE]
1597
+ if (!assistantMessage.trim()) {{
1598
+ console.log('Received empty response with [DONE], retrying...');
1599
+ if (retryCount < 3) {{
1600
+ console.log(`Retry attempt ${{retryCount + 1}}/3`);
1601
+ // Wait a bit before retrying
1602
+ await new Promise(resolve => setTimeout(resolve, 1000));
1603
+ return sendMessage(retryCount + 1);
1604
+ }} else {{
1605
+ console.log('Max retries reached, showing error');
1606
+ addMessage('Request failed after multiple retries. Please try again.', 'assistant');
1607
+ messages.push({{role: 'assistant', content: 'Request failed after multiple retries. Please try again.'}});
1608
+ return;
1609
+ }}
1610
+ }}
1611
+ // Add complete assistant message to conversation
1612
+ messages.push({{role: 'assistant', content: assistantMessage}});
1613
+ break;
1614
+ }}
1615
+
1616
+ try {{
1617
+ const parsed = JSON.parse(data);
1618
+ const content = parsed.choices[0]?.delta?.content;
1619
+ if (content) {{
1620
+ assistantMessage += content;
1621
+ updateLastMessage(assistantMessage, 'assistant');
1622
+ }}
1623
+ }} catch (e) {{
1624
+ console.log('Parse error:', e);
1625
+ }}
1626
+ }}
1627
+ }}
1628
+ }}
1629
+ }} catch (error) {{
1630
+ console.error('Error:', error);
1631
+ const errorMsg = 'Sorry, there was an error processing your request.';
1632
+ addMessage(errorMsg, 'assistant');
1633
+ messages.push({{role: 'assistant', content: errorMsg}});
1634
+ }} finally {{
1635
+ sendBtn.disabled = false;
1636
+ sendBtn.textContent = originalText;
1637
+ }}
1638
+ }}
1639
+
1640
+ function addMessage(content, type) {{
1641
+ const messagesDiv = document.getElementById('chatMessages');
1642
+ const messageDiv = document.createElement('div');
1643
+ messageDiv.className = `message ${{type}}`;
1644
+ messageDiv.textContent = content;
1645
+ messagesDiv.appendChild(messageDiv);
1646
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1647
+ }}
1648
+
1649
+ function updateLastMessage(content, type) {{
1650
+ const messagesDiv = document.getElementById('chatMessages');
1651
+ let lastMessage = messagesDiv.lastElementChild;
1652
+
1653
+ if (!lastMessage || !lastMessage.classList.contains(type)) {{
1654
+ lastMessage = document.createElement('div');
1655
+ lastMessage.className = `message ${{type}}`;
1656
+ messagesDiv.appendChild(lastMessage);
1657
+ }}
1658
+
1659
+ lastMessage.textContent = content;
1660
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1661
+ }}
1662
+ </script>
1663
+ </body>
1664
+ </html>
1665
+ '''
1666
+
1667
+ @app.route('/')
1668
+ def hello_world():
1669
+ try:
1670
+ response = app.test_client().get('/v1/models')
1671
+ models_data = json.loads(response.data.decode('utf-8'))
1672
+
1673
+ model_items = []
1674
+ for model in models_data['data']:
1675
+ display_name = model['id'].replace('-', ' ').title().replace('Gpt', 'GPT')
1676
+ model_items.append(f'<li>{display_name}</li>')
1677
+
1678
+ models_html = '\n '.join(model_items)
1679
+
1680
+ except Exception as e:
1681
+ models_html = '<li>Error loading models</li>'
1682
+
1683
+ return f'''
1684
+ <!DOCTYPE html>
1685
+ <html lang="en">
1686
+ <head>
1687
+ <meta charset="UTF-8">
1688
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1689
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
1690
+ <title>GoAPI</title>
1691
+ <style>
1692
+ body {{
1693
+ max-width: 800px;
1694
+ margin: 50px auto 0 auto;
1695
+ padding: 20px;
1696
+ border: 2px solid #000;
1697
+ border-radius: 10px;
1698
+ text-align: center;
1699
+ font-family: 'Inter', sans-serif;
1700
+ }}
1701
+ .pricing {{
1702
+ display: grid;
1703
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1704
+ gap: 20px;
1705
+ margin: 30px 0;
1706
+ text-align: center;
1707
+ }}
1708
+ .plan {{
1709
+ padding: 20px;
1710
+ border: 1px solid #ccc;
1711
+ border-radius: 5px;
1712
+ }}
1713
+ .plan h3 {{
1714
+ margin: 0 0 10px 0;
1715
+ }}
1716
+ .price {{
1717
+ font-size: 24px;
1718
+ font-weight: bold;
1719
+ margin: 10px 0;
1720
+ }}
1721
+ .features {{
1722
+ font-size: 14px;
1723
+ color: #666;
1724
+ margin: 10px 0;
1725
+ }}
1726
+ ul {{
1727
+ border: 1px solid #ccc;
1728
+ border-radius: 5px;
1729
+ padding: 10px;
1730
+ list-style-type: none;
1731
+ display: inline-block;
1732
+ }}
1733
+ li {{
1734
+ border-bottom: 1px solid #eee;
1735
+ padding: 5px 0;
1736
+ }}
1737
+ li:last-child {{
1738
+ border-bottom: none;
1739
+ }}
1740
+ a:hover {{
1741
+ background-color: #333;
1742
+ color: white;
1743
+ }}
1744
+ </style>
1745
+ </head>
1746
+ <body>
1747
+ <h1>GoAPI</h1>
1748
+
1749
+
1750
+
1751
+ </div>
1752
+
1753
+ <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>
1754
+ <h2>Model List</h2>
1755
+ <ul>
1756
+ {models_html}
1757
+ </ul>
1758
+ <div class="pricing">
1759
+ <div class="plan">
1760
+ <h3>Free</h3>
1761
+ <div class="price">$0/month</div>
1762
+ <div class="features">500,000 daily tokens</div>
1763
+ <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>
1764
+ </div>
1765
+ <div class="plan">
1766
+ <h3>Explorer</h3>
1767
+ <div class="price">$4.99/month</div>
1768
+ <div class="features">5 million daily tokens</div>
1769
+ <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>
1770
+ </div>
1771
+ <div class="plan">
1772
+ <h3>Voyager</h3>
1773
+ <div class="price">$19.99/month</div>
1774
+ <div class="features">50 million daily tokens</div>
1775
+ <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>
1776
+ </div>
1777
+ </body>
1778
+
1779
+ </html>
1780
+ '''
1781
+
1782
+ @app.route('/admin/process-pending-deductions')
1783
+ def process_deductions_now():
1784
+ """Admin endpoint to manually process pending deductions"""
1785
+ if request.remote_addr != '127.0.0.1': # Only allow from localhost
1786
+ return jsonify({"error": "Unauthorized"}), 403
1787
+
1788
+ # Run in background thread to avoid blocking the response
1789
+ threading.Thread(target=process_pending_deductions, daemon=True).start()
1790
+ return jsonify({"message": "Pending deductions processing started"})
1791
+
1792
+ @atexit.register
1793
+ def cleanup_pending_deductions():
1794
+ """Process any remaining pending deductions when the app shuts down"""
1795
+ if pending_deductions:
1796
+ print("Processing remaining pending deductions on shutdown...")
1797
+ # Run in a separate thread to avoid blocking shutdown
1798
+ cleanup_thread = threading.Thread(target=process_pending_deductions, daemon=True)
1799
+ cleanup_thread.start()
1800
+ cleanup_thread.join(timeout=5) # Wait up to 5 seconds for completion
1801
+
1802
+ if __name__ == '__main__':
1803
+ # Start the background timer when the app starts
1804
+ start_batch_timer()
1805
+ app.run(debug=True)