bibibi12345 commited on
Commit
3a68c3f
·
1 Parent(s): c08ee29

first commit

Browse files
Files changed (5) hide show
  1. Dockerfile +17 -0
  2. README.md +13 -0
  3. freeplay2api.py +675 -0
  4. proxy_pool.py +211 -0
  5. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Start from a standard Python base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy all project files into the container
8
+ COPY . .
9
+
10
+ # Install the Python dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Expose the application port
14
+ EXPOSE 7860
15
+
16
+ # Specify the command to run the application
17
+ CMD ["python", "freeplay2api.py"]
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FreePlay2API
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+ ## How to Use
10
+
11
+ 1. **Set the API Key**: This application requires an API key for authentication. You need to set a secret environment variable named `API_KEY` in your Hugging Face Space settings. The value can be any string you choose.
12
+ 2. **Start the Application**: Once the environment variable is set, the application will start, and you can access the API.
13
+ 3. **Accessing the API**: The API will be available at the port specified in the Dockerfile (default is 7860). You can use the API key you set in the `Authorization` header as a Bearer token.
freeplay2api.py ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ import uuid
5
+ import threading
6
+ import logging
7
+ import asyncio
8
+ import concurrent.futures
9
+ from typing import Any, List, Optional, Dict, Generator, Union
10
+
11
+ from proxy_pool import ProxyPool
12
+ from fastapi import FastAPI, HTTPException, Depends, Response, Request
13
+ from fastapi.responses import StreamingResponse, JSONResponse
14
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
+ from pydantic import BaseModel, Field
16
+ from faker import Faker
17
+ import requests
18
+
19
+ # --- 基本配置 ---
20
+ logging.basicConfig(
21
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
22
+ )
23
+
24
+ # --- 全局变量 ---
25
+ config = {}
26
+ account_manager = None
27
+ freeplay_client = None
28
+ proxy_pool = None
29
+ valid_client_keys = set()
30
+ app_lock = threading.Lock() # 用于保护全局资源的初始化
31
+
32
+
33
+ # --- Pydantic模型定义 (来自模板) ---
34
+ class ChatMessage(BaseModel):
35
+ role: str
36
+ content: Union[str, List[Dict[str, Any]]]
37
+
38
+
39
+ class ChatCompletionRequest(BaseModel):
40
+ model: str
41
+ messages: List[ChatMessage]
42
+ stream: bool = False
43
+ temperature: Optional[float] = 1.0 # 映射到Freeplay参数
44
+ max_tokens: Optional[int] = 32000 # 映射到Freeplay参数
45
+ top_p: Optional[float] = 1.0 # 映射到Freeplay参数
46
+
47
+
48
+ class ModelInfo(BaseModel):
49
+ id: str
50
+ object: str = "model"
51
+ created: int = Field(default_factory=lambda: int(time.time()))
52
+ owned_by: str = "freeplay"
53
+
54
+
55
+ class ModelList(BaseModel):
56
+ object: str = "list"
57
+ data: List[ModelInfo]
58
+
59
+
60
+ class ChatCompletionChoice(BaseModel):
61
+ message: ChatMessage
62
+ index: int = 0
63
+ finish_reason: str = "stop"
64
+
65
+
66
+ class ChatCompletionResponse(BaseModel):
67
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
68
+ object: str = "chat.completion"
69
+ created: int = Field(default_factory=lambda: int(time.time()))
70
+ model: str
71
+ choices: List[ChatCompletionChoice]
72
+ usage: Dict[str, int] = Field(
73
+ default_factory=lambda: {
74
+ "prompt_tokens": 0,
75
+ "completion_tokens": 0,
76
+ "total_tokens": 0,
77
+ }
78
+ )
79
+
80
+
81
+ class StreamChoice(BaseModel):
82
+ delta: Dict[str, Any] = Field(default_factory=dict)
83
+ index: int = 0
84
+ finish_reason: Optional[str] = None
85
+
86
+
87
+ class StreamResponse(BaseModel):
88
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
89
+ object: str = "chat.completion.chunk"
90
+ created: int = Field(default_factory=lambda: int(time.time()))
91
+ model: str
92
+ choices: List[StreamChoice]
93
+
94
+
95
+ # --- 模型映射 ---
96
+ MODEL_MAPPING = {
97
+ "claude-3-7-sonnet-20250219": {
98
+ "model_id": "be71f37b-1487-49fa-a989-a9bb99c0b129",
99
+ "max_tokens": 64000,
100
+ "provider": "Anthropic",
101
+ },
102
+ "claude-4-opus-20250514": {
103
+ "model_id": "bebc7dd5-a24d-4147-85b0-8f62902ea1a3",
104
+ "max_tokens": 32000,
105
+ "provider": "Anthropic",
106
+ },
107
+ "claude-4-sonnet": {
108
+ "model_id": "884dde7c-8def-4365-b19a-57af2787ab84",
109
+ "max_tokens": 64000,
110
+ "provider": "Anthropic",
111
+ },
112
+ }
113
+
114
+
115
+ # --- 服务类 ---
116
+ class FreeplayClient:
117
+ def __init__(self, proxy_pool_instance: Optional[ProxyPool] = None):
118
+ self.proxy_pool = proxy_pool_instance
119
+ self.faker = Faker()
120
+
121
+ def check_balance(self, session_id: str) -> float:
122
+ headers = {
123
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
124
+ "Accept": "application/json",
125
+ }
126
+ cookies = {"session": session_id}
127
+
128
+ proxy_info = self.proxy_pool.get_proxy() if self.proxy_pool else None
129
+ proxies = {"http": proxy_info['full'], "https": proxy_info['full']} if proxy_info else None
130
+
131
+ try:
132
+ response = requests.get(
133
+ "https://app.freeplay.ai/app_data/settings/billing",
134
+ headers=headers,
135
+ cookies=cookies,
136
+ proxies=proxies,
137
+ timeout=10,
138
+ )
139
+ if response.status_code == 200:
140
+ data = response.json()
141
+ for feature in data.get("feature_usage", []):
142
+ if feature.get("feature_name") == "Freeplay credits":
143
+ return feature.get("usage_limit", 0) - feature.get(
144
+ "usage_value", 0
145
+ )
146
+ return 0.0
147
+ return 0.0
148
+ except requests.exceptions.ProxyError as e:
149
+ logging.warning(f"Proxy error during balance check: {e}")
150
+ if self.proxy_pool and proxy_info:
151
+ self.proxy_pool.remove_proxy(proxy_info['ip'], proxy_info['port'])
152
+ return 0.0
153
+ except Exception as e:
154
+ logging.warning(
155
+ f"Failed to check balance for session_id ending in ...{session_id[-4:]}: {e}"
156
+ )
157
+ return 0.0
158
+
159
+ def register(self) -> Optional[Dict]:
160
+ url = "https://app.freeplay.ai/app_data/auth/signup"
161
+ payload = {
162
+ "email": self.faker.email(),
163
+ "password": f"aA1!{uuid.uuid4().hex[:8]}",
164
+ "account_name": self.faker.name(),
165
+ "first_name": self.faker.first_name(),
166
+ "last_name": self.faker.last_name(),
167
+ }
168
+ headers = {
169
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
170
+ "Accept": "application/json",
171
+ "Content-Type": "application/json",
172
+ "origin": "https://app.freeplay.ai",
173
+ "referer": "https://app.freeplay.ai/signup",
174
+ }
175
+ try:
176
+ proxy_info = self.proxy_pool.get_proxy() if self.proxy_pool else None
177
+ proxies = {"http": proxy_info['full'], "https": proxy_info['full']} if proxy_info else None
178
+ response = requests.post(
179
+ url,
180
+ data=json.dumps(payload),
181
+ headers=headers,
182
+ proxies=proxies,
183
+ timeout=20,
184
+ )
185
+ response.raise_for_status()
186
+ project_id = response.json()["project_id"]
187
+ session = response.cookies.get("session")
188
+ if project_id and session:
189
+ return {
190
+ "email": payload["email"],
191
+ "password": payload["password"],
192
+ "session_id": session,
193
+ "project_id": project_id,
194
+ "balance": 5.0, # 新注册账号默认5刀
195
+ }
196
+ except requests.exceptions.ProxyError as e:
197
+ logging.warning(f"Proxy error during registration: {e}")
198
+ if self.proxy_pool and proxy_info:
199
+ self.proxy_pool.remove_proxy(proxy_info['ip'], proxy_info['port'])
200
+ return None
201
+ except Exception as e:
202
+ logging.error(f"Account registration failed: {e}")
203
+ return None
204
+
205
+ def chat(
206
+ self,
207
+ session_id: str,
208
+ project_id: str,
209
+ model_config: Dict,
210
+ messages: List[Dict],
211
+ params: Dict,
212
+ ) -> requests.Response:
213
+ url = f"https://app.freeplay.ai/app_data/projects/{project_id}/llm-completions"
214
+ headers = {
215
+ "accept": "*/*",
216
+ "origin": "https://app.freeplay.ai",
217
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
218
+ }
219
+ cookies = {"session": session_id}
220
+
221
+ # 将 system message 转换为 user message
222
+ for msg in messages:
223
+ if msg["role"] == "system":
224
+ msg["role"] = "user"
225
+
226
+ json_payload = {
227
+ "messages": messages,
228
+ "params": [
229
+ {
230
+ "name": "max_tokens",
231
+ "value": params.get("max_tokens", model_config["max_tokens"]),
232
+ "type": "integer",
233
+ },
234
+ {
235
+ "name": "temperature",
236
+ "value": params.get("temperature", 1.0),
237
+ "type": "float",
238
+ },
239
+ {"name": "top_p", "value": params.get("top_p", 1.0), "type": "float"},
240
+ ],
241
+ "model_id": model_config["model_id"],
242
+ "variables": {},
243
+ "history": None,
244
+ "asset_references": {},
245
+ }
246
+ files = {"json_data": (None, json.dumps(json_payload))}
247
+ proxy_info = self.proxy_pool.get_proxy() if self.proxy_pool else None
248
+ proxies = {"http": proxy_info['full'], "https": proxy_info['full']} if proxy_info else None
249
+ try:
250
+ return requests.post(
251
+ url, headers=headers, cookies=cookies, files=files, stream=True, proxies=proxies
252
+ )
253
+ except requests.exceptions.ProxyError as e:
254
+ logging.warning(f"Proxy error during chat: {e}")
255
+ if self.proxy_pool and proxy_info:
256
+ self.proxy_pool.remove_proxy(proxy_info['ip'], proxy_info['port'])
257
+ raise # Re-raise to be caught by the retry logic
258
+
259
+
260
+ class AccountManager:
261
+ def __init__(self, filepath: str):
262
+ self.filepath = filepath
263
+ self.accounts = []
264
+ self.lock = threading.Lock()
265
+ self.load_accounts()
266
+
267
+ def load_accounts(self):
268
+ with self.lock:
269
+ if not os.path.exists(self.filepath):
270
+ self.accounts = []
271
+ return
272
+ with open(self.filepath, "r", encoding="utf-8") as f:
273
+ self.accounts = [json.loads(line) for line in f if line.strip()]
274
+ logging.info(f"Loaded {len(self.accounts)} accounts from {self.filepath}")
275
+
276
+ def save_accounts(self):
277
+ with self.lock:
278
+ with open(self.filepath, "w", encoding="utf-8") as f:
279
+ for account in self.accounts:
280
+ f.write(json.dumps(account) + "\n")
281
+
282
+ def add_account(self, account: Dict):
283
+ with self.lock:
284
+ self.accounts.append(account)
285
+ self.save_accounts()
286
+ logging.info(f"Added new account: {account.get('email')}")
287
+
288
+ def get_account(self) -> Optional[Dict]:
289
+ with self.lock:
290
+ # 优先选择余额最高的
291
+ available_accounts = [
292
+ acc for acc in self.accounts if acc.get("balance", 0) > 0
293
+ ]
294
+ if not available_accounts:
295
+ return None
296
+ return max(available_accounts, key=lambda x: x.get("balance", 0))
297
+
298
+ def update_account(self, account_data: Dict):
299
+ with self.lock:
300
+ for i, acc in enumerate(self.accounts):
301
+ if acc["session_id"] == account_data["session_id"]:
302
+ self.accounts[i] = account_data
303
+ break
304
+ self.save_accounts()
305
+
306
+ def get_all_accounts(self) -> List[Dict]:
307
+ with self.lock:
308
+ return self.accounts.copy()
309
+
310
+
311
+ class KeyMaintainer(threading.Thread):
312
+ def __init__(
313
+ self, account_manager: AccountManager, client: FreeplayClient, config: Dict
314
+ ):
315
+ super().__init__(daemon=True)
316
+ self.manager = account_manager
317
+ self.client = client
318
+ self.config = config
319
+
320
+ def run(self):
321
+ while True:
322
+ try:
323
+ logging.info("KeyMaintainer: Starting maintenance cycle.")
324
+ accounts = self.manager.get_all_accounts()
325
+
326
+ # 更新所有账户余额
327
+ for account in accounts:
328
+ balance = self.client.check_balance(account["session_id"])
329
+ if balance != account.get("balance"):
330
+ account["balance"] = balance
331
+ self.manager.update_account(account)
332
+ logging.info(
333
+ f"Account {account['email']} balance updated to ${balance:.4f}"
334
+ )
335
+
336
+ # 检查是否需要注册新账号
337
+ healthy_accounts = [
338
+ acc
339
+ for acc in self.manager.get_all_accounts()
340
+ if acc.get("balance", 0) > self.config["LOW_BALANCE_THRESHOLD"]
341
+ ]
342
+ needed = self.config["ACTIVE_KEY_THRESHOLD"] - len(healthy_accounts)
343
+
344
+ if needed > 0:
345
+ logging.info(
346
+ f"Healthy accounts ({len(healthy_accounts)}) below threshold ({self.config['ACTIVE_KEY_THRESHOLD']}). Need to register {needed} new accounts."
347
+ )
348
+ with concurrent.futures.ThreadPoolExecutor(
349
+ max_workers=self.config["REGISTRATION_CONCURRENCY"]
350
+ ) as executor:
351
+ futures = [
352
+ executor.submit(self.client.register) for _ in range(needed)
353
+ ]
354
+ for future in concurrent.futures.as_completed(futures):
355
+ new_account = future.result()
356
+ if new_account:
357
+ self.manager.add_account(new_account)
358
+ else:
359
+ logging.info(
360
+ f"Sufficient healthy accounts ({len(healthy_accounts)}). No registration needed."
361
+ )
362
+
363
+ except Exception as e:
364
+ logging.error(f"Error in KeyMaintainer cycle: {e}")
365
+
366
+ time.sleep(self.config["CHECK_INTERVAL_SECONDS"])
367
+
368
+
369
+ # --- FastAPI应用 ---
370
+ app = FastAPI(title="Freeplay.ai to OpenAI API Adapter")
371
+ security = HTTPBearer()
372
+
373
+
374
+ def initialize_app():
375
+ global config, account_manager, freeplay_client, valid_client_keys, proxy_pool
376
+
377
+ with app_lock:
378
+ if account_manager: # 已经初始化
379
+ return
380
+
381
+ # 1. 加载配置
382
+ if not os.path.exists("config.json"):
383
+ default_config = {
384
+ "HOST": "0.0.0.0",
385
+ "PORT": 8057,
386
+ "ACCOUNTS_FILE": "accounts.json",
387
+ "LOW_BALANCE_THRESHOLD": 2.0,
388
+ "ACTIVE_KEY_THRESHOLD": 5,
389
+ "CHECK_INTERVAL_SECONDS": 300,
390
+ "REGISTRATION_CONCURRENCY": 2,
391
+ "USE_PROXY_POOL": True,
392
+ "PROXY_POOL_CONFIG": {
393
+ "target_count": 20,
394
+ "min_threshold": 5,
395
+ "check_interval": 180,
396
+ }
397
+ }
398
+ with open("config.json", "w") as f:
399
+ json.dump(default_config, f, indent=4)
400
+ config = default_config
401
+ logging.info("Created default config.json")
402
+ else:
403
+ with open("config.json", "r") as f:
404
+ config = json.load(f)
405
+ logging.info("Loaded config from config.json")
406
+
407
+ # 2. 加载客户端密钥
408
+ api_key = os.environ.get("API_KEY", "sk-123456")
409
+ valid_client_keys = {api_key}
410
+ logging.info("Loaded API_KEY from environment variable.")
411
+
412
+ # 3. 初始化代理池
413
+ if config.get("USE_PROXY_POOL"):
414
+ logging.info("Initializing proxy pool...")
415
+ proxy_pool_config = config.get("PROXY_POOL_CONFIG", {})
416
+ proxy_pool = ProxyPool(proxy_pool_config)
417
+ proxy_pool.initialize()
418
+ logging.info("Proxy pool initialized.")
419
+ else:
420
+ logging.info("Proxy pool is disabled in config.")
421
+
422
+ # 4. 初始化服务
423
+ freeplay_client = FreeplayClient(proxy_pool_instance=proxy_pool)
424
+ account_manager = AccountManager(filepath=config["ACCOUNTS_FILE"])
425
+
426
+ # 5. 启动后台维护线程
427
+ maintainer = KeyMaintainer(account_manager, freeplay_client, config)
428
+ maintainer.start()
429
+ logging.info("Key maintenance service started.")
430
+
431
+
432
+ async def authenticate_client(auth: HTTPAuthorizationCredentials = Depends(security)):
433
+ if not auth or auth.credentials not in valid_client_keys:
434
+ raise HTTPException(status_code=403, detail="Invalid client API key.")
435
+
436
+
437
+ @app.on_event("startup")
438
+ async def startup_event():
439
+ initialize_app()
440
+
441
+
442
+ @app.get("/v1/models", response_model=ModelList)
443
+ async def list_models(_: None = Depends(authenticate_client)):
444
+ model_infos = [
445
+ ModelInfo(id=name, owned_by=details["provider"])
446
+ for name, details in MODEL_MAPPING.items()
447
+ ]
448
+ return ModelList(data=model_infos)
449
+
450
+
451
+ def stream_generator(
452
+ response: requests.Response, model_name: str, account: Dict
453
+ ) -> Generator[str, None, None]:
454
+ chat_id = f"chatcmpl-{uuid.uuid4().hex}"
455
+ created = int(time.time())
456
+
457
+ # Start chunk
458
+ start_chunk = StreamResponse(
459
+ model=model_name, choices=[StreamChoice(delta={"role": "assistant"})]
460
+ ).dict()
461
+ start_chunk["id"] = chat_id
462
+ start_chunk["created"] = created
463
+ yield f"data: {json.dumps(start_chunk)}\n\n"
464
+
465
+ try:
466
+ for line in response.iter_lines(decode_unicode=True):
467
+ if line and line.startswith("data: "):
468
+ try:
469
+ data = json.loads(line[6:])
470
+ if data.get("content"):
471
+ chunk = StreamResponse(
472
+ model=model_name,
473
+ choices=[StreamChoice(delta={"content": data["content"]})],
474
+ ).dict()
475
+ chunk["id"] = chat_id
476
+ chunk["created"] = created
477
+ yield f"data: {json.dumps(chunk)}\n\n"
478
+ if data.get("cost") is not None:
479
+ break # 结束
480
+ except json.JSONDecodeError:
481
+ continue
482
+ finally:
483
+ # End chunk
484
+ end_chunk = StreamResponse(
485
+ model=model_name, choices=[StreamChoice(delta={}, finish_reason="stop")]
486
+ ).dict()
487
+ end_chunk["id"] = chat_id
488
+ end_chunk["created"] = created
489
+ yield f"data: {json.dumps(end_chunk)}\n\n"
490
+ yield "data: [DONE]\n\n"
491
+
492
+ # 更新余额
493
+ new_balance = freeplay_client.check_balance(account["session_id"])
494
+ if new_balance != account.get("balance"):
495
+ account["balance"] = new_balance
496
+ account_manager.update_account(account)
497
+ logging.info(
498
+ f"Post-chat balance update for {account['email']}: ${new_balance:.4f}"
499
+ )
500
+
501
+
502
+ @app.post("/v1/chat/completions")
503
+ async def chat_completions(
504
+ req: ChatCompletionRequest, _: None = Depends(authenticate_client)
505
+ ):
506
+ if req.model not in MODEL_MAPPING:
507
+ raise HTTPException(status_code=404, detail=f"Model '{req.model}' not found.")
508
+
509
+ model_config = MODEL_MAPPING[req.model]
510
+ messages_dict = [msg.dict() for msg in req.messages]
511
+
512
+ # Convert OpenAI vision format to Anthropic format
513
+ for message in messages_dict:
514
+ if isinstance(message.get("content"), list):
515
+ new_content = []
516
+ for part in message["content"]:
517
+ if part.get("type") == "text":
518
+ new_content.append({"type": "text", "text": part.get("text", "")})
519
+ elif part.get("type") == "image_url":
520
+ image_url = part.get("image_url", {}).get("url", "")
521
+ if image_url.startswith("data:"):
522
+ try:
523
+ # "data:image/jpeg;base64,{base64_string}"
524
+ header, encoded = image_url.split(",", 1)
525
+ media_type = header.split(":")[1].split(";")[0]
526
+ new_content.append(
527
+ {
528
+ "type": "image",
529
+ "source": {
530
+ "type": "base64",
531
+ "media_type": media_type,
532
+ "data": encoded,
533
+ },
534
+ }
535
+ )
536
+ except Exception as e:
537
+ logging.warning(f"Could not parse image data URL: {e}")
538
+ message["content"] = new_content
539
+
540
+ # 账户选择和重试逻辑
541
+ max_retries = len(account_manager.get_all_accounts())
542
+ for attempt in range(max_retries):
543
+ account = account_manager.get_account()
544
+ if not account:
545
+ raise HTTPException(
546
+ status_code=503, detail="No available accounts in the pool."
547
+ )
548
+
549
+ try:
550
+ params = {
551
+ "max_tokens": req.max_tokens,
552
+ "temperature": req.temperature,
553
+ "top_p": req.top_p,
554
+ }
555
+ response = freeplay_client.chat(
556
+ account["session_id"],
557
+ account["project_id"],
558
+ model_config,
559
+ messages_dict,
560
+ params,
561
+ )
562
+
563
+ if response.status_code == 200:
564
+ # 请求成功
565
+ if req.stream:
566
+ return StreamingResponse(
567
+ stream_generator(response, req.model, account),
568
+ media_type="text/event-stream",
569
+ )
570
+ else:
571
+ full_content = ""
572
+ for line in response.iter_lines(decode_unicode=True):
573
+ if line and line.startswith("data: "):
574
+ try:
575
+ data = json.loads(line[6:])
576
+ full_content += data.get("content", "")
577
+ if data.get("cost") is not None:
578
+ break
579
+ except json.JSONDecodeError:
580
+ continue
581
+
582
+ # 更新余额
583
+ new_balance = freeplay_client.check_balance(account["session_id"])
584
+ account["balance"] = new_balance
585
+ account_manager.update_account(account)
586
+ logging.info(
587
+ f"Post-chat balance update for {account['email']}: ${new_balance:.4f}"
588
+ )
589
+
590
+ return ChatCompletionResponse(
591
+ model=req.model,
592
+ choices=[
593
+ ChatCompletionChoice(
594
+ message=ChatMessage(
595
+ role="assistant", content=full_content
596
+ )
597
+ )
598
+ ],
599
+ )
600
+
601
+ elif response.status_code in [401, 403, 404]:
602
+ logging.warning(
603
+ f"Account {account['email']} failed with status {response.status_code}. Disabling it."
604
+ )
605
+ account["balance"] = 0.0 # 禁用账户
606
+ account_manager.update_account(account)
607
+ continue # 重试下一个
608
+ else:
609
+ logging.error(
610
+ f"API call failed with status {response.status_code}: {response.text}"
611
+ )
612
+ response.raise_for_status()
613
+
614
+ except requests.exceptions.ProxyError:
615
+ # Proxy error was already logged and handled in FreeplayClient
616
+ logging.warning(f"Retrying request due to proxy error.")
617
+ # Don't disable the account, just retry with a new proxy (and potentially new account)
618
+ continue
619
+ except Exception as e:
620
+ logging.error(
621
+ f"Error with account {account['email']}: {e}. Trying next account."
622
+ )
623
+ account["balance"] = 0.0 # 发生未知异常也禁用
624
+ account_manager.update_account(account)
625
+ continue
626
+
627
+ raise HTTPException(
628
+ status_code=503, detail="All available accounts failed to process the request."
629
+ )
630
+
631
+
632
+ @app.get("/admin/accounts/status")
633
+ async def accounts_status(_: None = Depends(authenticate_client)):
634
+ accounts = account_manager.get_all_accounts()
635
+ total_balance = sum(acc.get("balance", 0) for acc in accounts)
636
+ healthy_count = len(
637
+ [
638
+ acc
639
+ for acc in accounts
640
+ if acc.get("balance", 0) > config.get("LOW_BALANCE_THRESHOLD", 2.0)
641
+ ]
642
+ )
643
+
644
+ return JSONResponse(
645
+ {
646
+ "total_accounts": len(accounts),
647
+ "healthy_accounts": healthy_count,
648
+ "total_balance": f"${total_balance:.4f}",
649
+ "accounts": [
650
+ {
651
+ "email": acc.get("email"),
652
+ "balance": f"${acc.get('balance', 0):.4f}",
653
+ "project_id": acc.get("project_id"),
654
+ }
655
+ for acc in accounts
656
+ ],
657
+ }
658
+ )
659
+
660
+
661
+ if __name__ == "__main__":
662
+ import uvicorn
663
+
664
+ initialize_app()
665
+ logging.info("--- Freeplay.ai to OpenAI API Adapter ---")
666
+ logging.info(f"Starting server on {config['HOST']}:{config['PORT']}")
667
+ logging.info(f"Supported models: {list(MODEL_MAPPING.keys())}")
668
+ logging.info(f"Client keys loaded: {len(valid_client_keys)}")
669
+ logging.info(f"Accounts loaded: {len(account_manager.get_all_accounts())}")
670
+ logging.info("Endpoints:")
671
+ logging.info(" POST /v1/chat/completions (Client API Key Auth)")
672
+ logging.info(" GET /v1/models (Client API Key Auth)")
673
+ logging.info(" GET /admin/accounts/status (Client API Key Auth)")
674
+
675
+ uvicorn.run(app, host=config["HOST"], port=config["PORT"])
proxy_pool.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import threading
3
+ import time
4
+ import random
5
+ from datetime import datetime, timedelta
6
+
7
+ class ProxyPool:
8
+ def __init__(self, options={}):
9
+ self.target_count = options.get('target_count', 20)
10
+ self.batch_size = options.get('batch_size', 20)
11
+ self.test_timeout = options.get('test_timeout', 5)
12
+ self.request_timeout = options.get('request_timeout', 10)
13
+ self.target_url = options.get('target_url', 'https://app.freeplay.ai/')
14
+ self.concurrent_requests = options.get('concurrent_requests', 10)
15
+ self.min_threshold = options.get('min_threshold', 5)
16
+ self.check_interval = options.get('check_interval', 30)
17
+ self.proxy_protocol = options.get('proxy_protocol', 'http')
18
+ self.max_refill_attempts = options.get('max_refill_attempts', 20)
19
+ self.retry_delay = options.get('retry_delay', 1)
20
+
21
+ self.available_proxies = []
22
+ self.current_index = 0
23
+ self.is_initialized = False
24
+ self.is_refilling = False
25
+ self.check_timer = None
26
+ self.lock = threading.Lock()
27
+
28
+ def initialize(self):
29
+ if self.is_initialized:
30
+ return
31
+ print(f"Initializing proxy pool, target count: {self.target_count}")
32
+ self.refill_proxies()
33
+ self.check_timer = threading.Timer(self.check_interval, self.check_and_refill)
34
+ self.check_timer.start()
35
+ self.is_initialized = True
36
+ print(f"Proxy pool initialized, current available proxies: {len(self.available_proxies)}")
37
+
38
+ def stop(self):
39
+ if self.check_timer:
40
+ self.check_timer.cancel()
41
+ self.check_timer = None
42
+ print("Proxy pool service stopped")
43
+
44
+ def check_and_refill(self):
45
+ with self.lock:
46
+ if len(self.available_proxies) <= self.min_threshold and not self.is_refilling:
47
+ print(f"Available proxies ({len(self.available_proxies)}) below threshold ({self.min_threshold}), starting refill")
48
+ self.refill_proxies()
49
+ if self.is_initialized:
50
+ self.check_timer = threading.Timer(self.check_interval, self.check_and_refill)
51
+ self.check_timer.start()
52
+
53
+ def refill_proxies(self):
54
+ if self.is_refilling:
55
+ return
56
+ self.is_refilling = True
57
+ print(f"Starting to refill proxies, current count: {len(self.available_proxies)}, target: {self.target_count}")
58
+
59
+ attempts = 0
60
+ try:
61
+ while len(self.available_proxies) < self.target_count and attempts < self.max_refill_attempts:
62
+ attempts += 1
63
+ print(f"Refill attempt #{attempts}, current available: {len(self.available_proxies)}/{self.target_count}")
64
+
65
+ remaining_needed = self.target_count - len(self.available_proxies)
66
+ batch_size_needed = max(self.batch_size, remaining_needed * 2)
67
+
68
+ proxies = self.get_proxies_from_provider(batch_size_needed)
69
+ if not proxies:
70
+ print(f"No proxies received, retrying in {self.retry_delay} seconds...")
71
+ time.sleep(self.retry_delay)
72
+ continue
73
+
74
+ new_proxies = self.filter_existing_proxies(proxies)
75
+ if not new_proxies:
76
+ print("All fetched proxies already exist, getting new ones...")
77
+ continue
78
+
79
+ threads = []
80
+ for proxy in new_proxies:
81
+ thread = threading.Thread(target=self.test_and_add_proxy, args=(proxy,))
82
+ threads.append(thread)
83
+ thread.start()
84
+
85
+ for thread in threads:
86
+ thread.join()
87
+
88
+ if len(self.available_proxies) >= self.target_count:
89
+ break
90
+ if len(self.available_proxies) < self.target_count:
91
+ time.sleep(self.retry_delay)
92
+ except Exception as e:
93
+ print(f"Error during proxy refill: {e}")
94
+ finally:
95
+ self.is_refilling = False
96
+ if len(self.available_proxies) >= self.target_count:
97
+ print(f"Proxy refill complete, current available: {len(self.available_proxies)}/{self.target_count}")
98
+ else:
99
+ print(f"Max refill attempts reached, current available: {len(self.available_proxies)}/{self.target_count}")
100
+
101
+ def test_and_add_proxy(self, proxy):
102
+ if self.test_proxy(proxy):
103
+ with self.lock:
104
+ if not any(p['full'] == f"{self.proxy_protocol}://{proxy}" for p in self.available_proxies):
105
+ ip, port = proxy.split(':')
106
+ proxy_obj = {
107
+ 'ip': ip,
108
+ 'port': port,
109
+ 'protocol': self.proxy_protocol,
110
+ 'full': f"{self.proxy_protocol}://{proxy}",
111
+ 'added_at': datetime.now().isoformat()
112
+ }
113
+ self.available_proxies.append(proxy_obj)
114
+ print(f"Successfully added proxy: {proxy_obj['full']}, current available: {len(self.available_proxies)}/{self.target_count}")
115
+
116
+ def filter_existing_proxies(self, proxies):
117
+ existing = {f"{p['ip']}:{p['port']}" for p in self.available_proxies}
118
+ return [p for p in proxies if p not in existing]
119
+
120
+ def get_proxies_from_provider(self, count=None):
121
+ try:
122
+ request_count = count or self.batch_size
123
+ url = f"https://proxy.scdn.io/api/get_proxy.php?protocol={self.proxy_protocol}&count={request_count}"
124
+ print(f"Getting proxies from: {url}")
125
+ response = requests.get(url, timeout=10)
126
+ if response.status_code == 200:
127
+ data = response.json()
128
+ if data.get('code') == 200:
129
+ print(f"Successfully got {data['data']['count']} proxies")
130
+ return data['data']['proxies']
131
+ print(f"Failed to get proxies: {response.text}")
132
+ return []
133
+ except Exception as e:
134
+ print(f"Error getting proxies: {e}")
135
+ return []
136
+
137
+ def test_proxy(self, proxy_url):
138
+ try:
139
+ proxies = {self.proxy_protocol: f"{self.proxy_protocol}://{proxy_url}"}
140
+ headers = {
141
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
142
+ }
143
+ response = requests.get(self.target_url, proxies=proxies, headers=headers, timeout=self.request_timeout, allow_redirects=True)
144
+ is_valid = response.status_code == 200
145
+ if is_valid:
146
+ print(f"Proxy {proxy_url} successfully tested, status: {response.status_code}")
147
+ else:
148
+ print(f"Proxy {proxy_url} failed test, status: {response.status_code}")
149
+ return is_valid
150
+ except Exception as e:
151
+ print(f"Proxy {proxy_url} request error: {e}")
152
+ return False
153
+
154
+ def get_proxy(self):
155
+ with self.lock:
156
+ if not self.available_proxies:
157
+ print("No available proxies")
158
+ return None
159
+ proxy = self.available_proxies[self.current_index]
160
+ self.current_index = (self.current_index + 1) % len(self.available_proxies)
161
+ return proxy
162
+
163
+ def remove_proxy(self, ip, port):
164
+ with self.lock:
165
+ port_str = str(port)
166
+ initial_length = len(self.available_proxies)
167
+ self.available_proxies = [p for p in self.available_proxies if not (p['ip'] == ip and p['port'] == port_str)]
168
+ if self.current_index >= len(self.available_proxies) and self.available_proxies:
169
+ self.current_index = 0
170
+ removed = initial_length > len(self.available_proxies)
171
+ if removed:
172
+ print(f"Removed proxy {ip}:{port}, current available: {len(self.available_proxies)}")
173
+ else:
174
+ print(f"Could not find proxy to remove {ip}:{port}")
175
+ self.check_and_refill()
176
+ return removed
177
+
178
+ def get_all_proxies(self):
179
+ with self.lock:
180
+ return list(self.available_proxies)
181
+
182
+ def get_count(self):
183
+ with self.lock:
184
+ return len(self.available_proxies)
185
+
186
+ if __name__ == '__main__':
187
+ proxy_pool = ProxyPool({
188
+ 'target_count': 10,
189
+ 'min_threshold': 3,
190
+ 'check_interval': 60,
191
+ 'target_url': 'https://app.freeplay.ai/',
192
+ 'concurrent_requests': 15,
193
+ 'max_refill_attempts': 15,
194
+ 'retry_delay': 1
195
+ })
196
+ proxy_pool.initialize()
197
+
198
+ time.sleep(5)
199
+ proxy = proxy_pool.get_proxy()
200
+ print(f"Got proxy: {proxy}")
201
+
202
+ if proxy:
203
+ time.sleep(5)
204
+ proxy_pool.remove_proxy(proxy['ip'], proxy['port'])
205
+
206
+ all_proxies = proxy_pool.get_all_proxies()
207
+ print(f"Current all proxies ({len(all_proxies)}): {all_proxies}")
208
+
209
+ time.sleep(5)
210
+ proxy_pool.stop()
211
+ print("Proxy pool example finished.")
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ faker
3
+ requests
4
+ uvicorn
5
+ python-multipart
6
+ pydantic==1.10.16