Upload 15 files
Browse files- .dockerignore +11 -0
- Dockerfile +11 -0
- app.py +328 -0
- client_example.py +181 -0
- db_helper.py +266 -0
- deepinfra_client.py +473 -0
- docker-compose.override.yml.example +39 -0
- docker-compose.yml +33 -0
- docker_entrypoint.py +102 -0
- hf_setup.py +0 -0
- hf_utils.py +106 -0
- proxy_finder.py +290 -0
- pyscout_api.py +438 -0
- requirements.txt +9 -0
- run_api_server.py +46 -0
.dockerignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.env
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.coverage
|
| 7 |
+
.venv/
|
| 8 |
+
venv/
|
| 9 |
+
data/
|
| 10 |
+
logs/
|
| 11 |
+
*.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY . .
|
| 6 |
+
EXPOSE 7860
|
| 7 |
+
EXPOSE 8000
|
| 8 |
+
ENV PYTHONUNBUFFERED=1
|
| 9 |
+
RUN useradd -m appuser
|
| 10 |
+
USER appuser
|
| 11 |
+
ENTRYPOINT ["python", "docker_entrypoint.py"]
|
app.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import asyncio
|
| 6 |
+
import subprocess
|
| 7 |
+
import threading
|
| 8 |
+
import requests
|
| 9 |
+
import time
|
| 10 |
+
import random
|
| 11 |
+
import string
|
| 12 |
+
from typing import Dict, List, Optional
|
| 13 |
+
import uuid
|
| 14 |
+
|
| 15 |
+
from hf_utils import HuggingFaceSpaceHelper
|
| 16 |
+
|
| 17 |
+
# Initialize helpers
|
| 18 |
+
hf_helper = HuggingFaceSpaceHelper()
|
| 19 |
+
|
| 20 |
+
# Install required packages for HF Spaces if needed
|
| 21 |
+
if hf_helper.is_in_space:
|
| 22 |
+
hf_helper.install_dependencies([
|
| 23 |
+
"pymongo", "python-dotenv", "gradio", "requests"
|
| 24 |
+
])
|
| 25 |
+
|
| 26 |
+
# Import after potentially installing dependencies
|
| 27 |
+
try:
|
| 28 |
+
from db_helper import MongoDBHelper
|
| 29 |
+
db = MongoDBHelper(hf_helper.get_mongodb_uri())
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Warning: MongoDB connection failed: {e}")
|
| 32 |
+
print("API key management will not work!")
|
| 33 |
+
db = None
|
| 34 |
+
|
| 35 |
+
# Function to start the API in the background
|
| 36 |
+
def start_api_server():
|
| 37 |
+
"""Start the API server in a separate process"""
|
| 38 |
+
api_process = subprocess.Popen(
|
| 39 |
+
[sys.executable, "pyscout_api.py"],
|
| 40 |
+
stdout=subprocess.PIPE,
|
| 41 |
+
stderr=subprocess.PIPE,
|
| 42 |
+
text=True
|
| 43 |
+
)
|
| 44 |
+
return api_process
|
| 45 |
+
|
| 46 |
+
# Determine the API base URL based on environment
|
| 47 |
+
# In Docker, use the environment variable if set
|
| 48 |
+
API_BASE_URL = os.getenv("API_BASE_URL", f"http://{hf_helper.get_hostname()}:8000")
|
| 49 |
+
|
| 50 |
+
# Function to check API health
|
| 51 |
+
def check_api_health():
|
| 52 |
+
"""Check if the API is running and healthy"""
|
| 53 |
+
try:
|
| 54 |
+
response = requests.get(f"{API_BASE_URL}/health", timeout=5)
|
| 55 |
+
if response.status_code == 200:
|
| 56 |
+
return response.json()
|
| 57 |
+
return {"status": "error", "code": response.status_code}
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return {"status": "error", "message": str(e)}
|
| 60 |
+
|
| 61 |
+
# Utility functions for the Gradio UI
|
| 62 |
+
def generate_api_key(email: str, name: str, organization: str = ""):
|
| 63 |
+
"""Generate a new API key for a user"""
|
| 64 |
+
if not email or not name:
|
| 65 |
+
return "Error: Email and name are required"
|
| 66 |
+
|
| 67 |
+
if not db:
|
| 68 |
+
# Generate a dummy key for demonstration if MongoDB is not connected
|
| 69 |
+
dummy_key = f"PyScoutAI-demo-{uuid.uuid4().hex[:16]}"
|
| 70 |
+
return f"Generated demo API key (MongoDB not connected):\n\n{dummy_key}\n\nThis key won't be validated in actual requests."
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
# Create a user ID from email
|
| 74 |
+
user_id = email.strip().lower()
|
| 75 |
+
|
| 76 |
+
# Generate the API key
|
| 77 |
+
api_key = db.generate_api_key(user_id, name)
|
| 78 |
+
|
| 79 |
+
return f"API key generated successfully:\n\n{api_key}\n\nStore this key safely. It won't be displayed again."
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return f"Error generating API key: {str(e)}"
|
| 83 |
+
|
| 84 |
+
def list_user_api_keys(email: str):
|
| 85 |
+
"""List all API keys for a user"""
|
| 86 |
+
if not email:
|
| 87 |
+
return "Error: Email is required"
|
| 88 |
+
|
| 89 |
+
if not db:
|
| 90 |
+
return "Error: MongoDB not connected. Cannot list API keys."
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
# Get user ID from email
|
| 94 |
+
user_id = email.strip().lower()
|
| 95 |
+
|
| 96 |
+
# Get all API keys for the user
|
| 97 |
+
keys = db.get_user_api_keys(user_id)
|
| 98 |
+
|
| 99 |
+
if not keys:
|
| 100 |
+
return f"No API keys found for {email}"
|
| 101 |
+
|
| 102 |
+
result = f"Found {len(keys)} API key(s) for {email}:\n\n"
|
| 103 |
+
for i, key in enumerate(keys):
|
| 104 |
+
status = "Active" if key.get("is_active", False) else "Revoked"
|
| 105 |
+
last_used = key.get("last_used", "Never")
|
| 106 |
+
if isinstance(last_used, str):
|
| 107 |
+
last_used_str = last_used
|
| 108 |
+
else:
|
| 109 |
+
last_used_str = last_used.strftime("%Y-%m-%d %H:%M:%S") if last_used else "Never"
|
| 110 |
+
|
| 111 |
+
result += f"{i+1}. {key.get('name', 'Unnamed')} - {key.get('key')}\n"
|
| 112 |
+
result += f" Status: {status}, Created: {key.get('created_at').strftime('%Y-%m-%d')}, "
|
| 113 |
+
result += f"Last used: {last_used_str}\n\n"
|
| 114 |
+
|
| 115 |
+
return result
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return f"Error listing API keys: {str(e)}"
|
| 119 |
+
|
| 120 |
+
def revoke_api_key(api_key: str):
|
| 121 |
+
"""Revoke an API key"""
|
| 122 |
+
if not api_key:
|
| 123 |
+
return "Error: API key is required"
|
| 124 |
+
|
| 125 |
+
if not db:
|
| 126 |
+
return "Error: MongoDB not connected. Cannot revoke API key."
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
if not api_key.startswith("PyScoutAI-"):
|
| 130 |
+
return "Error: Invalid API key format. Keys should start with 'PyScoutAI-'."
|
| 131 |
+
|
| 132 |
+
success = db.revoke_api_key(api_key)
|
| 133 |
+
|
| 134 |
+
if success:
|
| 135 |
+
return f"API key {api_key} revoked successfully"
|
| 136 |
+
else:
|
| 137 |
+
return f"API key {api_key} not found or already revoked"
|
| 138 |
+
|
| 139 |
+
except Exception as e:
|
| 140 |
+
return f"Error revoking API key: {str(e)}"
|
| 141 |
+
|
| 142 |
+
def test_api(api_key: str, prompt: str, model: str = "meta-llama/Llama-3.3-70B-Instruct-Turbo", temperature: float = 0.7):
|
| 143 |
+
"""Test the API with a simple chat completion request"""
|
| 144 |
+
if not api_key:
|
| 145 |
+
return "Error: API key is required"
|
| 146 |
+
|
| 147 |
+
if not prompt:
|
| 148 |
+
return "Error: Prompt is required"
|
| 149 |
+
|
| 150 |
+
headers = {
|
| 151 |
+
"Content-Type": "application/json",
|
| 152 |
+
"Authorization": f"Bearer {api_key}"
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
data = {
|
| 156 |
+
"model": model,
|
| 157 |
+
"messages": [
|
| 158 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 159 |
+
{"role": "user", "content": prompt}
|
| 160 |
+
],
|
| 161 |
+
"temperature": temperature
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
start_time = time.time()
|
| 166 |
+
response = requests.post(
|
| 167 |
+
f"{API_BASE_URL}/v1/chat/completions",
|
| 168 |
+
headers=headers,
|
| 169 |
+
json=data,
|
| 170 |
+
timeout=60
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
elapsed = time.time() - start_time
|
| 174 |
+
|
| 175 |
+
if response.status_code == 200:
|
| 176 |
+
result = response.json()
|
| 177 |
+
content = result["choices"][0]["message"]["content"]
|
| 178 |
+
tokens = result.get("usage", {}).get("total_tokens", "unknown")
|
| 179 |
+
|
| 180 |
+
return f"Response (in {elapsed:.2f}s, {tokens} tokens):\n\n{content}"
|
| 181 |
+
else:
|
| 182 |
+
error_detail = "Unknown error"
|
| 183 |
+
try:
|
| 184 |
+
error_detail = response.json().get("detail", "Unknown error")
|
| 185 |
+
except:
|
| 186 |
+
error_detail = response.text
|
| 187 |
+
|
| 188 |
+
return f"API Error (status {response.status_code}):\n{error_detail}"
|
| 189 |
+
|
| 190 |
+
except requests.RequestException as e:
|
| 191 |
+
return f"Request error: {str(e)}"
|
| 192 |
+
except Exception as e:
|
| 193 |
+
return f"Unexpected error: {str(e)}"
|
| 194 |
+
|
| 195 |
+
def list_models(api_key: str):
|
| 196 |
+
"""List available models from the API"""
|
| 197 |
+
if not api_key:
|
| 198 |
+
return "Error: API key is required"
|
| 199 |
+
|
| 200 |
+
headers = {
|
| 201 |
+
"Authorization": f"Bearer {api_key}"
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
response = requests.get(
|
| 206 |
+
f"{API_BASE_URL}/v1/models",
|
| 207 |
+
headers=headers,
|
| 208 |
+
timeout=10
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if response.status_code == 200:
|
| 212 |
+
models = response.json()
|
| 213 |
+
result = "Available models:\n\n"
|
| 214 |
+
for i, model in enumerate(models.get("data", [])):
|
| 215 |
+
result += f"{i+1}. {model.get('id')}\n"
|
| 216 |
+
return result
|
| 217 |
+
else:
|
| 218 |
+
error_detail = "Unknown error"
|
| 219 |
+
try:
|
| 220 |
+
error_detail = response.json().get("detail", "Unknown error")
|
| 221 |
+
except:
|
| 222 |
+
error_detail = response.text
|
| 223 |
+
|
| 224 |
+
return f"API Error (status {response.status_code}):\n{error_detail}"
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
return f"Error listing models: {str(e)}"
|
| 228 |
+
|
| 229 |
+
def create_ui():
|
| 230 |
+
"""Create the Gradio UI"""
|
| 231 |
+
with gr.Blocks(title="PyScoutAI API Manager") as app:
|
| 232 |
+
gr.Markdown("# PyScoutAI API Manager")
|
| 233 |
+
gr.Markdown("Manage API keys and test the PyScoutAI API")
|
| 234 |
+
|
| 235 |
+
# API Status
|
| 236 |
+
with gr.Row():
|
| 237 |
+
check_api_btn = gr.Button("Check API Status")
|
| 238 |
+
api_status = gr.JSON(label="API Status")
|
| 239 |
+
check_api_btn.click(check_api_health, outputs=[api_status])
|
| 240 |
+
|
| 241 |
+
with gr.Tabs():
|
| 242 |
+
# API Key Management Tab
|
| 243 |
+
with gr.TabItem("Manage API Keys"):
|
| 244 |
+
with gr.Tab("Generate API Key"):
|
| 245 |
+
email_input = gr.Textbox(label="Email")
|
| 246 |
+
name_input = gr.Textbox(label="Name")
|
| 247 |
+
org_input = gr.Textbox(label="Organization (optional)")
|
| 248 |
+
gen_key_btn = gr.Button("Generate API Key")
|
| 249 |
+
key_output = gr.Textbox(label="Generated Key", lines=5)
|
| 250 |
+
|
| 251 |
+
gen_key_btn.click(
|
| 252 |
+
generate_api_key,
|
| 253 |
+
inputs=[email_input, name_input, org_input],
|
| 254 |
+
outputs=[key_output]
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
with gr.Tab("List User Keys"):
|
| 258 |
+
email_list_input = gr.Textbox(label="Email")
|
| 259 |
+
list_keys_btn = gr.Button("List API Keys")
|
| 260 |
+
keys_output = gr.Textbox(label="User API Keys", lines=10)
|
| 261 |
+
|
| 262 |
+
list_keys_btn.click(
|
| 263 |
+
list_user_api_keys,
|
| 264 |
+
inputs=[email_list_input],
|
| 265 |
+
outputs=[keys_output]
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
with gr.Tab("Revoke API Key"):
|
| 269 |
+
key_revoke_input = gr.Textbox(label="API Key to Revoke")
|
| 270 |
+
revoke_btn = gr.Button("Revoke API Key")
|
| 271 |
+
revoke_output = gr.Textbox(label="Result")
|
| 272 |
+
|
| 273 |
+
revoke_btn.click(
|
| 274 |
+
revoke_api_key,
|
| 275 |
+
inputs=[key_revoke_input],
|
| 276 |
+
outputs=[revoke_output]
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# API Testing Tab
|
| 280 |
+
with gr.TabItem("Test API"):
|
| 281 |
+
with gr.Row():
|
| 282 |
+
api_key_input = gr.Textbox(label="API Key")
|
| 283 |
+
model_input = gr.Dropdown(
|
| 284 |
+
choices=[
|
| 285 |
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 286 |
+
"meta-llama/Meta-Llama-3.1-70B-Instruct",
|
| 287 |
+
"mistralai/Mistral-Small-24B-Instruct-2501",
|
| 288 |
+
"deepseek-ai/DeepSeek-V3"
|
| 289 |
+
],
|
| 290 |
+
label="Model",
|
| 291 |
+
value="meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
| 292 |
+
)
|
| 293 |
+
temperature_input = gr.Slider(
|
| 294 |
+
minimum=0.0,
|
| 295 |
+
maximum=1.0,
|
| 296 |
+
value=0.7,
|
| 297 |
+
step=0.1,
|
| 298 |
+
label="Temperature"
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
prompt_input = gr.Textbox(label="Prompt", lines=3)
|
| 302 |
+
test_btn = gr.Button("Send Request")
|
| 303 |
+
list_models_btn = gr.Button("List Available Models")
|
| 304 |
+
api_output = gr.Textbox(label="Response", lines=15)
|
| 305 |
+
|
| 306 |
+
test_btn.click(
|
| 307 |
+
test_api,
|
| 308 |
+
inputs=[api_key_input, prompt_input, model_input, temperature_input],
|
| 309 |
+
outputs=[api_output]
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
list_models_btn.click(
|
| 313 |
+
list_models,
|
| 314 |
+
inputs=[api_key_input],
|
| 315 |
+
outputs=[api_output]
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
return app
|
| 319 |
+
|
| 320 |
+
def main():
|
| 321 |
+
"""Main entry point for the app"""
|
| 322 |
+
# Check if API server is running
|
| 323 |
+
api_health = check_api_health()
|
| 324 |
+
|
| 325 |
+
if "status" in api_health and api_health["status"] == "error":
|
| 326 |
+
print("API server doesn't seem to be running. Starting it...")
|
| 327 |
+
# Start the API server if it's not already running
|
| 328 |
+
api_process = start_api_server()
|
client_example.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
from typing import Dict, List, Any
|
| 5 |
+
from rich import print
|
| 6 |
+
from rich.console import Console
|
| 7 |
+
from rich.table import Table
|
| 8 |
+
from rich.panel import Panel
|
| 9 |
+
|
| 10 |
+
from deepinfra_client import DeepInfraClient
|
| 11 |
+
|
| 12 |
+
console = Console()
|
| 13 |
+
|
| 14 |
+
def print_proxy_status(client):
|
| 15 |
+
"""Print the proxy and IP rotation status"""
|
| 16 |
+
status = []
|
| 17 |
+
|
| 18 |
+
if client.use_proxy_rotation and client.proxy_finder:
|
| 19 |
+
proxy_counts = {k: len(v) for k, v in client.proxy_finder.proxy_dict.items()}
|
| 20 |
+
total_proxies = sum(proxy_counts.values())
|
| 21 |
+
status.append(f"Proxy rotation: [green]Enabled[/green] ({total_proxies} proxies)")
|
| 22 |
+
|
| 23 |
+
# Show available proxies per type
|
| 24 |
+
table = Table(title="Available Proxies")
|
| 25 |
+
table.add_column("Type", style="cyan")
|
| 26 |
+
table.add_column("Count", style="green")
|
| 27 |
+
|
| 28 |
+
for proxy_type, count in proxy_counts.items():
|
| 29 |
+
if count > 0:
|
| 30 |
+
table.add_row(proxy_type, str(count))
|
| 31 |
+
|
| 32 |
+
console.print(table)
|
| 33 |
+
else:
|
| 34 |
+
status.append("Proxy rotation: [red]Disabled[/red]")
|
| 35 |
+
|
| 36 |
+
if client.use_ip_rotation and client.ip_rotator:
|
| 37 |
+
status.append(f"IP rotation: [green]Enabled[/green] (AWS API Gateway - {len(client.ip_rotator.gateways)} regions)")
|
| 38 |
+
else:
|
| 39 |
+
status.append("IP rotation: [red]Disabled[/red]")
|
| 40 |
+
|
| 41 |
+
if client.use_random_user_agent:
|
| 42 |
+
status.append("User-Agent rotation: [green]Enabled[/green]")
|
| 43 |
+
else:
|
| 44 |
+
status.append("User-Agent rotation: [red]Disabled[/red]")
|
| 45 |
+
|
| 46 |
+
console.print(Panel("\n".join(status), title="Client Configuration", border_style="blue"))
|
| 47 |
+
|
| 48 |
+
def chat_with_model():
|
| 49 |
+
"""Demonstrate interactive chat with DeepInfra models"""
|
| 50 |
+
# Initialize the client with all rotation features enabled
|
| 51 |
+
client = DeepInfraClient(
|
| 52 |
+
api_key=os.getenv("DEEPINFRA_API_KEY"), # Set this environment variable if you have an API key
|
| 53 |
+
use_random_user_agent=True,
|
| 54 |
+
use_ip_rotation=True,
|
| 55 |
+
use_proxy_rotation=True,
|
| 56 |
+
proxy_types=['http', 'socks5'],
|
| 57 |
+
model="meta-llama/Llama-3.3-70B-Instruct-Turbo" # Use a good default model
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
print_proxy_status(client)
|
| 61 |
+
|
| 62 |
+
# Show available models
|
| 63 |
+
console.print("\n[bold cyan]Fetching available models...[/bold cyan]")
|
| 64 |
+
try:
|
| 65 |
+
models_response = client.models.list()
|
| 66 |
+
model_table = Table(title="Available Models")
|
| 67 |
+
model_table.add_column("Model", style="green")
|
| 68 |
+
|
| 69 |
+
for model in models_response["data"]:
|
| 70 |
+
model_table.add_row(model["id"])
|
| 71 |
+
|
| 72 |
+
console.print(model_table)
|
| 73 |
+
except Exception as e:
|
| 74 |
+
console.print(f"[red]Error fetching models: {str(e)}[/red]")
|
| 75 |
+
|
| 76 |
+
# Start interactive chat
|
| 77 |
+
console.print("\n[bold green]Starting interactive chat (type 'quit' to exit)[/bold green]")
|
| 78 |
+
console.print("[yellow]Note: Every 3 messages, the client will rotate IP and proxy[/yellow]\n")
|
| 79 |
+
|
| 80 |
+
messages = [{"role": "system", "content": "You are a helpful assistant."}]
|
| 81 |
+
message_count = 0
|
| 82 |
+
|
| 83 |
+
while True:
|
| 84 |
+
user_input = input("\nYou: ")
|
| 85 |
+
if user_input.lower() in ["quit", "exit", "bye"]:
|
| 86 |
+
break
|
| 87 |
+
|
| 88 |
+
messages.append({"role": "user", "content": user_input})
|
| 89 |
+
|
| 90 |
+
# Rotate IP and proxy every 3 messages
|
| 91 |
+
message_count += 1
|
| 92 |
+
if message_count % 3 == 0:
|
| 93 |
+
console.print("[yellow]Rotating IP and proxy...[/yellow]")
|
| 94 |
+
client.refresh_proxies()
|
| 95 |
+
client.refresh_session()
|
| 96 |
+
|
| 97 |
+
# Make the API call
|
| 98 |
+
console.print("\n[cyan]Waiting for response...[/cyan]")
|
| 99 |
+
start_time = time.time()
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
response = client.chat.create(
|
| 103 |
+
messages=messages,
|
| 104 |
+
temperature=0.7,
|
| 105 |
+
max_tokens=1024
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
elapsed = time.time() - start_time
|
| 109 |
+
assistant_message = response["choices"][0]["message"]["content"]
|
| 110 |
+
|
| 111 |
+
# Add the assistant's message to our history
|
| 112 |
+
messages.append({"role": "assistant", "content": assistant_message})
|
| 113 |
+
|
| 114 |
+
console.print(f"\n[bold green]Assistant[/bold green] [dim]({elapsed:.2f}s)[/dim]:")
|
| 115 |
+
console.print(assistant_message)
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
console.print(f"[bold red]Error: {str(e)}[/bold red]")
|
| 119 |
+
console.print("[yellow]Refreshing session and trying again...[/yellow]")
|
| 120 |
+
client.refresh_session()
|
| 121 |
+
|
| 122 |
+
def stream_example():
|
| 123 |
+
"""Demonstrate streaming responses"""
|
| 124 |
+
client = DeepInfraClient(
|
| 125 |
+
use_random_user_agent=True,
|
| 126 |
+
use_ip_rotation=True,
|
| 127 |
+
use_proxy_rotation=True
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
print_proxy_status(client)
|
| 131 |
+
|
| 132 |
+
prompt = "Write a short story about a robot that learns to feel emotions."
|
| 133 |
+
|
| 134 |
+
console.print(f"\n[bold cyan]Prompt:[/bold cyan] {prompt}")
|
| 135 |
+
console.print("\n[bold green]Streaming response:[/bold green]")
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
response_stream = client.completions.create(
|
| 139 |
+
prompt=prompt,
|
| 140 |
+
temperature=0.8,
|
| 141 |
+
max_tokens=1024,
|
| 142 |
+
stream=True
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
full_response = ""
|
| 146 |
+
for chunk in response_stream:
|
| 147 |
+
if 'choices' in chunk and len(chunk['choices']) > 0:
|
| 148 |
+
delta = chunk['choices'][0].get('delta', {})
|
| 149 |
+
if 'content' in delta:
|
| 150 |
+
content = delta['content']
|
| 151 |
+
print(content, end='', flush=True)
|
| 152 |
+
full_response += content
|
| 153 |
+
print("\n")
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
console.print(f"\n[bold red]Error: {str(e)}[/bold red]")
|
| 157 |
+
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
console.print(Panel.fit(
|
| 160 |
+
"[bold green]DeepInfra Client Example[/bold green]\n"
|
| 161 |
+
"This example demonstrates the enhanced client with proxy and IP rotation",
|
| 162 |
+
border_style="yellow"
|
| 163 |
+
))
|
| 164 |
+
|
| 165 |
+
while True:
|
| 166 |
+
console.print("\n[bold cyan]Choose an option:[/bold cyan]")
|
| 167 |
+
console.print("1. Interactive Chat")
|
| 168 |
+
console.print("2. Streaming Example")
|
| 169 |
+
console.print("3. Exit")
|
| 170 |
+
|
| 171 |
+
choice = input("\nEnter your choice (1-3): ")
|
| 172 |
+
|
| 173 |
+
if choice == "1":
|
| 174 |
+
chat_with_model()
|
| 175 |
+
elif choice == "2":
|
| 176 |
+
stream_example()
|
| 177 |
+
elif choice == "3":
|
| 178 |
+
console.print("[yellow]Exiting...[/yellow]")
|
| 179 |
+
break
|
| 180 |
+
else:
|
| 181 |
+
console.print("[red]Invalid choice. Please try again.[/red]")
|
db_helper.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
import datetime
|
| 4 |
+
from typing import Dict, List, Optional, Any
|
| 5 |
+
from pymongo import MongoClient
|
| 6 |
+
from bson.objectid import ObjectId
|
| 7 |
+
|
| 8 |
+
class MongoDBHelper:
|
| 9 |
+
"""Helper class for MongoDB operations"""
|
| 10 |
+
|
| 11 |
+
def __init__(self, connection_string: Optional[str] = None):
|
| 12 |
+
"""Initialize the MongoDB client"""
|
| 13 |
+
# Get connection string from env var or use provided one
|
| 14 |
+
self.connection_string = connection_string or os.getenv('MONGODB_URI')
|
| 15 |
+
|
| 16 |
+
if not self.connection_string:
|
| 17 |
+
raise ValueError("MongoDB connection string not provided. Set MONGODB_URI environment variable or pass it to constructor.")
|
| 18 |
+
|
| 19 |
+
self.client = MongoClient(self.connection_string)
|
| 20 |
+
self.db = self.client.get_database("pyscout_ai")
|
| 21 |
+
|
| 22 |
+
# Collections
|
| 23 |
+
self.api_keys_collection = self.db.api_keys
|
| 24 |
+
self.usage_collection = self.db.usage
|
| 25 |
+
self.users_collection = self.db.users
|
| 26 |
+
self.conversations_collection = self.db.conversations
|
| 27 |
+
self.messages_collection = self.db.messages
|
| 28 |
+
|
| 29 |
+
self._create_indexes()
|
| 30 |
+
|
| 31 |
+
def _create_indexes(self):
|
| 32 |
+
# API Keys indexes
|
| 33 |
+
self.api_keys_collection.create_index("key", unique=True)
|
| 34 |
+
self.api_keys_collection.create_index("user_id")
|
| 35 |
+
self.api_keys_collection.create_index("created_at")
|
| 36 |
+
|
| 37 |
+
# Usage indexes
|
| 38 |
+
self.usage_collection.create_index("api_key")
|
| 39 |
+
self.usage_collection.create_index("timestamp")
|
| 40 |
+
|
| 41 |
+
# Users indexes
|
| 42 |
+
self.users_collection.create_index("email", unique=True)
|
| 43 |
+
|
| 44 |
+
# Conversations indexes
|
| 45 |
+
self.conversations_collection.create_index("user_id")
|
| 46 |
+
self.conversations_collection.create_index("created_at")
|
| 47 |
+
|
| 48 |
+
# Messages indexes
|
| 49 |
+
self.messages_collection.create_index("conversation_id")
|
| 50 |
+
self.messages_collection.create_index("timestamp")
|
| 51 |
+
|
| 52 |
+
def create_user(self, email: str, name: str, organization: str = None) -> str:
|
| 53 |
+
user_id = str(ObjectId())
|
| 54 |
+
self.users_collection.insert_one({
|
| 55 |
+
"_id": ObjectId(user_id),
|
| 56 |
+
"email": email,
|
| 57 |
+
"name": name,
|
| 58 |
+
"organization": organization,
|
| 59 |
+
"created_at": datetime.datetime.utcnow(),
|
| 60 |
+
"last_active": datetime.datetime.utcnow()
|
| 61 |
+
})
|
| 62 |
+
return user_id
|
| 63 |
+
|
| 64 |
+
def create_conversation(self, user_id: str, system_prompt: str = None) -> str:
|
| 65 |
+
conversation_id = str(ObjectId())
|
| 66 |
+
self.conversations_collection.insert_one({
|
| 67 |
+
"_id": ObjectId(conversation_id),
|
| 68 |
+
"user_id": user_id,
|
| 69 |
+
"system_prompt": system_prompt,
|
| 70 |
+
"created_at": datetime.datetime.utcnow(),
|
| 71 |
+
"last_message_at": datetime.datetime.utcnow(),
|
| 72 |
+
"is_active": True
|
| 73 |
+
})
|
| 74 |
+
return conversation_id
|
| 75 |
+
|
| 76 |
+
def add_message(self, conversation_id: str, role: str, content: str,
|
| 77 |
+
model: str = None, tokens: int = 0) -> str:
|
| 78 |
+
message_id = str(ObjectId())
|
| 79 |
+
self.messages_collection.insert_one({
|
| 80 |
+
"_id": ObjectId(message_id),
|
| 81 |
+
"conversation_id": conversation_id,
|
| 82 |
+
"role": role,
|
| 83 |
+
"content": content,
|
| 84 |
+
"model": model,
|
| 85 |
+
"tokens": tokens,
|
| 86 |
+
"timestamp": datetime.datetime.utcnow()
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
# Update conversation last_message_at
|
| 90 |
+
self.conversations_collection.update_one(
|
| 91 |
+
{"_id": ObjectId(conversation_id)},
|
| 92 |
+
{"$set": {"last_message_at": datetime.datetime.utcnow()}}
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return message_id
|
| 96 |
+
|
| 97 |
+
def get_conversation_history(self, conversation_id: str) -> List[Dict]:
|
| 98 |
+
return list(self.messages_collection.find(
|
| 99 |
+
{"conversation_id": conversation_id},
|
| 100 |
+
{"_id": 0}
|
| 101 |
+
).sort("timestamp", 1))
|
| 102 |
+
|
| 103 |
+
def get_user_conversations(self, user_id: str, limit: int = 10) -> List[Dict]:
|
| 104 |
+
conversations = list(self.conversations_collection.find(
|
| 105 |
+
{"user_id": user_id},
|
| 106 |
+
{"_id": 1, "system_prompt": 1, "created_at": 1, "last_message_at": 1}
|
| 107 |
+
).sort("last_message_at", -1).limit(limit))
|
| 108 |
+
|
| 109 |
+
# Convert ObjectId to string
|
| 110 |
+
for conv in conversations:
|
| 111 |
+
conv["_id"] = str(conv["_id"])
|
| 112 |
+
return conversations
|
| 113 |
+
|
| 114 |
+
def generate_api_key(self, user_id: str, name: str = "Default API Key") -> str:
|
| 115 |
+
"""Generate a new API key for a user"""
|
| 116 |
+
# Format: PyScoutAI-{uuid4-hex}
|
| 117 |
+
api_key = f"PyScoutAI-{uuid.uuid4().hex}"
|
| 118 |
+
|
| 119 |
+
# Store in database
|
| 120 |
+
self.api_keys_collection.insert_one({
|
| 121 |
+
"key": api_key,
|
| 122 |
+
"user_id": user_id,
|
| 123 |
+
"name": name,
|
| 124 |
+
"created_at": datetime.datetime.utcnow(),
|
| 125 |
+
"last_used": None,
|
| 126 |
+
"is_active": True,
|
| 127 |
+
"rate_limit": {
|
| 128 |
+
"requests_per_day": 1000,
|
| 129 |
+
"tokens_per_day": 1000000
|
| 130 |
+
}
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
return api_key
|
| 134 |
+
|
| 135 |
+
def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
| 136 |
+
"""
|
| 137 |
+
Validate an API key
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
Dict with user info if valid, None otherwise
|
| 141 |
+
"""
|
| 142 |
+
if not api_key:
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
# Find the API key in the database
|
| 146 |
+
key_data = self.api_keys_collection.find_one({"key": api_key, "is_active": True})
|
| 147 |
+
if not key_data:
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
# Update last used timestamp
|
| 151 |
+
self.api_keys_collection.update_one(
|
| 152 |
+
{"_id": key_data["_id"]},
|
| 153 |
+
{"$set": {"last_used": datetime.datetime.utcnow()}}
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
return key_data
|
| 157 |
+
|
| 158 |
+
def log_api_usage(self, api_key: str, endpoint: str, tokens: int = 0,
|
| 159 |
+
model: str = None, conversation_id: str = None):
|
| 160 |
+
usage_data = {
|
| 161 |
+
"api_key": api_key,
|
| 162 |
+
"endpoint": endpoint,
|
| 163 |
+
"tokens": tokens,
|
| 164 |
+
"model": model,
|
| 165 |
+
"timestamp": datetime.datetime.utcnow()
|
| 166 |
+
}
|
| 167 |
+
if conversation_id:
|
| 168 |
+
usage_data["conversation_id"] = conversation_id
|
| 169 |
+
|
| 170 |
+
self.usage_collection.insert_one(usage_data)
|
| 171 |
+
|
| 172 |
+
def get_user_api_keys(self, user_id: str) -> List[Dict[str, Any]]:
|
| 173 |
+
"""Get all API keys for a user"""
|
| 174 |
+
keys = list(self.api_keys_collection.find({"user_id": user_id}))
|
| 175 |
+
# Convert ObjectId to string for JSON serialization
|
| 176 |
+
for key in keys:
|
| 177 |
+
key["_id"] = str(key["_id"])
|
| 178 |
+
return keys
|
| 179 |
+
|
| 180 |
+
def revoke_api_key(self, api_key: str) -> bool:
|
| 181 |
+
"""Revoke an API key"""
|
| 182 |
+
result = self.api_keys_collection.update_one(
|
| 183 |
+
{"key": api_key},
|
| 184 |
+
{"$set": {"is_active": False}}
|
| 185 |
+
)
|
| 186 |
+
return result.modified_count > 0
|
| 187 |
+
|
| 188 |
+
def check_rate_limit(self, api_key: str) -> Dict[str, Any]:
|
| 189 |
+
"""
|
| 190 |
+
Check if the API key has exceeded its rate limits
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
Dict with rate limit info and allowed status
|
| 194 |
+
"""
|
| 195 |
+
key_data = self.api_keys_collection.find_one({"key": api_key, "is_active": True})
|
| 196 |
+
if not key_data:
|
| 197 |
+
return {"allowed": False, "reason": "Invalid API key"}
|
| 198 |
+
|
| 199 |
+
# Get rate limit settings
|
| 200 |
+
rate_limit = key_data.get("rate_limit", {})
|
| 201 |
+
requests_per_day = rate_limit.get("requests_per_day", 1000)
|
| 202 |
+
tokens_per_day = rate_limit.get("tokens_per_day", 1000000)
|
| 203 |
+
|
| 204 |
+
# Calculate usage for today
|
| 205 |
+
today_start = datetime.datetime.combine(
|
| 206 |
+
datetime.datetime.utcnow().date(),
|
| 207 |
+
datetime.time.min
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
# Count requests today
|
| 211 |
+
requests_today = self.usage_collection.count_documents({
|
| 212 |
+
"api_key": api_key,
|
| 213 |
+
"timestamp": {"$gte": today_start}
|
| 214 |
+
})
|
| 215 |
+
|
| 216 |
+
# Sum tokens used today
|
| 217 |
+
tokens_pipeline = [
|
| 218 |
+
{"$match": {"api_key": api_key, "timestamp": {"$gte": today_start}}},
|
| 219 |
+
{"$group": {"_id": None, "total_tokens": {"$sum": "$tokens"}}}
|
| 220 |
+
]
|
| 221 |
+
tokens_result = list(self.usage_collection.aggregate(tokens_pipeline))
|
| 222 |
+
tokens_today = tokens_result[0]["total_tokens"] if tokens_result else 0
|
| 223 |
+
|
| 224 |
+
# Check if limits are exceeded
|
| 225 |
+
if requests_today >= requests_per_day:
|
| 226 |
+
return {
|
| 227 |
+
"allowed": False,
|
| 228 |
+
"reason": "Daily request limit exceeded",
|
| 229 |
+
"limit": requests_per_day,
|
| 230 |
+
"used": requests_today
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
if tokens_today >= tokens_per_day:
|
| 234 |
+
return {
|
| 235 |
+
"allowed": False,
|
| 236 |
+
"reason": "Daily token limit exceeded",
|
| 237 |
+
"limit": tokens_per_day,
|
| 238 |
+
"used": tokens_today
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return {
|
| 242 |
+
"allowed": True,
|
| 243 |
+
"requests": {
|
| 244 |
+
"limit": requests_per_day,
|
| 245 |
+
"used": requests_today,
|
| 246 |
+
"remaining": requests_per_day - requests_today
|
| 247 |
+
},
|
| 248 |
+
"tokens": {
|
| 249 |
+
"limit": tokens_per_day,
|
| 250 |
+
"used": tokens_today,
|
| 251 |
+
"remaining": tokens_per_day - tokens_today
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
def get_user_stats(self, user_id: str) -> Dict:
|
| 256 |
+
pipeline = [
|
| 257 |
+
{"$match": {"user_id": user_id}},
|
| 258 |
+
{"$group": {
|
| 259 |
+
"_id": None,
|
| 260 |
+
"total_conversations": {"$sum": 1},
|
| 261 |
+
"total_messages": {"$sum": "$message_count"},
|
| 262 |
+
"total_tokens": {"$sum": "$total_tokens"}
|
| 263 |
+
}}
|
| 264 |
+
]
|
| 265 |
+
stats = list(self.conversations_collection.aggregate(pipeline))
|
| 266 |
+
return stats[0] if stats else {"total_conversations": 0, "total_messages": 0, "total_tokens": 0}
|
deepinfra_client.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
import time
|
| 6 |
+
from typing import Any, Dict, Optional, Generator, Union, List
|
| 7 |
+
import warnings
|
| 8 |
+
from fake_useragent import UserAgent
|
| 9 |
+
from requests_ip_rotator import ApiGateway
|
| 10 |
+
from proxy_finder import ProxyFinder
|
| 11 |
+
|
| 12 |
+
class IPRotator:
|
| 13 |
+
"""Manages AWS API Gateway rotation for multiple regions"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, target_url="deepinfra.com", regions=None):
|
| 16 |
+
"""Initialize with target URL and regions"""
|
| 17 |
+
self.target_url = target_url
|
| 18 |
+
self.regions = regions or ["us-east-1", "us-west-1", "eu-west-1", "ap-southeast-1"]
|
| 19 |
+
self.gateways = {}
|
| 20 |
+
|
| 21 |
+
def setup(self):
|
| 22 |
+
"""Set up API gateways for each region"""
|
| 23 |
+
for region in self.regions:
|
| 24 |
+
try:
|
| 25 |
+
gateway = ApiGateway(self.target_url, region=region)
|
| 26 |
+
gateway.start()
|
| 27 |
+
self.gateways[region] = gateway
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"Failed to set up gateway in {region}: {str(e)}")
|
| 30 |
+
|
| 31 |
+
if not self.gateways:
|
| 32 |
+
raise Exception("Failed to set up any API gateways for IP rotation")
|
| 33 |
+
|
| 34 |
+
def get_session(self):
|
| 35 |
+
"""Get a random session from a gateway"""
|
| 36 |
+
if not self.gateways:
|
| 37 |
+
return requests.Session()
|
| 38 |
+
|
| 39 |
+
# Choose a random gateway
|
| 40 |
+
region = random.choice(list(self.gateways.keys()))
|
| 41 |
+
gateway = self.gateways[region]
|
| 42 |
+
return gateway.get_session()
|
| 43 |
+
|
| 44 |
+
def shutdown(self):
|
| 45 |
+
"""Clean up all gateways"""
|
| 46 |
+
for gateway in self.gateways.values():
|
| 47 |
+
try:
|
| 48 |
+
gateway.shutdown()
|
| 49 |
+
except:
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
class ProxyManager:
|
| 53 |
+
"""Manages proxy rotation for HTTP requests"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, proxies=None):
|
| 56 |
+
"""Initialize with a list of proxies or an empty list"""
|
| 57 |
+
self.proxies = proxies or []
|
| 58 |
+
|
| 59 |
+
def add_proxy(self, proxy):
|
| 60 |
+
"""Add a proxy to the list"""
|
| 61 |
+
self.proxies.append(proxy)
|
| 62 |
+
|
| 63 |
+
def get_random(self):
|
| 64 |
+
"""Return a random proxy if available, otherwise None"""
|
| 65 |
+
if not self.proxies:
|
| 66 |
+
return None
|
| 67 |
+
return random.choice(self.proxies)
|
| 68 |
+
|
| 69 |
+
class DeepInfraClient:
|
| 70 |
+
"""
|
| 71 |
+
A client for DeepInfra API with OpenAI-compatible interface and enhanced features
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
AVAILABLE_MODELS = [
|
| 75 |
+
"deepseek-ai/DeepSeek-R1-Turbo",
|
| 76 |
+
"deepseek-ai/DeepSeek-R1",
|
| 77 |
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
| 78 |
+
"deepseek-ai/DeepSeek-V3",
|
| 79 |
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 80 |
+
"mistralai/Mistral-Small-24B-Instruct-2501",
|
| 81 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
| 82 |
+
"microsoft/phi-4",
|
| 83 |
+
"meta-llama/Meta-Llama-3.1-70B-Instruct",
|
| 84 |
+
"meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 85 |
+
"meta-llama/Meta-Llama-3.1-405B-Instruct",
|
| 86 |
+
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
|
| 87 |
+
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
| 88 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 89 |
+
"nvidia/Llama-3.1-Nemotron-70B-Instruct",
|
| 90 |
+
"Qwen/Qwen2.5-72B-Instruct",
|
| 91 |
+
"meta-llama/Llama-3.2-90B-Vision-Instruct",
|
| 92 |
+
"meta-llama/Llama-3.2-11B-Vision-Instruct",
|
| 93 |
+
"Gryphe/MythoMax-L2-13b",
|
| 94 |
+
"NousResearch/Hermes-3-Llama-3.1-405B",
|
| 95 |
+
"NovaSky-AI/Sky-T1-32B-Preview",
|
| 96 |
+
"Qwen/Qwen2.5-7B-Instruct",
|
| 97 |
+
"Sao10K/L3.1-70B-Euryale-v2.2",
|
| 98 |
+
"Sao10K/L3.3-70B-Euryale-v2.3",
|
| 99 |
+
"google/gemma-2-27b-it",
|
| 100 |
+
"google/gemma-2-9b-it",
|
| 101 |
+
"meta-llama/Llama-3.2-1B-Instruct",
|
| 102 |
+
"meta-llama/Llama-3.2-3B-Instruct",
|
| 103 |
+
"meta-llama/Meta-Llama-3-70B-Instruct",
|
| 104 |
+
"meta-llama/Meta-Llama-3-8B-Instruct",
|
| 105 |
+
"mistralai/Mistral-Nemo-Instruct-2407",
|
| 106 |
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
| 107 |
+
"mistralai/Mixtral-8x7B-Instruct-v0.1"
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
api_key: Optional[str] = None,
|
| 113 |
+
base_url: str = "https://api.deepinfra.com/v1",
|
| 114 |
+
timeout: int = 30,
|
| 115 |
+
max_tokens: int = 2049,
|
| 116 |
+
model: str = "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 117 |
+
use_random_user_agent: bool = True,
|
| 118 |
+
use_proxy_rotation: bool = True,
|
| 119 |
+
use_ip_rotation: bool = True,
|
| 120 |
+
proxy_types: List[str] = None
|
| 121 |
+
):
|
| 122 |
+
"""Initialize the DeepInfraClient"""
|
| 123 |
+
self.base_url = base_url
|
| 124 |
+
self.api_key = api_key
|
| 125 |
+
self.model = model
|
| 126 |
+
self.timeout = timeout
|
| 127 |
+
self.max_tokens = max_tokens
|
| 128 |
+
self.use_random_user_agent = use_random_user_agent
|
| 129 |
+
self.use_ip_rotation = use_ip_rotation
|
| 130 |
+
self.use_proxy_rotation = use_proxy_rotation
|
| 131 |
+
self.proxy_types = proxy_types or ['http', 'socks5'] # Default proxy types
|
| 132 |
+
|
| 133 |
+
# Initialize user agent generator
|
| 134 |
+
self.user_agent = UserAgent()
|
| 135 |
+
|
| 136 |
+
# Set up proxy finder and get initial proxies if proxy rotation is enabled
|
| 137 |
+
self.proxy_finder = None
|
| 138 |
+
if self.use_proxy_rotation:
|
| 139 |
+
self.proxy_finder = ProxyFinder(verbose=False)
|
| 140 |
+
self.proxy_finder.get_proxies(self.proxy_types)
|
| 141 |
+
|
| 142 |
+
# Set up IP rotator if enabled
|
| 143 |
+
self.ip_rotator = None
|
| 144 |
+
if use_ip_rotation:
|
| 145 |
+
try:
|
| 146 |
+
self.ip_rotator = IPRotator(target_url="deepinfra.com")
|
| 147 |
+
self.ip_rotator.setup()
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print(f"Failed to set up IP rotation: {e}. Continuing without IP rotation.")
|
| 150 |
+
self.ip_rotator = None
|
| 151 |
+
|
| 152 |
+
# Set up headers with random or fixed user agent
|
| 153 |
+
self.headers = self._create_headers()
|
| 154 |
+
|
| 155 |
+
# Initialize session based on available rotation methods
|
| 156 |
+
if self.use_ip_rotation and self.ip_rotator:
|
| 157 |
+
self.session = self.ip_rotator.get_session()
|
| 158 |
+
else:
|
| 159 |
+
self.session = requests.Session()
|
| 160 |
+
|
| 161 |
+
self.session.headers.update(self.headers)
|
| 162 |
+
|
| 163 |
+
# Apply proxy if proxy rotation is enabled
|
| 164 |
+
if self.use_proxy_rotation and self.proxy_finder:
|
| 165 |
+
self._apply_random_proxy()
|
| 166 |
+
|
| 167 |
+
# Resources
|
| 168 |
+
self.models = Models(self)
|
| 169 |
+
self.chat = ChatCompletions(self)
|
| 170 |
+
self.completions = Completions(self)
|
| 171 |
+
|
| 172 |
+
def _create_headers(self) -> Dict[str, str]:
|
| 173 |
+
"""Create headers for the HTTP request, optionally with a random user agent"""
|
| 174 |
+
user_agent = self.user_agent.random if self.use_random_user_agent else \
|
| 175 |
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
|
| 176 |
+
|
| 177 |
+
headers = {
|
| 178 |
+
'User-Agent': user_agent,
|
| 179 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 180 |
+
'Cache-Control': 'no-cache',
|
| 181 |
+
'Connection': 'keep-alive',
|
| 182 |
+
'Content-Type': 'application/json',
|
| 183 |
+
'Origin': 'https://deepinfra.com',
|
| 184 |
+
'Referer': 'https://deepinfra.com/',
|
| 185 |
+
'Sec-Fetch-Dest': 'empty',
|
| 186 |
+
'Sec-Fetch-Mode': 'cors',
|
| 187 |
+
'Sec-Fetch-Site': 'same-site',
|
| 188 |
+
'X-Deepinfra-Source': 'web-embed',
|
| 189 |
+
'accept': 'text/event-stream',
|
| 190 |
+
'sec-ch-ua-mobile': '?0',
|
| 191 |
+
'sec-ch-ua-platform': '"macOS"'
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
if self.api_key:
|
| 195 |
+
headers['Authorization'] = f"Bearer {self.api_key}"
|
| 196 |
+
|
| 197 |
+
return headers
|
| 198 |
+
|
| 199 |
+
def _apply_random_proxy(self):
|
| 200 |
+
"""Apply a random proxy to the current session"""
|
| 201 |
+
if not self.proxy_finder:
|
| 202 |
+
return False
|
| 203 |
+
|
| 204 |
+
# First try to get a proxy of preferred type (http/https first, then socks5)
|
| 205 |
+
for proxy_type in self.proxy_types:
|
| 206 |
+
proxy = self.proxy_finder.get_random_proxy(proxy_type)
|
| 207 |
+
if proxy:
|
| 208 |
+
if proxy_type in ['http', 'https']:
|
| 209 |
+
self.session.proxies.update({
|
| 210 |
+
"http": f"http://{proxy}",
|
| 211 |
+
"https": f"http://{proxy}"
|
| 212 |
+
})
|
| 213 |
+
return True
|
| 214 |
+
elif proxy_type == 'socks4':
|
| 215 |
+
self.session.proxies.update({
|
| 216 |
+
"http": f"socks4://{proxy}",
|
| 217 |
+
"https": f"socks4://{proxy}"
|
| 218 |
+
})
|
| 219 |
+
return True
|
| 220 |
+
elif proxy_type == 'socks5':
|
| 221 |
+
self.session.proxies.update({
|
| 222 |
+
"http": f"socks5://{proxy}",
|
| 223 |
+
"https": f"socks5://{proxy}"
|
| 224 |
+
})
|
| 225 |
+
return True
|
| 226 |
+
|
| 227 |
+
# If no proxy found, return False
|
| 228 |
+
return False
|
| 229 |
+
|
| 230 |
+
def refresh_session(self):
|
| 231 |
+
"""Refresh the session with new headers and possibly a new proxy or IP"""
|
| 232 |
+
if self.use_random_user_agent:
|
| 233 |
+
self.headers['User-Agent'] = self.user_agent.random
|
| 234 |
+
|
| 235 |
+
# Apply a random proxy if proxy rotation is enabled
|
| 236 |
+
if self.use_proxy_rotation and self.proxy_finder:
|
| 237 |
+
proxy_applied = self._apply_random_proxy()
|
| 238 |
+
# If no proxy was applied, try to get new proxies
|
| 239 |
+
if not proxy_applied:
|
| 240 |
+
self.proxy_finder.get_proxies(self.proxy_types)
|
| 241 |
+
self._apply_random_proxy()
|
| 242 |
+
|
| 243 |
+
# Rotate IP if enabled
|
| 244 |
+
if self.use_ip_rotation and self.ip_rotator:
|
| 245 |
+
self.session = self.ip_rotator.get_session()
|
| 246 |
+
|
| 247 |
+
self.session.headers.update(self.headers)
|
| 248 |
+
|
| 249 |
+
def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
| 250 |
+
"""Make an HTTP request with automatic retry and proxy/user-agent rotation"""
|
| 251 |
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
| 252 |
+
max_retries = 3
|
| 253 |
+
retry_delay = 1
|
| 254 |
+
|
| 255 |
+
for attempt in range(max_retries):
|
| 256 |
+
try:
|
| 257 |
+
response = self.session.request(method, url, **kwargs)
|
| 258 |
+
response.raise_for_status()
|
| 259 |
+
return response
|
| 260 |
+
except requests.RequestException as e:
|
| 261 |
+
if attempt < max_retries - 1:
|
| 262 |
+
self.refresh_session()
|
| 263 |
+
time.sleep(retry_delay * (attempt + 1))
|
| 264 |
+
continue
|
| 265 |
+
raise e
|
| 266 |
+
|
| 267 |
+
def refresh_proxies(self):
|
| 268 |
+
"""Refresh all proxies by fetching new ones"""
|
| 269 |
+
if self.proxy_finder:
|
| 270 |
+
self.proxy_finder.get_proxies(self.proxy_types)
|
| 271 |
+
self._apply_random_proxy()
|
| 272 |
+
return True
|
| 273 |
+
return False
|
| 274 |
+
|
| 275 |
+
def __del__(self):
|
| 276 |
+
"""Clean up resources on deletion"""
|
| 277 |
+
if self.ip_rotator:
|
| 278 |
+
try:
|
| 279 |
+
self.ip_rotator.shutdown()
|
| 280 |
+
except:
|
| 281 |
+
pass
|
| 282 |
+
|
| 283 |
+
class Models:
|
| 284 |
+
def __init__(self, client: DeepInfraClient):
|
| 285 |
+
self.client = client
|
| 286 |
+
|
| 287 |
+
def list(self) -> Dict[str, Any]:
|
| 288 |
+
"""Get available models, similar to OpenAI's /v1/models endpoint"""
|
| 289 |
+
model_data = []
|
| 290 |
+
for model_id in self.client.AVAILABLE_MODELS:
|
| 291 |
+
model_data.append({
|
| 292 |
+
"id": model_id,
|
| 293 |
+
"object": "model",
|
| 294 |
+
"created": 1677610602,
|
| 295 |
+
"owned_by": "deepinfra"
|
| 296 |
+
})
|
| 297 |
+
|
| 298 |
+
return {
|
| 299 |
+
"object": "list",
|
| 300 |
+
"data": model_data
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
class ChatCompletions:
|
| 304 |
+
def __init__(self, client: DeepInfraClient):
|
| 305 |
+
self.client = client
|
| 306 |
+
|
| 307 |
+
def create(
|
| 308 |
+
self,
|
| 309 |
+
messages: List[Dict[str, str]],
|
| 310 |
+
model: str = None,
|
| 311 |
+
temperature: float = 0.7,
|
| 312 |
+
max_tokens: int = None,
|
| 313 |
+
stream: bool = False,
|
| 314 |
+
**kwargs
|
| 315 |
+
) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]:
|
| 316 |
+
"""Create a chat completion, similar to OpenAI's chat/completions endpoint"""
|
| 317 |
+
model = model or self.client.model
|
| 318 |
+
max_tokens = max_tokens or self.client.max_tokens
|
| 319 |
+
|
| 320 |
+
url = "openai/chat/completions"
|
| 321 |
+
|
| 322 |
+
# Prepare the payload for the API request
|
| 323 |
+
payload = {
|
| 324 |
+
"model": model,
|
| 325 |
+
"messages": messages,
|
| 326 |
+
"temperature": temperature,
|
| 327 |
+
"max_tokens": max_tokens,
|
| 328 |
+
"stream": stream
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
# Add any additional parameters
|
| 332 |
+
payload.update({k: v for k, v in kwargs.items() if v is not None})
|
| 333 |
+
|
| 334 |
+
if stream:
|
| 335 |
+
return self._handle_stream(url, payload)
|
| 336 |
+
else:
|
| 337 |
+
return self._handle_request(url, payload)
|
| 338 |
+
|
| 339 |
+
def _handle_request(self, url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 340 |
+
"""Handle non-streaming requests"""
|
| 341 |
+
try:
|
| 342 |
+
response = self.client._request(
|
| 343 |
+
"POST",
|
| 344 |
+
url,
|
| 345 |
+
json=payload,
|
| 346 |
+
timeout=self.client.timeout
|
| 347 |
+
)
|
| 348 |
+
return response.json()
|
| 349 |
+
except requests.RequestException as e:
|
| 350 |
+
error_message = f"Request failed: {str(e)}"
|
| 351 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 352 |
+
try:
|
| 353 |
+
error_data = e.response.json()
|
| 354 |
+
if 'error' in error_data:
|
| 355 |
+
error_message = f"API error: {error_data['error']}"
|
| 356 |
+
except:
|
| 357 |
+
error_message = f"API error: {e.response.text}"
|
| 358 |
+
|
| 359 |
+
raise Exception(error_message)
|
| 360 |
+
|
| 361 |
+
def _handle_stream(self, url: str, payload: Dict[str, Any]) -> Generator[Dict[str, Any], None, None]:
|
| 362 |
+
"""Handle streaming requests"""
|
| 363 |
+
try:
|
| 364 |
+
response = self.client._request(
|
| 365 |
+
"POST",
|
| 366 |
+
url,
|
| 367 |
+
json=payload,
|
| 368 |
+
stream=True,
|
| 369 |
+
timeout=self.client.timeout
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
for line in response.iter_lines(decode_unicode=True):
|
| 373 |
+
if line:
|
| 374 |
+
line = line.strip()
|
| 375 |
+
if line.startswith("data: "):
|
| 376 |
+
json_str = line[6:]
|
| 377 |
+
if json_str == "[DONE]":
|
| 378 |
+
break
|
| 379 |
+
try:
|
| 380 |
+
json_data = json.loads(json_str)
|
| 381 |
+
yield json_data
|
| 382 |
+
except json.JSONDecodeError:
|
| 383 |
+
continue
|
| 384 |
+
except requests.RequestException as e:
|
| 385 |
+
error_message = f"Stream request failed: {str(e)}"
|
| 386 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 387 |
+
try:
|
| 388 |
+
error_data = e.response.json()
|
| 389 |
+
if 'error' in error_data:
|
| 390 |
+
error_message = f"API error: {error_data['error']}"
|
| 391 |
+
except:
|
| 392 |
+
error_message = f"API error: {e.response.text}"
|
| 393 |
+
|
| 394 |
+
raise Exception(error_message)
|
| 395 |
+
|
| 396 |
+
class Completions:
|
| 397 |
+
def __init__(self, client: DeepInfraClient):
|
| 398 |
+
self.client = client
|
| 399 |
+
|
| 400 |
+
def create(
|
| 401 |
+
self,
|
| 402 |
+
prompt: str,
|
| 403 |
+
model: str = None,
|
| 404 |
+
temperature: float = 0.7,
|
| 405 |
+
max_tokens: int = None,
|
| 406 |
+
stream: bool = False,
|
| 407 |
+
**kwargs
|
| 408 |
+
) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]:
|
| 409 |
+
"""Create a completion, similar to OpenAI's completions endpoint"""
|
| 410 |
+
# Convert prompt to messages format for chat models
|
| 411 |
+
messages = [{"role": "user", "content": prompt}]
|
| 412 |
+
|
| 413 |
+
return self.client.chat.create(
|
| 414 |
+
messages=messages,
|
| 415 |
+
model=model,
|
| 416 |
+
temperature=temperature,
|
| 417 |
+
max_tokens=max_tokens,
|
| 418 |
+
stream=stream,
|
| 419 |
+
**kwargs
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
if __name__ == "__main__":
|
| 423 |
+
import time
|
| 424 |
+
from rich import print
|
| 425 |
+
|
| 426 |
+
# Example with random user agent, proxy rotation and IP rotation
|
| 427 |
+
client = DeepInfraClient(
|
| 428 |
+
use_random_user_agent=True,
|
| 429 |
+
use_ip_rotation=True,
|
| 430 |
+
use_proxy_rotation=True,
|
| 431 |
+
proxy_types=['http', 'socks5']
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# Get available models
|
| 435 |
+
models_response = client.models.list()
|
| 436 |
+
print("Available models:")
|
| 437 |
+
for model in models_response["data"][:5]: # Print first 5 models
|
| 438 |
+
print(f"- {model['id']}")
|
| 439 |
+
print("...")
|
| 440 |
+
|
| 441 |
+
# Non-streaming chat completion
|
| 442 |
+
chat_response = client.chat.create(
|
| 443 |
+
messages=[
|
| 444 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 445 |
+
{"role": "user", "content": "Write a short poem about AI"}
|
| 446 |
+
]
|
| 447 |
+
)
|
| 448 |
+
print("\nNon-streaming response:")
|
| 449 |
+
print(chat_response["choices"][0]["message"]["content"])
|
| 450 |
+
|
| 451 |
+
# Refresh proxies and try again with another request
|
| 452 |
+
print("\nRefreshing proxies and making another request...")
|
| 453 |
+
client.refresh_proxies()
|
| 454 |
+
client.refresh_session()
|
| 455 |
+
|
| 456 |
+
streaming_response = client.chat.create(
|
| 457 |
+
messages=[
|
| 458 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 459 |
+
{"role": "user", "content": "Tell me about the future of AI in 3 sentences."}
|
| 460 |
+
],
|
| 461 |
+
stream=True
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
print("\nStreaming response:")
|
| 465 |
+
full_response = ""
|
| 466 |
+
for chunk in streaming_response:
|
| 467 |
+
if 'choices' in chunk and len(chunk['choices']) > 0:
|
| 468 |
+
delta = chunk['choices'][0].get('delta', {})
|
| 469 |
+
if 'content' in delta:
|
| 470 |
+
content = delta['content']
|
| 471 |
+
print(content, end='', flush=True)
|
| 472 |
+
full_response += content
|
| 473 |
+
print("\n")
|
docker-compose.override.yml.example
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Example override file for docker-compose.yml
|
| 2 |
+
# Copy this file to docker-compose.override.yml and edit as needed
|
| 3 |
+
|
| 4 |
+
version: '3.8'
|
| 5 |
+
|
| 6 |
+
services:
|
| 7 |
+
# MongoDB service customization
|
| 8 |
+
mongodb:
|
| 9 |
+
# Example: Expose MongoDB port locally for debugging
|
| 10 |
+
ports:
|
| 11 |
+
- "27017:27017"
|
| 12 |
+
# Example: Add authentication
|
| 13 |
+
environment:
|
| 14 |
+
MONGO_INITDB_ROOT_USERNAME: your_custom_username
|
| 15 |
+
MONGO_INITDB_ROOT_PASSWORD: your_custom_password
|
| 16 |
+
# Example: Persistent volume on host
|
| 17 |
+
volumes:
|
| 18 |
+
- ./mongodb_data:/data/db
|
| 19 |
+
|
| 20 |
+
# PyScout API service customization
|
| 21 |
+
pyscout-api:
|
| 22 |
+
# Example: Use a different port
|
| 23 |
+
ports:
|
| 24 |
+
- "8080:8000"
|
| 25 |
+
# Example: Add debugging
|
| 26 |
+
environment:
|
| 27 |
+
- DEBUG=True
|
| 28 |
+
- LOG_LEVEL=DEBUG
|
| 29 |
+
- MONGODB_URI=mongodb://your_custom_username:your_custom_password@mongodb:27017/pyscout_ai?authSource=admin
|
| 30 |
+
|
| 31 |
+
# Gradio UI service customization
|
| 32 |
+
gradio-ui:
|
| 33 |
+
# Example: Use a different port
|
| 34 |
+
ports:
|
| 35 |
+
- "7000:7860"
|
| 36 |
+
# Example: Enable share link
|
| 37 |
+
environment:
|
| 38 |
+
- GRADIO_SHARE=true
|
| 39 |
+
- API_BASE_URL=http://pyscout-api:8000
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mongodb:
|
| 5 |
+
image: mongo:5.0
|
| 6 |
+
environment:
|
| 7 |
+
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-mongouser}
|
| 8 |
+
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-mongopassword}
|
| 9 |
+
volumes:
|
| 10 |
+
- mongo_data:/data/db
|
| 11 |
+
|
| 12 |
+
pyscout-api:
|
| 13 |
+
build: .
|
| 14 |
+
depends_on:
|
| 15 |
+
- mongodb
|
| 16 |
+
environment:
|
| 17 |
+
- MONGODB_URI=mongodb://${MONGO_USER:-mongouser}:${MONGO_PASSWORD:-mongopassword}@mongodb:27017/pyscout_ai?authSource=admin
|
| 18 |
+
- PYSCOUT_MODE=api
|
| 19 |
+
ports:
|
| 20 |
+
- "8000:8000"
|
| 21 |
+
|
| 22 |
+
gradio-ui:
|
| 23 |
+
build: .
|
| 24 |
+
depends_on:
|
| 25 |
+
- pyscout-api
|
| 26 |
+
environment:
|
| 27 |
+
- PYSCOUT_MODE=ui
|
| 28 |
+
- API_BASE_URL=http://pyscout-api:8000
|
| 29 |
+
ports:
|
| 30 |
+
- "7860:7860"
|
| 31 |
+
|
| 32 |
+
volumes:
|
| 33 |
+
mongo_data:
|
docker_entrypoint.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Docker entrypoint script that decides which component to run based on environment variables.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
import subprocess
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
def run_command(cmd):
|
| 12 |
+
print(f"Running command: {' '.join(cmd)}")
|
| 13 |
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
| 14 |
+
|
| 15 |
+
# Stream the output
|
| 16 |
+
for line in process.stdout:
|
| 17 |
+
sys.stdout.write(line)
|
| 18 |
+
sys.stdout.flush()
|
| 19 |
+
|
| 20 |
+
process.wait()
|
| 21 |
+
return process.returncode
|
| 22 |
+
|
| 23 |
+
def check_required_files():
|
| 24 |
+
"""Check if all required files exist"""
|
| 25 |
+
required_files = [
|
| 26 |
+
"pyscout_api.py",
|
| 27 |
+
"deepinfra_client.py",
|
| 28 |
+
"proxy_finder.py",
|
| 29 |
+
"db_helper.py",
|
| 30 |
+
"hf_utils.py",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
for file in required_files:
|
| 34 |
+
if not Path(file).exists():
|
| 35 |
+
print(f"ERROR: Required file '{file}' not found!")
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
def wait_for_mongodb():
|
| 41 |
+
"""Wait for MongoDB to be available"""
|
| 42 |
+
import time
|
| 43 |
+
import pymongo
|
| 44 |
+
|
| 45 |
+
mongo_uri = os.environ.get("MONGODB_URI")
|
| 46 |
+
if not mongo_uri:
|
| 47 |
+
print("MongoDB URI not found in environment variables, skipping connection check")
|
| 48 |
+
return True
|
| 49 |
+
|
| 50 |
+
max_attempts = 30
|
| 51 |
+
for attempt in range(max_attempts):
|
| 52 |
+
try:
|
| 53 |
+
client = pymongo.MongoClient(mongo_uri, serverSelectionTimeoutMS=5000)
|
| 54 |
+
client.admin.command('ping') # Simple command to check connection
|
| 55 |
+
print(f"MongoDB connection successful after {attempt+1} attempts")
|
| 56 |
+
return True
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"Attempt {attempt+1}/{max_attempts}: MongoDB not yet available. Waiting... ({str(e)})")
|
| 59 |
+
time.sleep(2)
|
| 60 |
+
|
| 61 |
+
print("ERROR: Failed to connect to MongoDB after multiple attempts")
|
| 62 |
+
return False
|
| 63 |
+
|
| 64 |
+
def main():
|
| 65 |
+
"""Main entry point for the Docker container"""
|
| 66 |
+
if not check_required_files():
|
| 67 |
+
sys.exit(1)
|
| 68 |
+
|
| 69 |
+
# Determine which component to run based on environment variable
|
| 70 |
+
mode = os.environ.get("PYSCOUT_MODE", "api").lower()
|
| 71 |
+
|
| 72 |
+
if mode == "api":
|
| 73 |
+
print("Starting PyScoutAI API server")
|
| 74 |
+
wait_for_mongodb()
|
| 75 |
+
cmd = ["python", "pyscout_api.py"]
|
| 76 |
+
return run_command(cmd)
|
| 77 |
+
|
| 78 |
+
elif mode == "ui":
|
| 79 |
+
print("Starting Gradio UI")
|
| 80 |
+
cmd = ["python", "app.py"]
|
| 81 |
+
return run_command(cmd)
|
| 82 |
+
|
| 83 |
+
elif mode == "all":
|
| 84 |
+
print("Starting both API server and UI")
|
| 85 |
+
# Start API server in background
|
| 86 |
+
api_process = subprocess.Popen(["python", "pyscout_api.py"])
|
| 87 |
+
time.sleep(5) # Wait for API to start
|
| 88 |
+
|
| 89 |
+
# Start UI in foreground
|
| 90 |
+
ui_cmd = ["python", "app.py"]
|
| 91 |
+
ui_code = run_command(ui_cmd)
|
| 92 |
+
|
| 93 |
+
# Kill API process when UI exits
|
| 94 |
+
api_process.terminate()
|
| 95 |
+
return ui_code
|
| 96 |
+
|
| 97 |
+
else:
|
| 98 |
+
print(f"ERROR: Unknown mode '{mode}'. Valid options: api, ui, all")
|
| 99 |
+
return 1
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
sys.exit(main())
|
hf_setup.py
ADDED
|
File without changes
|
hf_utils.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
+
from typing import Dict, Optional, List, Any
|
| 5 |
+
|
| 6 |
+
class HuggingFaceSpaceHelper:
|
| 7 |
+
"""Helper utilities for integrating with Hugging Face Spaces"""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.hf_token = os.getenv("HF_TOKEN")
|
| 11 |
+
self.space_name = os.getenv("HF_SPACE_NAME")
|
| 12 |
+
self.is_in_space = self._is_huggingface_space()
|
| 13 |
+
|
| 14 |
+
def _is_huggingface_space(self) -> bool:
|
| 15 |
+
"""Check if code is running in a Hugging Face Space"""
|
| 16 |
+
return os.path.exists("/opt/conda/bin/python") and os.getenv("SPACE_ID") is not None
|
| 17 |
+
|
| 18 |
+
def get_space_url(self) -> str:
|
| 19 |
+
"""Get the URL for the current HF space"""
|
| 20 |
+
if not self.space_name:
|
| 21 |
+
return "Not running in a named HF Space"
|
| 22 |
+
return f"https://huggingface.co/spaces/{self.space_name}"
|
| 23 |
+
|
| 24 |
+
def get_gradio_url(self) -> str:
|
| 25 |
+
"""Get the Gradio URL for the Space"""
|
| 26 |
+
if not self.space_name:
|
| 27 |
+
return ""
|
| 28 |
+
return f"https://{self.space_name}.hf.space"
|
| 29 |
+
|
| 30 |
+
def get_hostname(self) -> str:
|
| 31 |
+
"""Get the appropriate hostname for services in HF Spaces"""
|
| 32 |
+
if self.is_in_space:
|
| 33 |
+
# In HF Spaces, services on the same container are accessible via localhost
|
| 34 |
+
return "localhost"
|
| 35 |
+
return "0.0.0.0" # Default for local development
|
| 36 |
+
|
| 37 |
+
def get_port_mapping(self) -> Dict[int, int]:
|
| 38 |
+
"""Get mappings for ports in HF spaces vs local environment"""
|
| 39 |
+
# HF Spaces uses specific ports for various services
|
| 40 |
+
return {
|
| 41 |
+
7860: 7860, # Default Gradio port, available publicly
|
| 42 |
+
8000: 8000, # FastAPI server, available within container
|
| 43 |
+
8001: 8001, # Additional services, available within container
|
| 44 |
+
# Add more port mappings as needed
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def get_hf_metadata(self) -> Dict[str, Any]:
|
| 48 |
+
"""Get metadata about the HF Space environment"""
|
| 49 |
+
metadata = {
|
| 50 |
+
"is_huggingface_space": self.is_in_space,
|
| 51 |
+
"space_name": self.space_name,
|
| 52 |
+
"space_url": self.get_space_url(),
|
| 53 |
+
"gradio_url": self.get_gradio_url(),
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# Add environment-specific info for HF Spaces
|
| 57 |
+
if self.is_in_space:
|
| 58 |
+
metadata.update({
|
| 59 |
+
"space_id": os.getenv("SPACE_ID"),
|
| 60 |
+
"space_runtime": os.getenv("SPACE_RUNTIME"),
|
| 61 |
+
"container_hostname": os.getenv("HOSTNAME"),
|
| 62 |
+
"python_path": os.getenv("PYTHONPATH"),
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
return metadata
|
| 66 |
+
|
| 67 |
+
def get_env_file_path(self) -> str:
|
| 68 |
+
"""Get appropriate .env file path based on environment"""
|
| 69 |
+
if self.is_in_space:
|
| 70 |
+
return "/app/.env" # Default location in HF Spaces
|
| 71 |
+
return ".env" # Local development
|
| 72 |
+
|
| 73 |
+
def get_mongodb_uri(self) -> Optional[str]:
|
| 74 |
+
"""Get the MongoDB URI, potentially customized for HF Space environment"""
|
| 75 |
+
# Use environment variable first
|
| 76 |
+
mongo_uri = os.getenv("MONGODB_URI")
|
| 77 |
+
|
| 78 |
+
# If not available and we're in a Space, try to use HF Spaces secrets
|
| 79 |
+
if not mongo_uri and self.is_in_space:
|
| 80 |
+
try:
|
| 81 |
+
# HF Spaces stores secrets in a specific location
|
| 82 |
+
from huggingface_hub import get_space_runtime
|
| 83 |
+
runtime = get_space_runtime()
|
| 84 |
+
mongo_uri = runtime.get_secret("MONGODB_URI")
|
| 85 |
+
except:
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
return mongo_uri
|
| 89 |
+
|
| 90 |
+
def install_dependencies(self, packages: List[str]):
|
| 91 |
+
"""Install Python packages if needed (useful for HF Spaces)"""
|
| 92 |
+
if not self.is_in_space:
|
| 93 |
+
return # Skip in local environments
|
| 94 |
+
|
| 95 |
+
import subprocess
|
| 96 |
+
try:
|
| 97 |
+
subprocess.check_call(["pip", "install", "--no-cache-dir"] + packages)
|
| 98 |
+
print(f"Successfully installed: {', '.join(packages)}")
|
| 99 |
+
except subprocess.CalledProcessError as e:
|
| 100 |
+
print(f"Failed to install packages: {e}")
|
| 101 |
+
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
hf_helper = HuggingFaceSpaceHelper()
|
| 104 |
+
print(json.dumps(hf_helper.get_hf_metadata(), indent=2))
|
| 105 |
+
print(f"MongoDB URI: {hf_helper.get_mongodb_uri() or 'Not configured'}")
|
| 106 |
+
print(f"Environment file path: {hf_helper.get_env_file_path()}")
|
proxy_finder.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import datetime
|
| 3 |
+
import requests
|
| 4 |
+
import re
|
| 5 |
+
import random
|
| 6 |
+
import time
|
| 7 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 8 |
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
| 9 |
+
|
| 10 |
+
class ProxyFinder:
|
| 11 |
+
"""Finds and validates proxies from various online sources"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, verbose: bool = False):
|
| 14 |
+
"""Initialize ProxyFinder with optional verbose logging"""
|
| 15 |
+
self.verbose = verbose
|
| 16 |
+
self.api: Dict[str, List[str]] = {
|
| 17 |
+
'socks4': [
|
| 18 |
+
"https://api.proxyscrape.com/?request=displayproxies&proxytype=socks4&timeout=10000&country=all&simplified=true",
|
| 19 |
+
"https://www.proxy-list.download/api/v1/get?type=socks4",
|
| 20 |
+
"https://api.openproxylist.xyz/socks4.txt",
|
| 21 |
+
'https://openproxy.space/list/socks4',
|
| 22 |
+
'https://proxyspace.pro/socks4.txt',
|
| 23 |
+
"https://sunny9577.github.io/proxy-scraper/generated/socks4_proxies.txt",
|
| 24 |
+
'https://cdn.rei.my.id/proxy/SOCKS4',
|
| 25 |
+
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt",
|
| 26 |
+
"https://raw.githubusercontent.com/roosterkid/openproxylist/main/SOCKS4_RAW.txt",
|
| 27 |
+
'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks4.txt'
|
| 28 |
+
],
|
| 29 |
+
'socks5': [
|
| 30 |
+
"https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks5&timeout=10000&country=all&simplified=true",
|
| 31 |
+
"https://www.proxy-list.download/api/v1/get?type=socks5",
|
| 32 |
+
"https://api.openproxylist.xyz/socks5.txt",
|
| 33 |
+
'https://openproxy.space/list/socks5',
|
| 34 |
+
'https://spys.me/socks.txt',
|
| 35 |
+
'https://proxyspace.pro/socks5.txt',
|
| 36 |
+
"https://sunny9577.github.io/proxy-scraper/generated/socks5_proxies.txt",
|
| 37 |
+
'https://cdn.rei.my.id/proxy/SOCKS5',
|
| 38 |
+
'https://raw.githubusercontent.com/manuGMG/proxy-365/main/SOCKS5.txt',
|
| 39 |
+
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt",
|
| 40 |
+
"https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt"
|
| 41 |
+
],
|
| 42 |
+
'http': [
|
| 43 |
+
'https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/http/http.txt',
|
| 44 |
+
"https://github.com/TheSpeedX/PROXY-List/raw/refs/heads/master/http.txt",
|
| 45 |
+
"https://api.proxyscrape.com/?request=displayproxies&proxytype=http&timeout=10000&country=all&simplified=true",
|
| 46 |
+
"https://www.proxy-list.download/api/v1/get?type=http",
|
| 47 |
+
"https://api.openproxylist.xyz/http.txt",
|
| 48 |
+
'https://openproxy.space/list/http',
|
| 49 |
+
'https://proxyspace.pro/http.txt',
|
| 50 |
+
"https://sunny9577.github.io/proxy-scraper/generated/http_proxies.txt",
|
| 51 |
+
'https://cdn.rei.my.id/proxy/HTTP',
|
| 52 |
+
'https://raw.githubusercontent.com/UptimerBot/proxy-list/master/proxies/http.txt',
|
| 53 |
+
'https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt'
|
| 54 |
+
],
|
| 55 |
+
'https': [
|
| 56 |
+
'https://raw.githubusercontent.com/Firdoxx/proxy-list/main/https',
|
| 57 |
+
'https://raw.githubusercontent.com/roosterkid/openproxylist/main/HTTPS_RAW.txt',
|
| 58 |
+
'https://raw.githubusercontent.com/aslisk/proxyhttps/main/https.txt',
|
| 59 |
+
'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',
|
| 60 |
+
'https://raw.githubusercontent.com/zloi-user/hideip.me/main/https.txt',
|
| 61 |
+
'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/https.txt',
|
| 62 |
+
'https://raw.githubusercontent.com/Vann-Dev/proxy-list/main/proxies/https.txt'
|
| 63 |
+
],
|
| 64 |
+
'mixed': [
|
| 65 |
+
'https://github.com/jetkai/proxy-list/blob/main/online-proxies/txt/proxies.txt',
|
| 66 |
+
'https://raw.githubusercontent.com/mertguvencli/http-proxy-list/main/proxy-list/data.txt',
|
| 67 |
+
'https://raw.githubusercontent.com/a2u/free-proxy-list/master/free-proxy-list.txt',
|
| 68 |
+
'https://raw.githubusercontent.com/mishakorzik/Free-Proxy/main/proxy.txt',
|
| 69 |
+
'http://rootjazz.com/proxies/proxies.txt',
|
| 70 |
+
'https://multiproxy.org/txt_all/proxy.txt',
|
| 71 |
+
'https://proxy-spider.com/api/proxies.example.txt'
|
| 72 |
+
]
|
| 73 |
+
}
|
| 74 |
+
self.proxy_dict: Dict[str, List[str]] = {'socks4': [], 'socks5': [], 'http': [], 'https': []}
|
| 75 |
+
self.max_workers = 20 # Maximum workers for parallel requests
|
| 76 |
+
|
| 77 |
+
def log(self, *args):
|
| 78 |
+
"""Log messages if verbose mode is enabled"""
|
| 79 |
+
if self.verbose:
|
| 80 |
+
print(*args)
|
| 81 |
+
|
| 82 |
+
def extract_proxy(self, line: str) -> Optional[str]:
|
| 83 |
+
"""
|
| 84 |
+
Extracts the first occurrence of an IP:port from a line.
|
| 85 |
+
"""
|
| 86 |
+
match = re.search(r'(\d{1,3}(?:\.\d{1,3}){3}:\d{2,5})', line)
|
| 87 |
+
if match:
|
| 88 |
+
return match.group(1)
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
def fetch_from_url(self, url: str, proxy_type: str) -> List[str]:
|
| 92 |
+
"""
|
| 93 |
+
Fetches proxies from a given URL for the specified type.
|
| 94 |
+
Returns a list of valid proxies.
|
| 95 |
+
"""
|
| 96 |
+
proxy_list = []
|
| 97 |
+
try:
|
| 98 |
+
r = requests.get(url, timeout=5)
|
| 99 |
+
if r.status_code == requests.codes.ok:
|
| 100 |
+
for line in r.text.splitlines():
|
| 101 |
+
proxy = self.extract_proxy(line)
|
| 102 |
+
if proxy:
|
| 103 |
+
proxy_list.append(proxy)
|
| 104 |
+
self.log(f"Got {len(proxy_list)} {proxy_type} proxies from {url}")
|
| 105 |
+
return proxy_list
|
| 106 |
+
except Exception as e:
|
| 107 |
+
self.log(f"Failed to retrieve from {url}: {str(e)}")
|
| 108 |
+
return []
|
| 109 |
+
|
| 110 |
+
def fetch_proxies_parallel(self, proxy_type: str) -> List[str]:
|
| 111 |
+
"""
|
| 112 |
+
Fetch proxies in parallel for a specific type from all sources.
|
| 113 |
+
"""
|
| 114 |
+
if proxy_type not in self.api:
|
| 115 |
+
return []
|
| 116 |
+
|
| 117 |
+
all_proxies = []
|
| 118 |
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
| 119 |
+
futures = [executor.submit(self.fetch_from_url, url, proxy_type)
|
| 120 |
+
for url in self.api[proxy_type]]
|
| 121 |
+
for future in futures:
|
| 122 |
+
all_proxies.extend(future.result())
|
| 123 |
+
|
| 124 |
+
return list(set(all_proxies)) # Remove duplicates
|
| 125 |
+
|
| 126 |
+
def get_geonode_proxies(self) -> Dict[str, List[str]]:
|
| 127 |
+
"""
|
| 128 |
+
Retrieves proxies from geonode API
|
| 129 |
+
"""
|
| 130 |
+
result = {'http': [], 'socks4': [], 'socks5': [], 'https': []}
|
| 131 |
+
try:
|
| 132 |
+
url = 'https://proxylist.geonode.com/api/proxy-list?limit=500&sort_by=lastChecked&sort_type=desc'
|
| 133 |
+
response = requests.get(url, timeout=10)
|
| 134 |
+
|
| 135 |
+
if response.status_code == 200:
|
| 136 |
+
data = response.json()
|
| 137 |
+
for p in data.get('data', []):
|
| 138 |
+
for protocol in p.get('protocols', []):
|
| 139 |
+
protocol = protocol.lower()
|
| 140 |
+
# Map 'https' to 'http' in our dictionary
|
| 141 |
+
if protocol == 'https':
|
| 142 |
+
result['http'].append(f"{p['ip']}:{p['port']}")
|
| 143 |
+
elif protocol in result:
|
| 144 |
+
result[protocol].append(f"{p['ip']}:{p['port']}")
|
| 145 |
+
|
| 146 |
+
self.log(f"Got {sum(len(v) for v in result.values())} proxies from GeoNode")
|
| 147 |
+
except Exception as e:
|
| 148 |
+
self.log(f"Failed to fetch from GeoNode: {str(e)}")
|
| 149 |
+
|
| 150 |
+
return result
|
| 151 |
+
|
| 152 |
+
def get_checkerproxy_archive(self) -> Dict[str, List[str]]:
|
| 153 |
+
"""
|
| 154 |
+
Gets proxies from checkerproxy.net archive
|
| 155 |
+
"""
|
| 156 |
+
result = {'http': [], 'socks5': []}
|
| 157 |
+
|
| 158 |
+
for q in range(5): # Check only last 5 days to be faster
|
| 159 |
+
day = datetime.date.today() + datetime.timedelta(days=-q)
|
| 160 |
+
formatted_date = f'{day.year}-{day.month}-{day.day}'
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
r = requests.get(f'https://checkerproxy.net/api/archive/{formatted_date}', timeout=5)
|
| 164 |
+
if r.text != '[]':
|
| 165 |
+
json_result = json.loads(r.text)
|
| 166 |
+
for i in json_result:
|
| 167 |
+
# Skip internal IPs
|
| 168 |
+
if re.match(r"172\.|192\.168\.|10\.", i['ip']):
|
| 169 |
+
continue
|
| 170 |
+
|
| 171 |
+
addr = i.get('addr')
|
| 172 |
+
if not addr:
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
+
if i['type'] in [1, 2]:
|
| 176 |
+
result['http'].append(addr)
|
| 177 |
+
if i['type'] == 4:
|
| 178 |
+
result['socks5'].append(addr)
|
| 179 |
+
|
| 180 |
+
self.log(f"Got {len(result['http'])} http and {len(result['socks5'])} socks5 proxies from CheckerProxy for {formatted_date}")
|
| 181 |
+
except Exception as e:
|
| 182 |
+
self.log(f"Failed to get archive for {formatted_date}: {str(e)}")
|
| 183 |
+
|
| 184 |
+
return result
|
| 185 |
+
|
| 186 |
+
def get_proxies(self, proxy_types: List[str] = None) -> Dict[str, List[str]]:
|
| 187 |
+
"""
|
| 188 |
+
Get proxies of the specified types. If None, get all types.
|
| 189 |
+
Returns a dictionary with proxy lists for each type.
|
| 190 |
+
"""
|
| 191 |
+
if proxy_types is None:
|
| 192 |
+
proxy_types = ['http', 'https', 'socks4', 'socks5']
|
| 193 |
+
|
| 194 |
+
# Reset proxy dictionary
|
| 195 |
+
self.proxy_dict = {'socks4': [], 'socks5': [], 'http': [], 'https': []}
|
| 196 |
+
|
| 197 |
+
self.log("Starting proxy retrieval process")
|
| 198 |
+
|
| 199 |
+
# Fetch from regular sources in parallel for each type
|
| 200 |
+
for ptype in proxy_types:
|
| 201 |
+
if ptype in self.api:
|
| 202 |
+
self.log(f"Processing {ptype} proxy sources")
|
| 203 |
+
proxies = self.fetch_proxies_parallel(ptype)
|
| 204 |
+
self.proxy_dict[ptype].extend(proxies)
|
| 205 |
+
|
| 206 |
+
# Add proxies from GeoNode
|
| 207 |
+
geonode_proxies = self.get_geonode_proxies()
|
| 208 |
+
for ptype, proxies in geonode_proxies.items():
|
| 209 |
+
if ptype in proxy_types:
|
| 210 |
+
self.proxy_dict[ptype].extend(proxies)
|
| 211 |
+
|
| 212 |
+
# Add proxies from CheckerProxy
|
| 213 |
+
checker_proxies = self.get_checkerproxy_archive()
|
| 214 |
+
for ptype, proxies in checker_proxies.items():
|
| 215 |
+
if ptype in proxy_types:
|
| 216 |
+
self.proxy_dict[ptype].extend(proxies)
|
| 217 |
+
|
| 218 |
+
# Process "mixed" sources if any proxy type is requested
|
| 219 |
+
if proxy_types:
|
| 220 |
+
self.log("Processing mixed proxy sources")
|
| 221 |
+
for url in self.api.get('mixed', []):
|
| 222 |
+
try:
|
| 223 |
+
proxies = self.fetch_from_url(url, 'mixed')
|
| 224 |
+
# Distribute mixed proxies equally among requested types
|
| 225 |
+
if proxies:
|
| 226 |
+
chunks = len(proxy_types)
|
| 227 |
+
chunk_size = len(proxies) // chunks if chunks > 0 else 0
|
| 228 |
+
for i, ptype in enumerate(proxy_types):
|
| 229 |
+
start = i * chunk_size
|
| 230 |
+
end = start + chunk_size if i < chunks - 1 else len(proxies)
|
| 231 |
+
self.proxy_dict[ptype].extend(proxies[start:end])
|
| 232 |
+
except Exception as e:
|
| 233 |
+
self.log(f"Failed to process mixed proxy source: {str(e)}")
|
| 234 |
+
|
| 235 |
+
# Remove duplicates for all types
|
| 236 |
+
for key in self.proxy_dict:
|
| 237 |
+
original_count = len(self.proxy_dict[key])
|
| 238 |
+
self.proxy_dict[key] = list(set(self.proxy_dict[key]))
|
| 239 |
+
new_count = len(self.proxy_dict[key])
|
| 240 |
+
self.log(f"Removed {original_count - new_count} duplicate {key} proxies")
|
| 241 |
+
|
| 242 |
+
self.log("Proxy retrieval process completed")
|
| 243 |
+
return self.proxy_dict
|
| 244 |
+
|
| 245 |
+
def get_random_proxy(self, proxy_type: str = None) -> Optional[str]:
|
| 246 |
+
"""
|
| 247 |
+
Returns a random proxy of the specified type.
|
| 248 |
+
If type is None, returns a random proxy from any type.
|
| 249 |
+
"""
|
| 250 |
+
if proxy_type and proxy_type in self.proxy_dict and self.proxy_dict[proxy_type]:
|
| 251 |
+
return random.choice(self.proxy_dict[proxy_type])
|
| 252 |
+
elif not proxy_type:
|
| 253 |
+
# Combine all proxy types and get a random one
|
| 254 |
+
all_proxies = []
|
| 255 |
+
for ptype in self.proxy_dict:
|
| 256 |
+
all_proxies.extend(self.proxy_dict[ptype])
|
| 257 |
+
if all_proxies:
|
| 258 |
+
return random.choice(all_proxies)
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
def get_random_proxies(self, count: int = 10, proxy_type: str = None) -> List[str]:
|
| 262 |
+
"""
|
| 263 |
+
Returns a list of random proxies of the specified type.
|
| 264 |
+
If type is None, returns random proxies from any type.
|
| 265 |
+
"""
|
| 266 |
+
if proxy_type and proxy_type in self.proxy_dict:
|
| 267 |
+
proxies = self.proxy_dict[proxy_type]
|
| 268 |
+
else:
|
| 269 |
+
# Combine all proxy types
|
| 270 |
+
proxies = []
|
| 271 |
+
for ptype in self.proxy_dict:
|
| 272 |
+
proxies.extend(self.proxy_dict[ptype])
|
| 273 |
+
|
| 274 |
+
# Get random proxies up to count or as many as available
|
| 275 |
+
if not proxies:
|
| 276 |
+
return []
|
| 277 |
+
|
| 278 |
+
return random.sample(proxies, min(count, len(proxies)))
|
| 279 |
+
|
| 280 |
+
if __name__ == "__main__":
|
| 281 |
+
# Example usage
|
| 282 |
+
finder = ProxyFinder(verbose=True)
|
| 283 |
+
proxies = finder.get_proxies(['http', 'socks5'])
|
| 284 |
+
|
| 285 |
+
print("\nSummary:")
|
| 286 |
+
for ptype, proxy_list in proxies.items():
|
| 287 |
+
print(f"{ptype}: {len(proxy_list)} proxies")
|
| 288 |
+
|
| 289 |
+
print("\nRandom HTTP proxy:", finder.get_random_proxy('http'))
|
| 290 |
+
print("\nRandom SOCKS5 proxies:", finder.get_random_proxies(5, 'socks5'))
|
pyscout_api.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
+
import uuid
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, List, Optional, Union, Any
|
| 7 |
+
from fastapi import FastAPI, HTTPException, Depends, Request, status, Body
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 10 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 11 |
+
from pydantic import BaseModel, Field, EmailStr
|
| 12 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 13 |
+
from slowapi.util import get_remote_address
|
| 14 |
+
from slowapi.errors import RateLimitExceeded
|
| 15 |
+
import uvicorn
|
| 16 |
+
|
| 17 |
+
from db_helper import MongoDBHelper
|
| 18 |
+
from deepinfra_client import DeepInfraClient
|
| 19 |
+
from hf_utils import HuggingFaceSpaceHelper
|
| 20 |
+
|
| 21 |
+
# Initialize Hugging Face Space helper
|
| 22 |
+
hf_helper = HuggingFaceSpaceHelper()
|
| 23 |
+
|
| 24 |
+
# Install required packages for HF Spaces if needed
|
| 25 |
+
if hf_helper.is_in_space:
|
| 26 |
+
hf_helper.install_dependencies([
|
| 27 |
+
"pymongo", "python-dotenv", "fastapi", "uvicorn", "slowapi",
|
| 28 |
+
"fake-useragent", "requests-ip-rotator", "pydantic[email]"
|
| 29 |
+
])
|
| 30 |
+
|
| 31 |
+
# Initialize FastAPI app
|
| 32 |
+
app = FastAPI(
|
| 33 |
+
title="PyScoutAI API",
|
| 34 |
+
description="An OpenAI-compatible API that provides access to DeepInfra models with enhanced features",
|
| 35 |
+
version="1.0.0"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Setup rate limiting
|
| 39 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 40 |
+
app.state.limiter = limiter
|
| 41 |
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 42 |
+
|
| 43 |
+
# Set up CORS
|
| 44 |
+
app.add_middleware(
|
| 45 |
+
CORSMiddleware,
|
| 46 |
+
allow_origins=["*"],
|
| 47 |
+
allow_credentials=True,
|
| 48 |
+
allow_methods=["*"],
|
| 49 |
+
allow_headers=["*"],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Security
|
| 53 |
+
security = HTTPBearer(auto_error=False)
|
| 54 |
+
|
| 55 |
+
# Database helper
|
| 56 |
+
try:
|
| 57 |
+
db = MongoDBHelper(hf_helper.get_mongodb_uri())
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Warning: MongoDB connection failed: {e}")
|
| 60 |
+
print("API key authentication will not work!")
|
| 61 |
+
db = None
|
| 62 |
+
|
| 63 |
+
# Models for requests and responses
|
| 64 |
+
class Message(BaseModel):
|
| 65 |
+
role: str
|
| 66 |
+
content: Optional[str] = None
|
| 67 |
+
name: Optional[str] = None
|
| 68 |
+
|
| 69 |
+
class ChatCompletionRequest(BaseModel):
|
| 70 |
+
model: str
|
| 71 |
+
messages: List[Message]
|
| 72 |
+
temperature: Optional[float] = 0.7
|
| 73 |
+
top_p: Optional[float] = 1.0
|
| 74 |
+
n: Optional[int] = 1
|
| 75 |
+
stream: Optional[bool] = False
|
| 76 |
+
max_tokens: Optional[int] = None
|
| 77 |
+
presence_penalty: Optional[float] = 0.0
|
| 78 |
+
frequency_penalty: Optional[float] = 0.0
|
| 79 |
+
user: Optional[str] = None
|
| 80 |
+
|
| 81 |
+
class CompletionRequest(BaseModel):
|
| 82 |
+
model: str
|
| 83 |
+
prompt: Union[str, List[str]]
|
| 84 |
+
temperature: Optional[float] = 0.7
|
| 85 |
+
top_p: Optional[float] = 1.0
|
| 86 |
+
n: Optional[int] = 1
|
| 87 |
+
stream: Optional[bool] = False
|
| 88 |
+
max_tokens: Optional[int] = None
|
| 89 |
+
presence_penalty: Optional[float] = 0.0
|
| 90 |
+
frequency_penalty: Optional[float] = 0.0
|
| 91 |
+
user: Optional[str] = None
|
| 92 |
+
|
| 93 |
+
class UserCreate(BaseModel):
|
| 94 |
+
email: EmailStr
|
| 95 |
+
name: str
|
| 96 |
+
organization: Optional[str] = None
|
| 97 |
+
|
| 98 |
+
class APIKeyCreate(BaseModel):
|
| 99 |
+
name: str = "Default API Key"
|
| 100 |
+
user_id: str
|
| 101 |
+
|
| 102 |
+
class APIKeyResponse(BaseModel):
|
| 103 |
+
key: str
|
| 104 |
+
name: str
|
| 105 |
+
created_at: str
|
| 106 |
+
|
| 107 |
+
# API clients storage (one per API key)
|
| 108 |
+
clients: Dict[str, DeepInfraClient] = {}
|
| 109 |
+
|
| 110 |
+
# Helper function to get the API key from the request
|
| 111 |
+
async def get_api_key(
|
| 112 |
+
request: Request,
|
| 113 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
| 114 |
+
) -> Optional[str]:
|
| 115 |
+
# Check Authorization header
|
| 116 |
+
if credentials:
|
| 117 |
+
return credentials.credentials
|
| 118 |
+
|
| 119 |
+
# Check for API key in the request headers
|
| 120 |
+
if "Authorization" in request.headers:
|
| 121 |
+
auth = request.headers["Authorization"]
|
| 122 |
+
if auth.startswith("Bearer "):
|
| 123 |
+
return auth.replace("Bearer ", "")
|
| 124 |
+
|
| 125 |
+
if "x-api-key" in request.headers:
|
| 126 |
+
return request.headers["x-api-key"]
|
| 127 |
+
|
| 128 |
+
# Check for API key in query parameters
|
| 129 |
+
api_key = request.query_params.get("api_key")
|
| 130 |
+
if api_key:
|
| 131 |
+
return api_key
|
| 132 |
+
|
| 133 |
+
# No API key found, return None
|
| 134 |
+
return None
|
| 135 |
+
|
| 136 |
+
# Helper function to validate a PyScout API key and get user info
|
| 137 |
+
async def get_user_info(api_key: Optional[str] = Depends(get_api_key)) -> Dict[str, Any]:
|
| 138 |
+
if not api_key:
|
| 139 |
+
raise HTTPException(
|
| 140 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 141 |
+
detail="API key is required",
|
| 142 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Skip validation if DB is not connected (development mode)
|
| 146 |
+
if not db:
|
| 147 |
+
return {"user_id": "development", "key": api_key}
|
| 148 |
+
|
| 149 |
+
# Check if key starts with PyScoutAI-
|
| 150 |
+
if not api_key.startswith("PyScoutAI-"):
|
| 151 |
+
raise HTTPException(
|
| 152 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 153 |
+
detail="Invalid API key format",
|
| 154 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Validate the API key
|
| 158 |
+
user_info = db.validate_api_key(api_key)
|
| 159 |
+
if not user_info:
|
| 160 |
+
raise HTTPException(
|
| 161 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 162 |
+
detail="Invalid API key",
|
| 163 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Check rate limits
|
| 167 |
+
rate_limit = db.check_rate_limit(api_key)
|
| 168 |
+
if not rate_limit["allowed"]:
|
| 169 |
+
raise HTTPException(
|
| 170 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 171 |
+
detail=rate_limit["reason"]
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return user_info
|
| 175 |
+
|
| 176 |
+
# Helper function to get or create a client
|
| 177 |
+
def get_client(api_key: str) -> DeepInfraClient:
|
| 178 |
+
if api_key not in clients:
|
| 179 |
+
# Create a client with IP rotation and random user agent
|
| 180 |
+
clients[api_key] = DeepInfraClient(
|
| 181 |
+
use_random_user_agent=True,
|
| 182 |
+
use_proxy_rotation=True,
|
| 183 |
+
use_ip_rotation=True
|
| 184 |
+
)
|
| 185 |
+
return clients[api_key]
|
| 186 |
+
|
| 187 |
+
@app.get("/")
|
| 188 |
+
async def root():
|
| 189 |
+
metadata = hf_helper.get_hf_metadata()
|
| 190 |
+
return {
|
| 191 |
+
"message": "Welcome to PyScoutAI API",
|
| 192 |
+
"documentation": "/docs",
|
| 193 |
+
"environment": "Hugging Face Space" if hf_helper.is_in_space else "Local",
|
| 194 |
+
"endpoints": [
|
| 195 |
+
"/v1/models",
|
| 196 |
+
"/v1/chat/completions",
|
| 197 |
+
"/v1/completions"
|
| 198 |
+
],
|
| 199 |
+
**metadata
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
@app.get("/v1/models")
|
| 203 |
+
@limiter.limit("20/minute")
|
| 204 |
+
async def list_models(
|
| 205 |
+
request: Request,
|
| 206 |
+
user_info: Dict[str, Any] = Depends(get_user_info)
|
| 207 |
+
):
|
| 208 |
+
api_key = user_info["key"]
|
| 209 |
+
client = get_client(api_key)
|
| 210 |
+
try:
|
| 211 |
+
models = await asyncio.to_thread(client.models.list)
|
| 212 |
+
# Log the API usage
|
| 213 |
+
if db:
|
| 214 |
+
db.log_api_usage(api_key, "/v1/models", 0)
|
| 215 |
+
return models
|
| 216 |
+
except Exception as e:
|
| 217 |
+
raise HTTPException(status_code=500, detail=f"Error listing models: {str(e)}")
|
| 218 |
+
|
| 219 |
+
@app.post("/v1/chat/completions")
|
| 220 |
+
@limiter.limit("60/minute")
|
| 221 |
+
async def create_chat_completion(
|
| 222 |
+
request: Request,
|
| 223 |
+
body: ChatCompletionRequest,
|
| 224 |
+
user_info: Dict[str, Any] = Depends(get_user_info)
|
| 225 |
+
):
|
| 226 |
+
api_key = user_info["key"]
|
| 227 |
+
client = get_client(api_key)
|
| 228 |
+
|
| 229 |
+
try:
|
| 230 |
+
# Prepare the messages
|
| 231 |
+
messages = [{"role": msg.role, "content": msg.content} for msg in body.messages if msg.content is not None]
|
| 232 |
+
|
| 233 |
+
kwargs = {
|
| 234 |
+
"model": body.model,
|
| 235 |
+
"temperature": body.temperature,
|
| 236 |
+
"max_tokens": body.max_tokens,
|
| 237 |
+
"stream": body.stream,
|
| 238 |
+
"top_p": body.top_p,
|
| 239 |
+
"presence_penalty": body.presence_penalty,
|
| 240 |
+
"frequency_penalty": body.frequency_penalty,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
if body.stream:
|
| 244 |
+
async def generate_stream():
|
| 245 |
+
response_stream = await asyncio.to_thread(
|
| 246 |
+
client.chat.create,
|
| 247 |
+
messages=messages,
|
| 248 |
+
**kwargs
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
total_tokens = 0
|
| 252 |
+
for chunk in response_stream:
|
| 253 |
+
# Track token usage for each chunk if available
|
| 254 |
+
if 'usage' in chunk and chunk['usage']:
|
| 255 |
+
total_tokens += chunk['usage'].get('total_tokens', 0)
|
| 256 |
+
|
| 257 |
+
yield f"data: {json.dumps(chunk)}\n\n"
|
| 258 |
+
|
| 259 |
+
# Log API usage at the end of streaming
|
| 260 |
+
if db:
|
| 261 |
+
db.log_api_usage(api_key, "/v1/chat/completions", total_tokens, body.model)
|
| 262 |
+
|
| 263 |
+
yield "data: [DONE]\n\n"
|
| 264 |
+
|
| 265 |
+
return StreamingResponse(
|
| 266 |
+
generate_stream(),
|
| 267 |
+
media_type="text/event-stream"
|
| 268 |
+
)
|
| 269 |
+
else:
|
| 270 |
+
response = await asyncio.to_thread(
|
| 271 |
+
client.chat.create,
|
| 272 |
+
messages=messages,
|
| 273 |
+
**kwargs
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Log the API usage
|
| 277 |
+
if db and 'usage' in response:
|
| 278 |
+
total_tokens = response['usage'].get('total_tokens', 0)
|
| 279 |
+
db.log_api_usage(api_key, "/v1/chat/completions", total_tokens, body.model)
|
| 280 |
+
|
| 281 |
+
return response
|
| 282 |
+
|
| 283 |
+
except Exception as e:
|
| 284 |
+
raise HTTPException(status_code=500, detail=f"Error generating chat completion: {str(e)}")
|
| 285 |
+
|
| 286 |
+
@app.post("/v1/completions")
|
| 287 |
+
@limiter.limit("60/minute")
|
| 288 |
+
async def create_completion(
|
| 289 |
+
request: Request,
|
| 290 |
+
body: CompletionRequest,
|
| 291 |
+
user_info: Dict[str, Any] = Depends(get_user_info)
|
| 292 |
+
):
|
| 293 |
+
api_key = user_info["key"]
|
| 294 |
+
client = get_client(api_key)
|
| 295 |
+
|
| 296 |
+
try:
|
| 297 |
+
# Handle different prompt types
|
| 298 |
+
prompt = body.prompt
|
| 299 |
+
if isinstance(prompt, list):
|
| 300 |
+
prompt = prompt[0] # Take the first prompt if it's a list
|
| 301 |
+
|
| 302 |
+
kwargs = {
|
| 303 |
+
"model": body.model,
|
| 304 |
+
"temperature": body.temperature,
|
| 305 |
+
"max_tokens": body.max_tokens,
|
| 306 |
+
"stream": body.stream,
|
| 307 |
+
"top_p": body.top_p,
|
| 308 |
+
"presence_penalty": body.presence_penalty,
|
| 309 |
+
"frequency_penalty": body.frequency_penalty,
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
if body.stream:
|
| 313 |
+
async def generate_stream():
|
| 314 |
+
response_stream = await asyncio.to_thread(
|
| 315 |
+
client.completions.create,
|
| 316 |
+
prompt=prompt,
|
| 317 |
+
**kwargs
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
total_tokens = 0
|
| 321 |
+
for chunk in response_stream:
|
| 322 |
+
if 'usage' in chunk and chunk['usage']:
|
| 323 |
+
total_tokens += chunk['usage'].get('total_tokens', 0)
|
| 324 |
+
|
| 325 |
+
yield f"data: {json.dumps(chunk)}\n\n"
|
| 326 |
+
|
| 327 |
+
# Log API usage at the end of streaming
|
| 328 |
+
if db:
|
| 329 |
+
db.log_api_usage(api_key, "/v1/completions", total_tokens, body.model)
|
| 330 |
+
|
| 331 |
+
yield "data: [DONE]\n\n"
|
| 332 |
+
|
| 333 |
+
return StreamingResponse(
|
| 334 |
+
generate_stream(),
|
| 335 |
+
media_type="text/event-stream"
|
| 336 |
+
)
|
| 337 |
+
else:
|
| 338 |
+
response = await asyncio.to_thread(
|
| 339 |
+
client.completions.create,
|
| 340 |
+
prompt=prompt,
|
| 341 |
+
**kwargs
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
# Log the API usage
|
| 345 |
+
if db and 'usage' in response:
|
| 346 |
+
total_tokens = response['usage'].get('total_tokens', 0)
|
| 347 |
+
db.log_api_usage(api_key, "/v1/completions", total_tokens, body.model)
|
| 348 |
+
|
| 349 |
+
return response
|
| 350 |
+
|
| 351 |
+
except Exception as e:
|
| 352 |
+
raise HTTPException(status_code=500, detail=f"Error generating completion: {str(e)}")
|
| 353 |
+
|
| 354 |
+
@app.get("/health")
|
| 355 |
+
async def health_check():
|
| 356 |
+
status_info = {"api": "ok"}
|
| 357 |
+
|
| 358 |
+
# Check MongoDB connection
|
| 359 |
+
if db:
|
| 360 |
+
try:
|
| 361 |
+
# Simple operation to check connection
|
| 362 |
+
db.api_keys_collection.find_one({})
|
| 363 |
+
status_info["database"] = "ok"
|
| 364 |
+
except Exception as e:
|
| 365 |
+
status_info["database"] = f"error: {str(e)}"
|
| 366 |
+
else:
|
| 367 |
+
status_info["database"] = "not configured"
|
| 368 |
+
|
| 369 |
+
# Add Hugging Face Space info
|
| 370 |
+
if hf_helper.is_in_space:
|
| 371 |
+
status_info["environment"] = "Hugging Face Space"
|
| 372 |
+
status_info["space_name"] = hf_helper.space_name
|
| 373 |
+
else:
|
| 374 |
+
status_info["environment"] = "Local"
|
| 375 |
+
|
| 376 |
+
return status_info
|
| 377 |
+
|
| 378 |
+
# API Key Management Endpoints
|
| 379 |
+
@app.post("/v1/api_keys", response_model=APIKeyResponse)
|
| 380 |
+
async def create_api_key(body: APIKeyCreate):
|
| 381 |
+
if not db:
|
| 382 |
+
raise HTTPException(status_code=500, detail="Database not configured")
|
| 383 |
+
|
| 384 |
+
try:
|
| 385 |
+
api_key = db.generate_api_key(body.user_id, body.name)
|
| 386 |
+
key_data = db.validate_api_key(api_key)
|
| 387 |
+
return {
|
| 388 |
+
"key": api_key,
|
| 389 |
+
"name": key_data["name"],
|
| 390 |
+
"created_at": key_data["created_at"].isoformat()
|
| 391 |
+
}
|
| 392 |
+
except Exception as e:
|
| 393 |
+
raise HTTPException(status_code=500, detail=f"Error creating API key: {str(e)}")
|
| 394 |
+
|
| 395 |
+
@app.get("/v1/api_keys")
|
| 396 |
+
async def list_api_keys(user_id: str):
|
| 397 |
+
if not db:
|
| 398 |
+
raise HTTPException(status_code=500, detail="Database not configured")
|
| 399 |
+
|
| 400 |
+
keys = db.get_user_api_keys(user_id)
|
| 401 |
+
for key in keys:
|
| 402 |
+
if "created_at" in key:
|
| 403 |
+
key["created_at"] = key["created_at"].isoformat()
|
| 404 |
+
if "last_used" in key and key["last_used"]:
|
| 405 |
+
key["last_used"] = key["last_used"].isoformat()
|
| 406 |
+
|
| 407 |
+
return {"keys": keys}
|
| 408 |
+
|
| 409 |
+
@app.post("/v1/api_keys/revoke")
|
| 410 |
+
async def revoke_api_key(api_key: str):
|
| 411 |
+
if not db:
|
| 412 |
+
raise HTTPException(status_code=500, detail="Database not configured")
|
| 413 |
+
|
| 414 |
+
success = db.revoke_api_key(api_key)
|
| 415 |
+
if not success:
|
| 416 |
+
raise HTTPException(status_code=404, detail="API key not found")
|
| 417 |
+
|
| 418 |
+
return {"message": "API key revoked successfully"}
|
| 419 |
+
|
| 420 |
+
# Clean up IP rotator clients on shutdown
|
| 421 |
+
@app.on_event("shutdown")
|
| 422 |
+
async def cleanup_clients():
|
| 423 |
+
for client in clients.values():
|
| 424 |
+
try:
|
| 425 |
+
if hasattr(client, 'ip_rotator') and client.ip_rotator:
|
| 426 |
+
client.ip_rotator.shutdown()
|
| 427 |
+
except:
|
| 428 |
+
pass
|
| 429 |
+
|
| 430 |
+
if __name__ == "__main__":
|
| 431 |
+
# Get host and port based on environment
|
| 432 |
+
host = hf_helper.get_hostname()
|
| 433 |
+
port = 8000 # Default port for FastAPI
|
| 434 |
+
|
| 435 |
+
print(f"Starting PyScoutAI API on http://{host}:{port}")
|
| 436 |
+
print(f"Environment: {'Hugging Face Space' if hf_helper.is_in_space else 'Local'}")
|
| 437 |
+
|
| 438 |
+
uvicorn.run("pyscout_api:app", host=host, port=port, reload=not hf_helper.is_in_space)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.4.2
|
| 4 |
+
requests==2.31.0
|
| 5 |
+
slowapi==0.1.8
|
| 6 |
+
python-dotenv==1.0.0
|
| 7 |
+
rich==13.6.0
|
| 8 |
+
fake-useragent==1.2.1
|
| 9 |
+
requests-ip-rotator==1.0.14
|
run_api_server.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uvicorn
|
| 3 |
+
import argparse
|
| 4 |
+
from rich.console import Console
|
| 5 |
+
from rich.panel import Panel
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Load environment variables from .env file if present
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
console = Console()
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
parser = argparse.ArgumentParser(description='Run the OpenAI-compatible API server for DeepInfra')
|
| 15 |
+
parser.add_argument('--host', default='0.0.0.0', help='Host to bind the server to')
|
| 16 |
+
parser.add_argument('--port', type=int, default=8000, help='Port to bind the server to')
|
| 17 |
+
parser.add_argument('--reload', action='store_true', help='Enable auto-reload for development')
|
| 18 |
+
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
|
| 21 |
+
console.print(Panel.fit(
|
| 22 |
+
"[bold green]DeepInfra OpenAI-Compatible API Server[/bold green]\n"
|
| 23 |
+
f"Starting server on http://{args.host}:{args.port}\n"
|
| 24 |
+
"Press Ctrl+C to stop the server.",
|
| 25 |
+
title="Server Info",
|
| 26 |
+
border_style="blue"
|
| 27 |
+
))
|
| 28 |
+
|
| 29 |
+
# Additional info on endpoints
|
| 30 |
+
console.print("[bold cyan]Available endpoints:[/bold cyan]")
|
| 31 |
+
console.print("- [yellow]/v1/models[/yellow] - List available models")
|
| 32 |
+
console.print("- [yellow]/v1/chat/completions[/yellow] - Chat completions endpoint")
|
| 33 |
+
console.print("- [yellow]/v1/completions[/yellow] - Text completions endpoint")
|
| 34 |
+
console.print("- [yellow]/health[/yellow] - Health check endpoint")
|
| 35 |
+
console.print("\nAPI documentation available at [link]http://localhost:8000/docs[/link]")
|
| 36 |
+
|
| 37 |
+
# Run the server
|
| 38 |
+
uvicorn.run(
|
| 39 |
+
"openai_compatible_api:app",
|
| 40 |
+
host=args.host,
|
| 41 |
+
port=args.port,
|
| 42 |
+
reload=args.reload
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|