polarbearblue commited on
Commit
9c8461d
·
verified ·
1 Parent(s): 277d8e6

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +669 -0
app.py ADDED
@@ -0,0 +1,669 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ import time
5
+ import uuid
6
+ import asyncio
7
+ import threading
8
+ from typing import Any, Dict, List, Optional, TypedDict, Union, Generator
9
+
10
+ import requests
11
+ from fastapi import FastAPI, HTTPException, Depends, Query
12
+ from fastapi.responses import StreamingResponse
13
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ # Yupp Account Management
18
+ class YuppAccount(TypedDict):
19
+ token: str
20
+ is_valid: bool
21
+ last_used: float
22
+ error_count: int
23
+
24
+
25
+ # Global variables
26
+ VALID_CLIENT_KEYS: set = set()
27
+ YUPP_ACCOUNTS: List[YuppAccount] = []
28
+ YUPP_MODELS: List[Dict[str, Any]] = []
29
+ account_rotation_lock = threading.Lock()
30
+ MAX_ERROR_COUNT = 3
31
+ ERROR_COOLDOWN = 300 # 5 minutes cooldown for accounts with errors
32
+ DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
33
+
34
+
35
+ # Pydantic Models
36
+ class ChatMessage(BaseModel):
37
+ role: str
38
+ content: Union[str, List[Dict[str, Any]]]
39
+ reasoning_content: Optional[str] = None
40
+
41
+
42
+ class ChatCompletionRequest(BaseModel):
43
+ model: str
44
+ messages: List[ChatMessage]
45
+ stream: bool = True
46
+ temperature: Optional[float] = None
47
+ max_tokens: Optional[int] = None
48
+ top_p: Optional[float] = None
49
+ raw_response: bool = False # 保留用于调试
50
+
51
+
52
+ class ModelInfo(BaseModel):
53
+ id: str
54
+ object: str = "model"
55
+ created: int
56
+ owned_by: str
57
+
58
+
59
+ class ModelList(BaseModel):
60
+ object: str = "list"
61
+ data: List[ModelInfo]
62
+
63
+
64
+ class ChatCompletionChoice(BaseModel):
65
+ message: ChatMessage
66
+ index: int = 0
67
+ finish_reason: str = "stop"
68
+
69
+
70
+ class ChatCompletionResponse(BaseModel):
71
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
72
+ object: str = "chat.completion"
73
+ created: int = Field(default_factory=lambda: int(time.time()))
74
+ model: str
75
+ choices: List[ChatCompletionChoice]
76
+ usage: Dict[str, int] = Field(
77
+ default_factory=lambda: {
78
+ "prompt_tokens": 0,
79
+ "completion_tokens": 0,
80
+ "total_tokens": 0,
81
+ }
82
+ )
83
+
84
+
85
+ class StreamChoice(BaseModel):
86
+ delta: Dict[str, Any] = Field(default_factory=dict)
87
+ index: int = 0
88
+ finish_reason: Optional[str] = None
89
+
90
+
91
+ class StreamResponse(BaseModel):
92
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
93
+ object: str = "chat.completion.chunk"
94
+ created: int = Field(default_factory=lambda: int(time.time()))
95
+ model: str
96
+ choices: List[StreamChoice]
97
+
98
+
99
+ # FastAPI App
100
+ app = FastAPI(title="Yupp.ai OpenAI API Adapter")
101
+ security = HTTPBearer(auto_error=False)
102
+
103
+
104
+ def log_debug(message: str):
105
+ """Debug日志函数"""
106
+ if DEBUG_MODE:
107
+ print(f"[DEBUG] {message}")
108
+
109
+
110
+ def load_client_api_keys():
111
+ """Load client API keys from client_api_keys.json"""
112
+ global VALID_CLIENT_KEYS
113
+ try:
114
+ with open("client_api_keys.json", "r", encoding="utf-8") as f:
115
+ keys = json.load(f)
116
+ VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set()
117
+ print(f"Successfully loaded {len(VALID_CLIENT_KEYS)} client API keys.")
118
+ except FileNotFoundError:
119
+ print("Error: client_api_keys.json not found. Client authentication will fail.")
120
+ VALID_CLIENT_KEYS = set()
121
+ except Exception as e:
122
+ print(f"Error loading client_api_keys.json: {e}")
123
+ VALID_CLIENT_KEYS = set()
124
+
125
+
126
+ def load_yupp_accounts():
127
+ """Load Yupp accounts from yupp.json"""
128
+ global YUPP_ACCOUNTS
129
+ YUPP_ACCOUNTS = []
130
+ try:
131
+ with open("yupp.json", "r", encoding="utf-8") as f:
132
+ accounts = json.load(f)
133
+ if not isinstance(accounts, list):
134
+ print("Warning: yupp.json should contain a list of account objects.")
135
+ return
136
+
137
+ for acc in accounts:
138
+ token = acc.get("token")
139
+ if token:
140
+ YUPP_ACCOUNTS.append({
141
+ "token": token,
142
+ "is_valid": True,
143
+ "last_used": 0,
144
+ "error_count": 0
145
+ })
146
+ print(f"Successfully loaded {len(YUPP_ACCOUNTS)} Yupp accounts.")
147
+ except FileNotFoundError:
148
+ print("Error: yupp.json not found. API calls will fail.")
149
+ except Exception as e:
150
+ print(f"Error loading yupp.json: {e}")
151
+
152
+
153
+ def load_yupp_models():
154
+ """Load Yupp models from model.json"""
155
+ global YUPP_MODELS
156
+ try:
157
+ with open("model.json", "r", encoding="utf-8") as f:
158
+ YUPP_MODELS = json.load(f)
159
+ if not isinstance(YUPP_MODELS, list):
160
+ YUPP_MODELS = []
161
+ print("Warning: model.json should contain a list of model objects.")
162
+ return
163
+ print(f"Successfully loaded {len(YUPP_MODELS)} models.")
164
+ except FileNotFoundError:
165
+ print("Error: model.json not found. Model list will be empty.")
166
+ YUPP_MODELS = []
167
+ except Exception as e:
168
+ print(f"Error loading model.json: {e}")
169
+ YUPP_MODELS = []
170
+
171
+
172
+ def get_best_yupp_account() -> Optional[YuppAccount]:
173
+ """Get the best available Yupp account using a smart selection algorithm."""
174
+ with account_rotation_lock:
175
+ now = time.time()
176
+ valid_accounts = [
177
+ acc for acc in YUPP_ACCOUNTS
178
+ if acc["is_valid"] and (
179
+ acc["error_count"] < MAX_ERROR_COUNT or
180
+ now - acc["last_used"] > ERROR_COOLDOWN
181
+ )
182
+ ]
183
+
184
+ if not valid_accounts:
185
+ return None
186
+
187
+ # Reset error count for accounts that have been in cooldown
188
+ for acc in valid_accounts:
189
+ if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN:
190
+ acc["error_count"] = 0
191
+
192
+ # Sort by last used (oldest first) and error count (lowest first)
193
+ valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"]))
194
+ account = valid_accounts[0]
195
+ account["last_used"] = now
196
+ return account
197
+
198
+
199
+ def format_messages_for_yupp(messages: List[ChatMessage]) -> str:
200
+ """将多轮对话格式化为Yupp单轮对话格式"""
201
+ formatted = []
202
+
203
+ # 处理系统消息
204
+ system_messages = [msg for msg in messages if msg.role == "system"]
205
+ if system_messages:
206
+ for sys_msg in system_messages:
207
+ content = sys_msg.content if isinstance(sys_msg.content, str) else json.dumps(sys_msg.content)
208
+ formatted.append(content)
209
+
210
+ # 处理用户和助手消息
211
+ user_assistant_msgs = [msg for msg in messages if msg.role != "system"]
212
+ for msg in user_assistant_msgs:
213
+ role = "Human" if msg.role == "user" else "Assistant"
214
+ content = msg.content if isinstance(msg.content, str) else json.dumps(msg.content)
215
+ formatted.append(f"\n\n{role}: {content}")
216
+
217
+ # 确保以Assistant:结尾
218
+ if not formatted or not formatted[-1].strip().startswith("Assistant:"):
219
+ formatted.append("\n\nAssistant:")
220
+
221
+ result = "".join(formatted)
222
+ # 如果以\n\n开头,则删除
223
+ if result.startswith("\n\n"):
224
+ result = result[2:]
225
+
226
+ return result
227
+
228
+
229
+ async def authenticate_client(
230
+ auth: Optional[HTTPAuthorizationCredentials] = Depends(security),
231
+ ):
232
+ """Authenticate client based on API key in Authorization header"""
233
+ if not VALID_CLIENT_KEYS:
234
+ raise HTTPException(
235
+ status_code=503,
236
+ detail="Service unavailable: Client API keys not configured on server.",
237
+ )
238
+
239
+ if not auth or not auth.credentials:
240
+ raise HTTPException(
241
+ status_code=401,
242
+ detail="API key required in Authorization header.",
243
+ headers={"WWW-Authenticate": "Bearer"},
244
+ )
245
+
246
+ if auth.credentials not in VALID_CLIENT_KEYS:
247
+ raise HTTPException(status_code=403, detail="Invalid client API key.")
248
+
249
+
250
+ @app.on_event("startup")
251
+ async def startup():
252
+ """应用启动时初始化配置"""
253
+ print("Starting Yupp.ai OpenAI API Adapter server...")
254
+ load_client_api_keys()
255
+ load_yupp_accounts()
256
+ load_yupp_models()
257
+ print("Server initialization completed.")
258
+
259
+
260
+ @app.on_event("shutdown")
261
+ async def shutdown():
262
+ """应用关闭时清理资源"""
263
+ print("Server shutdown completed.")
264
+
265
+
266
+ def get_models_list_response() -> ModelList:
267
+ """Helper to construct ModelList response from cached models."""
268
+ model_infos = [
269
+ ModelInfo(
270
+ id=model.get("label", "unknown"),
271
+ created=int(time.time()),
272
+ owned_by=model.get("publisher", "unknown")
273
+ )
274
+ for model in YUPP_MODELS
275
+ ]
276
+ return ModelList(data=model_infos)
277
+
278
+
279
+ @app.get("/v1/models", response_model=ModelList)
280
+ async def list_v1_models(_: None = Depends(authenticate_client)):
281
+ """List available models - authenticated"""
282
+ return get_models_list_response()
283
+
284
+
285
+ @app.get("/models", response_model=ModelList)
286
+ async def list_models_no_auth():
287
+ """List available models without authentication - for client compatibility"""
288
+ return get_models_list_response()
289
+
290
+
291
+ @app.get("/debug")
292
+ async def toggle_debug(enable: bool = Query(None)):
293
+ """切换调试模式"""
294
+ global DEBUG_MODE
295
+ if enable is not None:
296
+ DEBUG_MODE = enable
297
+ return {"debug_mode": DEBUG_MODE}
298
+
299
+
300
+ def claim_yupp_reward(account: YuppAccount, reward_id: str):
301
+ """同步领取Yupp奖励"""
302
+ try:
303
+ log_debug(f"Claiming reward {reward_id}...")
304
+ url = "https://yupp.ai/api/trpc/reward.claim?batch=1"
305
+ payload = {"0": {"json": {"rewardId": reward_id}}}
306
+ headers = {
307
+ "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 Edg/137.0.0.0",
308
+ "Content-Type": "application/json",
309
+ "sec-fetch-site": "same-origin",
310
+ "Cookie": f"__Secure-yupp.session-token={account['token']}",
311
+ }
312
+ response = requests.post(url, json=payload, headers=headers)
313
+ response.raise_for_status()
314
+ data = response.json()
315
+ balance = data[0]["result"]["data"]["json"]["currentCreditBalance"]
316
+ print(f"Reward claimed successfully. New balance: {balance}")
317
+ return balance
318
+ except Exception as e:
319
+ print(f"Failed to claim reward {reward_id}. Error: {e}")
320
+ return None
321
+
322
+
323
+ def yupp_stream_generator(response_lines, model_id: str, account: YuppAccount) -> Generator[str, None, None]:
324
+ """处理Yupp的流式响应并转换为OpenAI格式"""
325
+ stream_id = f"chatcmpl-{uuid.uuid4().hex}"
326
+ created_time = int(time.time())
327
+
328
+ # 发送初始角色
329
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n"
330
+
331
+ line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)")
332
+ chunks = {}
333
+ target_stream_id = None
334
+ reward_info = None
335
+ is_thinking = False
336
+ thinking_content = ""
337
+ normal_content = ""
338
+
339
+ def extract_ref_id(ref):
340
+ """从引用字符串中提取ID,例如从'$@123'提取'123'"""
341
+ return ref[2:] if ref and isinstance(ref, str) and ref.startswith("$@") else None
342
+
343
+ try:
344
+ for line in response_lines:
345
+ if not line:
346
+ continue
347
+
348
+ match = line_pattern.match(line)
349
+ if not match:
350
+ continue
351
+
352
+ chunk_id, chunk_data = match.groups()
353
+ chunk_id = chunk_id.decode()
354
+
355
+ try:
356
+ data = json.loads(chunk_data) if chunk_data != b"{}" else {}
357
+ chunks[chunk_id] = data
358
+ except json.JSONDecodeError:
359
+ continue
360
+
361
+ # 处理奖励信息
362
+ if chunk_id == "a":
363
+ reward_info = data
364
+
365
+ # 处理初始设置信息
366
+ elif chunk_id == "1":
367
+ left_stream = data.get("leftStream", {})
368
+ right_stream = data.get("rightStream", {})
369
+ select_stream = [left_stream, right_stream]
370
+
371
+ elif chunk_id == "e":
372
+ for i, selection in enumerate(data.get("modelSelections", [])):
373
+ if selection.get("selectionSource") == "USER_SELECTED":
374
+ target_stream_id = extract_ref_id(select_stream[i].get("next"))
375
+ break
376
+
377
+ # 处理目标流内容
378
+ elif target_stream_id and chunk_id == target_stream_id:
379
+ content = data.get("curr", "")
380
+ if content:
381
+ # 处理思考过程
382
+ if "<think>" in content:
383
+ parts = content.split("<think>", 1)
384
+ if parts[0]: # 思考标签前的内容
385
+ normal_content += parts[0]
386
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': parts[0]})]).json()}\n\n"
387
+
388
+ is_thinking = True
389
+ thinking_part = parts[1]
390
+
391
+ if "</think>" in thinking_part:
392
+ think_parts = thinking_part.split("</think>", 1)
393
+ thinking_content += think_parts[0]
394
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': think_parts[0]})]).json()}\n\n"
395
+
396
+ is_thinking = False
397
+ if think_parts[1]: # 思考标签后的内容
398
+ normal_content += think_parts[1]
399
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': think_parts[1]})]).json()}\n\n"
400
+ else:
401
+ thinking_content += thinking_part
402
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': thinking_part})]).json()}\n\n"
403
+
404
+ elif "</think>" in content and is_thinking:
405
+ parts = content.split("</think>", 1)
406
+ thinking_content += parts[0]
407
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': parts[0]})]).json()}\n\n"
408
+
409
+ is_thinking = False
410
+ if parts[1]: # 思考标签后的内容
411
+ normal_content += parts[1]
412
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': parts[1]})]).json()}\n\n"
413
+
414
+ elif is_thinking:
415
+ thinking_content += content
416
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': content})]).json()}\n\n"
417
+
418
+ else:
419
+ normal_content += content
420
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': content})]).json()}\n\n"
421
+
422
+ # 更新目标流ID
423
+ target_stream_id = extract_ref_id(data.get("next"))
424
+
425
+ except Exception as e:
426
+ print(f"Stream processing error: {e}")
427
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
428
+
429
+ finally:
430
+ # 发送完成信号
431
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n"
432
+ yield "data: [DONE]\n\n"
433
+
434
+ # 领取奖励
435
+ if reward_info and "unclaimedRewardInfo" in reward_info:
436
+ reward_id = reward_info["unclaimedRewardInfo"].get("rewardId")
437
+ if reward_id:
438
+ try:
439
+ claim_yupp_reward(account, reward_id)
440
+ except Exception as e:
441
+ print(f"Failed to claim reward in background: {e}")
442
+
443
+
444
+ def build_yupp_non_stream_response(response_lines, model_id: str, account: YuppAccount) -> ChatCompletionResponse:
445
+ """构建非流式响应"""
446
+ full_content = ""
447
+ full_reasoning_content = ""
448
+ reward_id = None
449
+
450
+ for event in yupp_stream_generator(response_lines, model_id, account):
451
+ if event.startswith("data:"):
452
+ data_str = event[5:].strip()
453
+ if data_str == "[DONE]":
454
+ break
455
+
456
+ try:
457
+ data = json.loads(data_str)
458
+ if "error" in data:
459
+ raise HTTPException(status_code=500, detail=data["error"]["message"])
460
+
461
+ delta = data.get("choices", [{}])[0].get("delta", {})
462
+ if "content" in delta:
463
+ full_content += delta["content"]
464
+ if "reasoning_content" in delta:
465
+ full_reasoning_content += delta["reasoning_content"]
466
+ except json.JSONDecodeError:
467
+ continue
468
+
469
+ # 构建完整响应
470
+ return ChatCompletionResponse(
471
+ model=model_id,
472
+ choices=[
473
+ ChatCompletionChoice(
474
+ message=ChatMessage(
475
+ role="assistant",
476
+ content=full_content,
477
+ reasoning_content=full_reasoning_content if full_reasoning_content else None,
478
+ )
479
+ )
480
+ ],
481
+ )
482
+
483
+
484
+ @app.post("/v1/chat/completions")
485
+ async def chat_completions(
486
+ request: ChatCompletionRequest, _: None = Depends(authenticate_client)
487
+ ):
488
+ """使用Yupp.ai创建聊天完成"""
489
+ # 查找模型
490
+ model_info = next((m for m in YUPP_MODELS if m.get("label") == request.model), None)
491
+ if not model_info:
492
+ raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.")
493
+
494
+ model_name = model_info.get("name")
495
+ if not model_name:
496
+ raise HTTPException(status_code=404, detail=f"Model '{request.model}' has no 'name' field.")
497
+
498
+ if not request.messages:
499
+ raise HTTPException(status_code=400, detail="No messages provided in the request.")
500
+
501
+ log_debug(f"Processing request for model: {request.model} (Yupp name: {model_name})")
502
+
503
+ # 格式化消息
504
+ question = format_messages_for_yupp(request.messages)
505
+ log_debug(f"Formatted question: {question[:100]}...")
506
+
507
+ # 尝试所有账户
508
+ for attempt in range(len(YUPP_ACCOUNTS)):
509
+ account = get_best_yupp_account()
510
+ if not account:
511
+ raise HTTPException(
512
+ status_code=503,
513
+ detail="No valid Yupp.ai accounts available."
514
+ )
515
+
516
+ try:
517
+ # 构建请求
518
+ url_uuid = str(uuid.uuid4())
519
+ url = f"https://yupp.ai/chat/{url_uuid}"
520
+
521
+ payload = [
522
+ url_uuid,
523
+ str(uuid.uuid4()),
524
+ question,
525
+ "$undefined",
526
+ "$undefined",
527
+ [],
528
+ "$undefined",
529
+ [{"modelName": model_name, "promptModifierId": "$undefined"}],
530
+ "text",
531
+ False,
532
+ ]
533
+
534
+ headers = {
535
+ "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 Edg/137.0.0.0",
536
+ "Accept": "text/x-component",
537
+ "Accept-Encoding": "gzip, deflate, br, zstd",
538
+ "Content-Type": "application/json",
539
+ "next-action": "7f48888536e2f0c0163640837db291777c39cc40c3",
540
+ "sec-fetch-site": "same-origin",
541
+ "Cookie": f"__Secure-yupp.session-token={account['token']}",
542
+ }
543
+
544
+ log_debug(f"Sending request to Yupp.ai with account token ending in ...{account['token'][-4:]}")
545
+
546
+ # 发送请求
547
+ response = requests.post(
548
+ url,
549
+ data=json.dumps(payload),
550
+ headers=headers,
551
+ stream=True
552
+ )
553
+ response.raise_for_status()
554
+
555
+ # 处理响应
556
+ if request.stream:
557
+ log_debug("Returning processed response stream")
558
+ return StreamingResponse(
559
+ yupp_stream_generator(response.iter_lines(), request.model, account),
560
+ media_type="text/event-stream",
561
+ headers={
562
+ "Cache-Control": "no-cache",
563
+ "Connection": "keep-alive",
564
+ "X-Accel-Buffering": "no",
565
+ },
566
+ )
567
+ else:
568
+ log_debug("Building non-stream response")
569
+
570
+ return build_yupp_non_stream_response(response.iter_lines(), model_name, account)
571
+
572
+ except requests.exceptions.HTTPError as e:
573
+ status_code = e.response.status_code
574
+ error_detail = e.response.text
575
+ print(f"Yupp.ai API error ({status_code}): {error_detail}")
576
+
577
+ with account_rotation_lock:
578
+ if status_code in [401, 403]:
579
+ account["is_valid"] = False
580
+ print(f"Account ...{account['token'][-4:]} marked as invalid due to auth error.")
581
+ elif status_code in [429, 500, 502, 503, 504]:
582
+ account["error_count"] += 1
583
+ print(f"Account ...{account['token'][-4:]} error count: {account['error_count']}")
584
+ else:
585
+ # 客户端错误,不尝试使用其他账户
586
+ raise HTTPException(status_code=status_code, detail=error_detail)
587
+
588
+ except Exception as e:
589
+ print(f"Request error: {e}")
590
+ with account_rotation_lock:
591
+ account["error_count"] += 1
592
+
593
+ # 所有尝试都失败
594
+ raise HTTPException(status_code=503, detail="All attempts to contact Yupp.ai API failed.")
595
+
596
+
597
+ async def error_stream_generator(error_detail: str, status_code: int):
598
+ """生成错误流响应"""
599
+ yield f'data: {json.dumps({"error": {"message": error_detail, "type": "yupp_api_error", "code": status_code}})}\n\n'
600
+ yield "data: [DONE]\n\n"
601
+
602
+
603
+ if __name__ == "__main__":
604
+ import uvicorn
605
+
606
+ # 设置环境变量以启用调试模式
607
+ if os.environ.get("DEBUG_MODE", "").lower() == "true":
608
+ DEBUG_MODE = True
609
+ print("Debug mode enabled via environment variable")
610
+
611
+ if not os.path.exists("yupp.json"):
612
+ print("Warning: yupp.json not found. Creating a dummy file.")
613
+ dummy_data = [
614
+ {
615
+ "token": "your_yupp_session_token_here",
616
+ }
617
+ ]
618
+ with open("yupp.json", "w", encoding="utf-8") as f:
619
+ json.dump(dummy_data, f, indent=4)
620
+ print("Created dummy yupp.json. Please replace with valid Yupp.ai data.")
621
+
622
+ if not os.path.exists("client_api_keys.json"):
623
+ print("Warning: client_api_keys.json not found. Creating a dummy file.")
624
+ dummy_key = f"sk-dummy-{uuid.uuid4().hex}"
625
+ with open("client_api_keys.json", "w", encoding="utf-8") as f:
626
+ json.dump([dummy_key], f, indent=2)
627
+ print(f"Created dummy client_api_keys.json with key: {dummy_key}")
628
+
629
+ if not os.path.exists("model.json"):
630
+ print("Warning: model.json not found. Creating a dummy file.")
631
+ dummy_models = [
632
+ {
633
+ "id": "claude-3.7-sonnet:thinking",
634
+ "name": "anthropic/claude-3.7-sonnet:thinking<>OPR",
635
+ "label": "Claude 3.7 Sonnet (Thinking) (OpenRouter)",
636
+ "publisher": "Anthropic",
637
+ "family": "Claude"
638
+ }
639
+ ]
640
+ with open("model.json", "w", encoding="utf-8") as f:
641
+ json.dump(dummy_models, f, indent=4)
642
+ print("Created dummy model.json.")
643
+
644
+ load_client_api_keys()
645
+ load_yupp_accounts()
646
+ load_yupp_models()
647
+
648
+ print("\n--- Yupp.ai OpenAI API Adapter ---")
649
+ print(f"Debug Mode: {DEBUG_MODE}")
650
+ print("Endpoints:")
651
+ print(" GET /v1/models (Client API Key Auth)")
652
+ print(" GET /models (No Auth)")
653
+ print(" POST /v1/chat/completions (Client API Key Auth)")
654
+ print(" GET /debug?enable=[true|false] (Toggle Debug Mode)")
655
+
656
+ print(f"\nClient API Keys: {len(VALID_CLIENT_KEYS)}")
657
+ if YUPP_ACCOUNTS:
658
+ print(f"Yupp.ai Accounts: {len(YUPP_ACCOUNTS)}")
659
+ else:
660
+ print("Yupp.ai Accounts: None loaded. Check yupp.json.")
661
+ if YUPP_MODELS:
662
+ models = sorted([m.get("label", m.get("id", "unknown")) for m in YUPP_MODELS])
663
+ print(f"Yupp.ai Models: {len(YUPP_MODELS)}")
664
+ print(f"Available models: {', '.join(models[:5])}{'...' if len(models) > 5 else ''}")
665
+ else:
666
+ print("Yupp.ai Models: None loaded. Check model.json.")
667
+ print("------------------------------------")
668
+
669
+ uvicorn.run(app, host="0.0.0.0", port=8000)