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()
|